Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2f5736d
Track Agent Host edit sources
amunger Jul 15, 2026
c2cf4c5
Fix Agent Host edit telemetry forwarding
amunger Jul 15, 2026
76c2dfb
Skip Agent Host attribution for dirty files
amunger Jul 15, 2026
e29e864
agentHost: unblock disconnected client tool calls
connor4312 Jul 15, 2026
da9bbcf
agentHost: defer interactive MCP client registration
connor4312 Jul 15, 2026
cf790c0
Merge pull request #326049 from microsoft/connor/fix-client-tool-disc…
connor4312 Jul 15, 2026
48fb220
Merge pull request #326055 from microsoft/connor/defer-mcp-dynamic-re…
connor4312 Jul 15, 2026
c54ca24
sessions: prompt timeline UX improvements (#326023)
osortega Jul 15, 2026
ee42cd1
Remove low-usage tools from vscode-general tool set (#326056)
bhavyaus Jul 15, 2026
ed83513
Default agents.voice.handsFree to false (#326046)
Copilot Jul 15, 2026
d4ea5d4
Add edit tracker characterization tests (#326062)
amunger Jul 15, 2026
b7d0a0b
Fix remote Agent Host dependency packaging (#326019)
roblourens Jul 15, 2026
7385ad1
Track Agent Host edit sources
amunger Jul 15, 2026
ddf8ca2
Fix Agent Host edit telemetry forwarding
amunger Jul 15, 2026
d3e7eaf
Skip Agent Host attribution for dirty files
amunger Jul 15, 2026
b3df4d7
Add unified edit document reconciler
amunger Jul 15, 2026
96a336a
Add unified edit document registry
amunger Jul 15, 2026
39e4741
Adapt Agent Host tracking to SCM interface
amunger Jul 15, 2026
d6f9913
reconciler for tracked edits
amunger Jul 16, 2026
c36b3da
Register unified edit tracker shadow
amunger Jul 16, 2026
5ea1f41
Compare unified edit tracker shadow
amunger Jul 16, 2026
78dec58
Unify long-term edit source tracking
amunger Jul 17, 2026
a590cd7
Remove redundant edit tracking scope
amunger Jul 17, 2026
7eaac75
Merge remote edit attribution history
amunger Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/skills/sessions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Then read the relevant spec for the area you are changing (see table below). If
- **Every untitled-session-title fallback must be quick-chat aware**: an untitled session's title observable is `''`, so a hardcoded `localize(…, "New Session")` fallback shows "New Session" even for a quick chat (whose composer says "New Chat"). Route **all** such fallbacks through the shared `getUntitledSessionTitle(isQuickChat)` helper (`services/sessions/common/session.ts`, boolean param so each caller controls reader-tracked `.read(reader)` vs `.get()`). There are ≥5 sites — titlebar (`sessionsTitleBarWidget`), session header (×2: title + rename placeholder), list-row hover (`sessionHoverContent`), sessions picker (`sessionsActions`) — keep them on the helper; never hardcode "New Session". (The Cmd+N *action* title stays "New Session" — that action creates a session, unrelated to a session's own title.)
- **`NeedsInput` is still an active turn for live turn UI**: agent-host tool and input confirmations intentionally transition a running chat from `InProgress` to `NeedsInput` without ending `activeTurn`. Live status surfaces such as the chat input pills must use `isActiveSessionStatus` so they do not disappear until the next output returns the chat to `InProgress`.
- **Agent-host-only exclusions for built-in client tools belong in `ClientToolSetsContribution`, not the global tool registration**: `AgentHostActiveClientService.getClientTools` advertises enabled members of every non-deprecated tool set, including extension-contributed sets. Omit an unsupported built-in tool from the client tool sets so normal Copilot chat can continue using it; do not treat this contribution as the sole Agent Host allowlist.
- **Non-interactive MCP authentication probes must not create dynamic authentication providers**: Provider creation can prompt for manual client registration when dynamic registration is unsupported. With `allowInteraction: false`, only inspect existing providers and sessions; defer metadata discovery and provider creation until the user invokes the `mcpAuthenticationRequired` action.

## Capturing Feedback (meta-rule)

Expand Down
129 changes: 129 additions & 0 deletions build/azure-pipelines/common/smokeTestAgentHost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type ChildProcess, fork } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { setTimeout as delay } from 'timers/promises';

const startupTimeoutMs = 30_000;
const shutdownTimeoutMs = 5_000;
const readyPattern = /Agent host server listening on \S+/;

async function main(serverRoot: string | undefined): Promise<void> {
if (!serverRoot) {
throw new Error('Usage: node smokeTestAgentHost.ts <server-root>');
}

const nodePath = path.join(serverRoot, process.platform === 'win32' ? 'node.exe' : 'node');
const bootstrapPath = path.join(serverRoot, 'out', 'bootstrap-fork.js');
for (const requiredPath of [nodePath, bootstrapPath]) {
if (!fs.existsSync(requiredPath)) {
throw new Error(`Packaged Agent Host dependency not found: ${requiredPath}`);
}
}

const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-agent-host-smoke-'));
const logsPath = path.join(temporaryRoot, 'logs');
const userDataPath = path.join(temporaryRoot, 'user-data');
fs.mkdirSync(logsPath, { recursive: true });
fs.mkdirSync(userDataPath, { recursive: true });

const child = fork(bootstrapPath, [
'--type=agentHost',
'--logsPath', logsPath,
'--user-data-dir', userDataPath,
'--disable-telemetry',
], {
cwd: serverRoot,
env: {
...process.env,
VSCODE_DEV: undefined,
VSCODE_ESM_ENTRYPOINT: 'vs/platform/agentHost/node/agentHostMain',
VSCODE_AGENT_HOST_CONNECTION_TOKEN: undefined,
VSCODE_AGENT_HOST_HOST: '127.0.0.1',
VSCODE_AGENT_HOST_PORT: '0',
VSCODE_AGENT_HOST_SOCKET_PATH: undefined,
VSCODE_NLS_CONFIG: JSON.stringify({
userLocale: 'en',
osLocale: 'en',
resolvedLanguage: 'en',
defaultMessagesFile: path.join(serverRoot, 'out', 'nls.messages.json'),
}),
VSCODE_PIPE_LOGGING: 'false',
VSCODE_VERBOSE_LOGGING: 'false',
},
execPath: nodePath,
silent: true,
});

try {
await waitForReady(child);
console.log('Packaged Agent Host started successfully.');
} finally {
await stopProcess(child);
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
}

function waitForReady(child: ChildProcess): Promise<void> {
return new Promise((resolve, reject) => {
let output = '';
let settled = false;

const finish = (error?: Error) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timeout);
if (error) {
reject(error);
} else {
resolve();
}
};

const appendOutput = (data: Buffer) => {
const text = data.toString();
process.stdout.write(text);
output = `${output}${text}`.slice(-64 * 1024);
if (readyPattern.test(output)) {
finish();
}
};

child.stdout?.on('data', appendOutput);
child.stderr?.on('data', appendOutput);
child.once('error', error => finish(error));
child.once('exit', (code, signal) => {
finish(new Error(`Packaged Agent Host exited before becoming ready (code: ${code}, signal: ${signal}).\n${output}`));
});

const timeout = setTimeout(() => {
finish(new Error(`Timed out after ${startupTimeoutMs}ms waiting for the packaged Agent Host to start.\n${output}`));
}, startupTimeoutMs);
});
}

async function stopProcess(child: ChildProcess): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) {
return;
}

const exited = new Promise<void>(resolve => child.once('exit', () => resolve()));
child.kill();
await Promise.race([exited, delay(shutdownTimeoutMs)]);
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGKILL');
await exited;
}
}

main(process.argv[2]).catch(error => {
console.error(error);
process.exit(1);
});
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ steps:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
displayName: Build server

- ${{ if eq(parameters.VSCODE_ARCH, 'arm64') }}:
- script: node build/azure-pipelines/common/smokeTestAgentHost.ts "$(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)"
displayName: 🧪 Smoke test packaged Agent Host

- script: |
set -e
npm run gulp vscode-reh-web-darwin-$(VSCODE_ARCH)-min-ci
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ steps:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
displayName: Build server

- ${{ if eq(parameters.VSCODE_ARCH, 'x64') }}:
- script: node build/azure-pipelines/common/smokeTestAgentHost.ts "$(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)"
displayName: 🧪 Smoke test packaged Agent Host

- script: |
set -e
npm run gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ steps:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
displayName: Build server

- ${{ if eq(parameters.VSCODE_ARCH, 'x64') }}:
- powershell: node build/azure-pipelines/common/smokeTestAgentHost.ts "$(Agent.BuildDirectory)\vscode-server-win32-$(VSCODE_ARCH)"
displayName: 🧪 Smoke test packaged Agent Host

- powershell: |
. build/azure-pipelines/win32/exec.ps1
$ErrorActionPreference = "Stop"
Expand Down
136 changes: 136 additions & 0 deletions build/lib/test/agentHostDependencies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import fs from 'fs';
import { builtinModules } from 'module';
import path from 'path';
import { suite, test } from 'node:test';
import * as ts from 'typescript';

const repositoryRoot = path.resolve(import.meta.dirname, '../../..');
const agentHostEntryPoints = [
'src/vs/platform/agentHost/node/agentHostMain.ts',
'src/vs/platform/agentHost/node/agentHostServerMain.ts',
];

suite('Agent Host dependencies', () => {
test('runtime packages are included in the remote server', () => {
const remotePackageJson = JSON.parse(fs.readFileSync(path.join(repositoryRoot, 'remote/package.json'), 'utf8')) as {
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};
const packagedDependencies = new Set([
...Object.keys(remotePackageJson.dependencies ?? {}),
...Object.keys(remotePackageJson.optionalDependencies ?? {}),
]);
const runtimeImports = collectRuntimePackageImports(
agentHostEntryPoints.map(entryPoint => path.join(repositoryRoot, entryPoint))
);
const missingDependencies = [...runtimeImports]
.filter(([packageName]) => !packagedDependencies.has(packageName))
.map(([packageName, importers]) => `${packageName}: ${[...importers].sort().map(importer => path.relative(repositoryRoot, importer)).join(', ')}`)
.sort();

assert.deepStrictEqual(missingDependencies, []);
});
});

function collectRuntimePackageImports(entryPoints: readonly string[]): Map<string, Set<string>> {
const pendingFiles = [...entryPoints];
const visitedFiles = new Set<string>();
const packageImports = new Map<string, Set<string>>();
const builtInModules = new Set([...builtinModules, ...builtinModules.map(moduleName => `node:${moduleName}`)]);

while (pendingFiles.length > 0) {
const file = pendingFiles.pop()!;
if (visitedFiles.has(file)) {
continue;
}
visitedFiles.add(file);

const sourceFile = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
for (const moduleSpecifier of getRuntimeModuleSpecifiers(sourceFile)) {
if (moduleSpecifier.startsWith('.')) {
const importedFile = resolveSourceImport(file, moduleSpecifier);
if (importedFile) {
pendingFiles.push(importedFile);
}
continue;
}

if (builtInModules.has(moduleSpecifier)) {
continue;
}

const packageName = getPackageName(moduleSpecifier);
let importers = packageImports.get(packageName);
if (!importers) {
importers = new Set<string>();
packageImports.set(packageName, importers);
}
importers.add(file);
}
}

return packageImports;
}

function getRuntimeModuleSpecifiers(sourceFile: ts.SourceFile): string[] {
const moduleSpecifiers: string[] = [];
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && isRuntimeImport(statement)) {
moduleSpecifiers.push(statement.moduleSpecifier.text);
} else if (ts.isExportDeclaration(statement) && !statement.isTypeOnly && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
moduleSpecifiers.push(statement.moduleSpecifier.text);
}
}

const visit = (node: ts.Node): void => {
if (
ts.isCallExpression(node)
&& ts.isIdentifier(node.expression)
&& (node.expression.text === 'require' || node.expression.text === 'nativeRequire')
&& node.arguments.length === 1
&& ts.isStringLiteral(node.arguments[0])
) {
moduleSpecifiers.push(node.arguments[0].text);
}
ts.forEachChild(node, visit);
};
visit(sourceFile);

return moduleSpecifiers;
}

function isRuntimeImport(statement: ts.ImportDeclaration): boolean {
const clause = statement.importClause;
if (!clause) {
return true;
}
if (clause.isTypeOnly) {
return false;
}
if (clause.name || !clause.namedBindings || ts.isNamespaceImport(clause.namedBindings)) {
return true;
}
return clause.namedBindings.elements.some(element => !element.isTypeOnly);
}

function resolveSourceImport(importer: string, moduleSpecifier: string): string | undefined {
const unresolvedPath = path.resolve(path.dirname(importer), moduleSpecifier);
const candidates = [
unresolvedPath,
unresolvedPath.replace(/\.js$/, '.ts'),
unresolvedPath.replace(/\.js$/, '.tsx'),
path.join(unresolvedPath, 'index.ts'),
];
return candidates.find(candidate => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
}

function getPackageName(moduleSpecifier: string): string {
const segments = moduleSpecifier.split('/');
return moduleSpecifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
}
20 changes: 20 additions & 0 deletions remote/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions remote/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@parcel/watcher": "^2.5.6",
"@vscode/copilot-api": "^0.4.2",
"@vscode/deviceid": "^0.1.1",
"@vscode/fs-copyfile": "2.0.0",
"@vscode/iconv-lite-umd": "0.7.1",
"@vscode/native-watchdog": "^1.4.6",
"@vscode/proxy-agent": "^0.44.0",
Expand Down Expand Up @@ -65,6 +66,7 @@
"node-pty@1.2.0-beta.13": true,
"@parcel/watcher@2.5.6": true,
"@vscode/deviceid@0.1.5": true,
"@vscode/fs-copyfile@2.0.0": true,
"@vscode/native-watchdog@1.4.6": true,
"@vscode/spdlog@0.15.8": true,
"@vscode/windows-registry@1.2.0": true,
Expand Down
4 changes: 4 additions & 0 deletions src/vs/editor/common/textModelEditSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,16 @@ export const EditSources = {
mode: string | undefined;
extensionId: VersionedExtensionId | undefined;
codeBlockSuggestionId: EditSuggestionId | undefined;
harness?: string;
origin?: string;
}) {
return createEditSource({
source: 'Chat.applyEdits',
$modelId: avoidPathRedaction(data.modelId),
$extensionId: data.extensionId?.extensionId,
$extensionVersion: data.extensionId?.version,
$harness: data.harness,
$origin: data.origin,
$$languageId: data.languageId,
$$sessionId: data.sessionId,
$$requestId: data.requestId,
Expand Down
5 changes: 5 additions & 0 deletions src/vs/platform/agentHost/common/pendingRequestRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export class PendingRequestRegistry<T> {
}
}

/** Whether a result arrived before a request registered under `key`. */
hasBufferedResult(key: string): boolean {
return this._earlyResults.has(key);
}

/**
* Resolve every parked deferred with `denyValue` and clear the registry.
*
Expand Down
Loading
Loading