Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
44da12a
fix: skip stale category-progress DOM elements during walkthrough unr…
vs-code-engineering[bot] May 8, 2026
4b27aff
Remove unused export const (#315244)
Tyriar May 8, 2026
615782e
Approval telemetry (#315215)
chrmarti May 8, 2026
152ffe5
agentHost: support image and blob user-message attachments
connor4312 May 8, 2026
3b8129f
Strip codicons from terminal quickpick filter matching (#313197)
yogeshwaran-c May 8, 2026
6471ef7
Escapes model id in chatEditHunk event
hediet May 8, 2026
d1ace9a
Fixes tests
hediet May 8, 2026
b72c91b
agentHost: rewrite Resource attachments and omit undefined fields
connor4312 May 8, 2026
10e5df7
Merge remote-tracking branch 'origin/main' into connor4312/315137
connor4312 May 8, 2026
0c161d3
sessions: fix Customizations single-entry width overflow (#315125)
joshspicer May 8, 2026
5c97d33
agentHost: revert undefined-field omission, update tests instead
connor4312 May 8, 2026
1a8d9d0
Add proposal for custom editor diff/merge priority
mjbvz May 8, 2026
79b6fb7
chat: remove 'Bridged' badge from MCP servers in AI Customizations UI…
joshspicer May 8, 2026
dd3ad60
Replace "Agents app" with "Agents window" in user-facing strings (#31…
sandy081 May 8, 2026
1242adc
sessions: restore last active session on reload (#315312)
sandy081 May 8, 2026
25035fb
fixes https://github.com/microsoft/vscode/issues/291188 (#314713)
Larsjep May 8, 2026
b0e2250
chat: hide plugin actions for synced customization items (#315320)
joshspicer May 8, 2026
56edb5e
Merge pull request #315321 from mjbvz/dev/mjbvz/grieving-toad
mjbvz May 8, 2026
e1106b6
agentHost: support image and blob user-message attachments (#315311)
connor4312 May 8, 2026
ca900cb
broaden definition of effectively empty, but only when re-selecting t…
amunger May 8, 2026
2ed505c
telemetry: add compressOutputEnabled to toolUse.runInTerminal (#315328)
meganrogge May 8, 2026
917766f
chat: gate agent host input completions on trigger characters (#315327)
connor4312 May 8, 2026
00ece6e
Stop chat input notification from announcing repeatedly to screen rea…
Copilot May 8, 2026
5187b7b
sessions: use codicon spinner in new chat input (#315332)
hawkticehurst May 8, 2026
ef1ce3e
sessions: round chat send button (#315330)
hawkticehurst May 8, 2026
e85a829
Add AHP transport JSONL logging (#315129)
roblourens May 8, 2026
6643651
chat: avoid Open Agents Window keybinding conflict in screen reader m…
meganrogge May 8, 2026
95648f1
Bump router decision timeout from 1s to 2.5s
May 8, 2026
cbd0c28
Fix build break in chatInputNotificationService announce (#315360)
dmitrivMS May 8, 2026
50069b3
Update routerTimeout test to match new 2.5s timeout
May 8, 2026
11ce4e2
Agents - add "Sync Changes" action to the new session screen in the F…
lszomoru May 8, 2026
977ad51
sessions: address Copilot review feedback on session restore (#315354)
sandy081 May 8, 2026
b55aa82
Strip Markdown when announcing chat input notification to screen read…
dmitrivMS May 8, 2026
0e2919b
Refactor browser element selection to be event-based (#315362)
kycutler May 8, 2026
a5922ee
Merge pull request #315364 from microsoft/aashnagarg/router-timeout-bump
aashna May 8, 2026
44669e9
Run copilot extension lint in PR CI (#315368)
dmitrivMS May 8, 2026
21b182d
Fixed tokenizer bug for customOAI models (#315370)
vikramnitin9 May 8, 2026
94ef84c
Agents - fix regression related to showing the "Create Pull Request" …
lszomoru May 8, 2026
7dfe537
Merge pull request #315298 from microsoft/fix/welcome-category-not-fo…
bryanchen-d May 8, 2026
688549d
Scope model picker changes to UBB only (#315372)
pwang347 May 8, 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
8 changes: 8 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ jobs:
working-directory: extensions/copilot
run: npm run typecheck

- name: Lint
working-directory: extensions/copilot
run: npm run lint

- name: Compile
working-directory: extensions/copilot
run: npm run compile
Expand Down Expand Up @@ -407,6 +411,10 @@ jobs:
working-directory: extensions/copilot
run: npm run typecheck

- name: Lint
working-directory: extensions/copilot
run: npm run lint

- name: Compile
working-directory: extensions/copilot
run: npm run compile
Expand Down
4 changes: 2 additions & 2 deletions extensions/copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5159,12 +5159,12 @@
},
{
"command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR",
"when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && sessions.hasGitHubRemote && !sessions.hasPullRequest && !sessions.isAgentHostSession",
"when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && sessions.hasGitHubRemote && !sessions.hasPullRequest && (sessions.hasUncommittedChanges || sessions.hasOutgoingChanges) && !sessions.isAgentHostSession",
"group": "2_pull_request@1"
},
{
"command": "github.copilot.chat.createDraftPullRequestCopilotCLIAgentSession.createDraftPR",
"when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && sessions.hasGitHubRemote && !sessions.hasPullRequest && !sessions.isAgentHostSession",
"when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && sessions.hasGitHubRemote && !sessions.hasPullRequest && (sessions.hasUncommittedChanges || sessions.hasOutgoingChanges) && !sessions.isAgentHostSession",
"group": "2_pull_request@2"
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ export function sendInvokedToolTelemetry(instantiationService: IInstantiationSer
// matching the prior behavior with modelMaxPromptTokens: Infinity
const endpointWithUnlimitedBudget: IChatEndpoint = {
...endpoint,
tokenizer: endpoint.tokenizer,
modelMaxPromptTokens: Infinity,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ export class GetNewWorkspaceTool implements ICopilotTool<INewWorkspaceToolParams
) { }

/**
* A folder is treated as empty for workspace creation purposes if it contains
* nothing other than a `.git` entry, so that newly cloned/initialized repos
* can still be used as a target for scaffolding.
* Used as a softer "empty" check for the case where the user re-selects the
* already-open workspace folder. Treats a folder as empty if every top-level
* entry name starts with `.` (e.g. `.git`, `.gitignore`, `.vscode`,
* `.editorconfig`), so that newly cloned/initialized repos and folders that
* only contain dotfile config can be used in place without reopening.
*/
private async _isEffectivelyEmpty(folder: Uri): Promise<boolean> {
const entries = await this.fileSystemService.readDirectory(folder);
return entries.every(([name]) => name === '.git');
return entries.every(([name]) => name.startsWith('.'));
}

async prepareInvocation?(options: LanguageModelToolInvocationPrepareOptions<INewWorkspaceToolParams>, token: CancellationToken): Promise<PreparedToolInvocation> {
Expand All @@ -57,7 +59,7 @@ export class GetNewWorkspaceTool implements ICopilotTool<INewWorkspaceToolParams
this._shouldPromptWorkspaceOpen = true;
}
else if (workspace && workspace.length > 0) {
this._shouldPromptWorkspaceOpen = !await this._isEffectivelyEmpty(workspace[0]);
this._shouldPromptWorkspaceOpen = (await this.fileSystemService.readDirectory(workspace[0])).length > 0;
}
if (this._shouldPromptWorkspaceOpen) {
const confirmationMessages = {
Expand Down Expand Up @@ -86,13 +88,26 @@ export class GetNewWorkspaceTool implements ICopilotTool<INewWorkspaceToolParams

if (this._shouldPromptWorkspaceOpen) {
const newWorkspaceUri = (await this.dialogService.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Select an Empty Workspace Folder' }))?.[0];
if (newWorkspaceUri && !extUri.isEqual(newWorkspaceUri, workspaceUri)) {
if (!newWorkspaceUri) {
return new LanguageModelToolResult([
new LanguageModelTextPart('The user has not opened a valid workspace folder in VS Code. Ask them to open an empty folder before continuing.')
]);
}

if (workspaceUri && extUri.isEqual(newWorkspaceUri, workspaceUri)) {
// User re-selected the already-open folder: if it only contains
// dotfile entries, use it in place without reopening the window.
if (!await this._isEffectivelyEmpty(newWorkspaceUri)) {
return new LanguageModelToolResult([
new LanguageModelTextPart('The user has not opened a valid workspace folder in VS Code. Ask them to open an empty folder before continuing.')
]);
}
} else {
if ((await this.fileSystemService.readDirectory(newWorkspaceUri)).length > 0) {
return new LanguageModelToolResult([
new LanguageModelTextPart('The user has not opened a valid workspace folder in VS Code. Ask them to open an empty folder before continuing.')
]);
}

saveNewWorkspaceContext({
workspaceURI: newWorkspaceUri.toString(),
Expand All @@ -109,10 +124,6 @@ export class GetNewWorkspaceTool implements ICopilotTool<INewWorkspaceToolParams
new LanguageModelTextPart(`The user is opening the folder ${newWorkspaceUri.toString()}. Do not proceed with project generation till the user has confirmed opening the folder.`)
]);
}

return new LanguageModelToolResult([
new LanguageModelTextPart('The user has not opened a valid workspace folder in VS Code. Ask them to open an empty folder before continuing.')
]);
}

if (!workspaceUri) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class RouterDecisionFetcher {
const copilotToken = copilotTokenObj.token;
requestBody.copilot_plan = copilotTokenObj.rawCopilotPlan;
const abortController = new AbortController();
const timeout = setTimeout(() => abortController.abort(), 1000);
const timeout = setTimeout(() => abortController.abort(), 2500);
let response: Response;
try {
response = await this._capiClientService.makeRequest<Response>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,8 +553,8 @@ describe('AutomodeService', () => {
};

const resultPromise = automodeService.resolveAutoModeEndpoint(chatRequest as ChatRequest, [claudeEndpoint, gpt4oEndpoint]);
// Advance past the 1-second router timeout to trigger the abort
await vi.advanceTimersByTimeAsync(1000);
// Advance past the 2.5-second router timeout to trigger the abort
await vi.advanceTimersByTimeAsync(2500);

const result = await resultPromise;
// Should fall back to first available model (claude-sonnet)
Expand Down
6 changes: 5 additions & 1 deletion extensions/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3438,7 +3438,11 @@
"git.confirmSync": {
"type": "boolean",
"description": "%config.confirmSync%",
"default": true
"default": true,
"agentsWindow": {
"default": false,
"readOnly": true
}
},
"git.confirmCommittedDelete": {
"type": "boolean",
Expand Down
14 changes: 12 additions & 2 deletions src/vs/platform/agentHost/browser/remoteAgentHostServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import { Emitter } from '../../../base/common/event.js';
import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
import { DeferredPromise, raceTimeout } from '../../../base/common/async.js';
import { ConfigurationTarget, IConfigurationService } from '../../configuration/common/configuration.js';
import { IEnvironmentService } from '../../environment/common/environment.js';
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
import { ILabelService } from '../../label/common/label.js';
import { ILogService } from '../../log/common/log.js';

import type { IAgentConnection } from '../common/agentService.js';
import { AgentHostAhpJsonlLoggingSettingId, type IAgentConnection } from '../common/agentService.js';
import {
IRemoteAgentHostService,
RemoteAgentHostConnectionStatus,
Expand Down Expand Up @@ -84,6 +85,7 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ILogService private readonly _logService: ILogService,
@ILabelService private readonly _labelService: ILabelService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
) {
super();

Expand Down Expand Up @@ -385,7 +387,15 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo
}

const store = new DisposableStore();
const transport = store.add(new WebSocketClientTransport(address, connectionToken));
const ahpLoggingEnabled = !!this._configurationService.getValue<boolean>(AgentHostAhpJsonlLoggingSettingId);
const transport = store.add(this._instantiationService.createInstance(
WebSocketClientTransport,
address,
connectionToken,
ahpLoggingEnabled
? { logsHome: this._environmentService.logsHome, connectionId: address, transport: 'websocket' }
: undefined,
));
const client = store.add(this._instantiationService.createInstance(RemoteAgentHostProtocolClient, address, transport));
const entry: IConnectionEntry = { store, client, connected: false, status: RemoteAgentHostConnectionStatus.connecting };
this._entries.set(address, entry);
Expand Down
20 changes: 16 additions & 4 deletions src/vs/platform/agentHost/browser/webSocketClientTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import { Emitter } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { connectionTokenQueryName } from '../../../base/common/network.js';
import type { AhpServerNotification, JsonRpcResponse, ProtocolMessage } from '../common/state/sessionProtocol.js';
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
import { AhpJsonlLogger, getAhpLogByteLength, IAhpJsonlLoggerOptions } from '../common/ahpJsonlLogger.js';
import type { AhpServerNotification, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ProtocolMessage } from '../common/state/sessionProtocol.js';
import type { IClientTransport } from '../common/state/sessionTransport.js';
import { MALFORMED_FRAMES_FORCE_CLOSE_THRESHOLD, MALFORMED_FRAMES_LOG_CAP } from '../common/transportConstants.js';

Expand Down Expand Up @@ -41,12 +43,19 @@ export class WebSocketClientTransport extends Disposable implements IClientTrans
return this._ws?.readyState === WebSocket.OPEN;
}

private readonly _ahpLogger?: AhpJsonlLogger;

constructor(
private readonly _address: string,
private readonly _connectionToken?: string,
private readonly _connectionToken: string | undefined,
ahpLogOptions: IAhpJsonlLoggerOptions | undefined,
@IInstantiationService instantiationService: IInstantiationService,
) {
// TODO: @osortega remove console.logs
super();
if (ahpLogOptions) {
this._ahpLogger = this._register(instantiationService.createInstance(AhpJsonlLogger, ahpLogOptions));
}
}

/**
Expand Down Expand Up @@ -138,6 +147,7 @@ export class WebSocketClientTransport extends Disposable implements IClientTrans
}
return;
}
this._ahpLogger?.log(message, 's2c', getAhpLogByteLength(text));
this._onMessage.fire(message);
});

Expand Down Expand Up @@ -165,9 +175,11 @@ export class WebSocketClientTransport extends Disposable implements IClientTrans
* transport is force-closed so reconnection is triggered immediately
* rather than silently losing messages.
*/
send(message: ProtocolMessage | AhpServerNotification | JsonRpcResponse): boolean {
send(message: ProtocolMessage | AhpServerNotification | JsonRpcNotification | JsonRpcResponse | JsonRpcRequest): boolean {
if (this._ws?.readyState === WebSocket.OPEN) {
this._ws.send(JSON.stringify(message));
const text = JSON.stringify(message);
this._ahpLogger?.log(message, 'c2s', getAhpLogByteLength(text));
this._ws.send(text);
return true;
}
console.warn(
Expand Down
3 changes: 3 additions & 0 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export const AgentHostEnabledSettingId = 'chat.agentHost.enabled';
/** Configuration key that controls whether per-host IPC traffic output channels are created. */
export const AgentHostIpcLoggingSettingId = 'chat.agentHost.ipcLoggingEnabled';

/** Configuration key that controls whether AHP transport JSONL logs are written. */
export const AgentHostAhpJsonlLoggingSettingId = 'chat.agentHost.ahpJsonlLoggingEnabled';

/**
* Configuration key that holds the absolute path to a locally-installed
* `@anthropic-ai/claude-agent-sdk` package. When non-empty, the Claude agent
Expand Down
135 changes: 135 additions & 0 deletions src/vs/platform/agentHost/common/ahpJsonlLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { VSBuffer } from '../../../base/common/buffer.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { joinPath } from '../../../base/common/resources.js';
import { URI } from '../../../base/common/uri.js';
import { IFileService, IFileStatWithMetadata } from '../../files/common/files.js';
import { ILogService } from '../../log/common/log.js';

export type AhpLogDirection = 'c2s' | 's2c';

export interface IAhpJsonlLoggerOptions {
readonly logsHome: URI;
readonly connectionId: string;
readonly transport: string;
readonly maxFileSizeBytes?: number;
readonly maxFiles?: number;
}

const AHP_LOG_DIR = 'ahp';
const DEFAULT_MAX_FILE_SIZE_BYTES = 75 * 1024 * 1024;
const DEFAULT_MAX_FILES = 5;

export class AhpJsonlLogger extends Disposable {

private readonly _directory: URI;
private readonly _baseName: string;
private readonly _maxFileSizeBytes: number;
private readonly _maxFiles: number;
private _currentFile: URI;
private _currentSize = 0;
private _segment = 0;
private _queue = Promise.resolve();
private _folderCreated: Promise<IFileStatWithMetadata> | undefined;

constructor(
private readonly _options: IAhpJsonlLoggerOptions,
@IFileService private readonly _fileService: IFileService,
@ILogService private readonly _logService: ILogService,
) {
super();
this._directory = joinPath(this._options.logsHome, AHP_LOG_DIR);
// Truncate connectionId to avoid filesystem filename length limits (e.g. 255 on ext4/APFS)
const safeConnectionId = sanitizeFilePart(this._options.connectionId).slice(0, 64);
this._baseName = `ahp-${toFileTimestamp(new Date())}-${safeConnectionId}.jsonl`;
this._maxFileSizeBytes = this._options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
this._maxFiles = this._options.maxFiles ?? DEFAULT_MAX_FILES;
this._currentFile = joinPath(this._directory, this._baseName);
}

get resource(): URI {
return this._currentFile;
}

log(message: object, dir: AhpLogDirection, byteLength?: number): void {
const entry = {
...message,
_ahpLog: {
ts: new Date().toISOString(),
dir,
connectionId: this._options.connectionId,
transport: this._options.transport,
...(typeof byteLength === 'number' ? { byteLength } : {}),
}
};
const line = `${JSON.stringify(entry)}\n`;
const buffer = VSBuffer.fromString(line);
this._queue = this._queue.then(() => this._appendLine(buffer)).catch(error => {
this._logService.error('[AHPLog] Failed to write transport log', error);
});
}

async flush(): Promise<void> {
await this._queue;
}

private async _appendLine(buffer: VSBuffer): Promise<void> {
// Create folder once and memoize to avoid repeated filesystem calls
if (!this._folderCreated) {
this._folderCreated = this._fileService.createFolder(this._directory);
}
await this._folderCreated;
if (this._currentSize === 0) {
this._currentSize = await this._getFileSize(this._currentFile);
}
if (this._currentSize > 0 && this._currentSize + buffer.byteLength > this._maxFileSizeBytes) {
await this._rotate();
}
await this._fileService.writeFile(this._currentFile, buffer, { append: true });
this._currentSize += buffer.byteLength;
}

private async _rotate(): Promise<void> {
this._segment++;
const oldSegment = this._segment - this._maxFiles;
if (oldSegment >= 0) {
await this._fileService.del(this._resourceForSegment(oldSegment)).catch(error => {
this._logService.trace('[AHPLog] Failed to delete old transport log', error);
});
}
this._currentFile = this._resourceForSegment(this._segment);
this._currentSize = await this._getFileSize(this._currentFile);
}

private _resourceForSegment(segment: number): URI {
if (segment === 0) {
return joinPath(this._directory, this._baseName);
}
const currentBaseName = this._baseName.slice(0, -'.jsonl'.length);
return joinPath(this._directory, `${currentBaseName}.${segment}.jsonl`);
}

private async _getFileSize(resource: URI): Promise<number> {
try {
return (await this._fileService.resolve(resource)).size ?? 0;
} catch {
return 0;
}
}
}

export function getAhpLogByteLength(text: string): number {
return VSBuffer.fromString(text).byteLength;
}

function toFileTimestamp(date: Date): string {
return date.toISOString().replace(/[:.]/g, '-');
}

function sanitizeFilePart(value: string): string {
return value.replace(/[\\/:\*\?"<>\|\s]+/g, '-').replace(/^-+|-+$/g, '') || 'connection';
}
Loading
Loading