Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
281b018
Hookup new model metadata via the sdk (#327143)
lramos15 Jul 23, 2026
cb7261d
copilot customizations avoid that test does not travers up to find a …
aeschli Jul 23, 2026
3a80565
Persist markdown tool result messages (#327044)
kycutler Jul 23, 2026
6d4a6d2
Don't implicitly attach browser editors over agent host (#327046)
kycutler Jul 23, 2026
3393362
Allow HTML files under Copilot root to be opened in the browser (#327…
kycutler Jul 23, 2026
d48127f
Add dictation backend selection; support for using MAI; rename dictat…
meganrogge Jul 23, 2026
f9257d2
Make websockets model aware (#327160)
lramos15 Jul 23, 2026
fcdf991
Update @vscode/codicons to version 0.0.46-24 and add new icons (#327161)
mrleemurray Jul 23, 2026
21d989b
sessions: Add first-time Agents window open telemetry (#327140)
benibenj Jul 23, 2026
0fcfb10
Fix persisted Agent Host MCP authentication (#327154)
ulugbekna Jul 23, 2026
0e8e2ab
Fix Modern UI sticky scroll and adjacent scrollbar styling (#326845)
Copilot Jul 23, 2026
4868aa8
chat: Restore failed MCP Apps in history (#327166)
connor4312 Jul 23, 2026
f444527
Allow model catalogs to incrementally resolve (#327168)
lramos15 Jul 23, 2026
a8f3714
Show the Voice Mode setting in the Settings UI (drop the advanced tag…
meganrogge Jul 23, 2026
e133293
sessions: Shorten input banner reveal labels (#327172)
benibenj Jul 23, 2026
c6b3bee
Add tool search to Copilot agent host (#326213)
bhavyaus Jul 23, 2026
e97bc18
Agents: Constrain workspace and harness picker widths (#326855)
Copilot Jul 23, 2026
90c3ee4
Fix some codeowners (#327174)
alexr00 Jul 23, 2026
2666198
new session tour version 2
benibenj Jul 23, 2026
34078bd
agentHost: Preserve trusted model IDs in telemetry (#327173)
roblourens Jul 23, 2026
d7e035e
sessions: improve blocked sessions overlay actions (#327162)
benibenj Jul 23, 2026
96b7470
Keep markdown editor updated (#327146)
hediet Jul 23, 2026
85409d2
Update chat UI send buttons to rounded squares and adjust glyph align…
hawkticehurst Jul 23, 2026
4789abe
Merge remote-tracking branch 'origin/main' into benibenj/industrial-g…
benibenj Jul 23, 2026
cfbc5af
Add 4px spacing between send button and adjacent controls (#327150)
hawkticehurst Jul 23, 2026
ed20a84
Style override: Enhance tab styling with margin and row gap (#327141)
mrleemurray Jul 23, 2026
b025077
Address spotlight review feedback
benibenj Jul 23, 2026
72750cc
fix: guard undefined stdio command in MCP server identity (fixes #327…
vs-code-engineering[bot] Jul 23, 2026
764a160
Merge pull request #327180 from microsoft/benibenj/industrial-gecko
benibenj Jul 23, 2026
d10e432
Prevent Welcome page clipping at laptop heights (#326846)
Copilot Jul 23, 2026
1b63f6d
sessions: don't flash input needed for auto-approved tools (#325679)
benibenj Jul 23, 2026
dd142c2
Align minimap viewport with editor scrollbar styling (#326844)
Copilot Jul 23, 2026
4224780
Update edit permissions (#327199)
pwang347 Jul 23, 2026
bd39912
Implement uninstall functionality for local plugins (#327071)
sbatten Jul 23, 2026
a71d15d
Improve focused list text contrast (#326853)
Copilot Jul 23, 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
6 changes: 3 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
# VS Code API
# Ensure the API team is aware of changes to the vscode-dts file
# this is only about the final API, not about proposed API changes
src/vscode-dts/vscode.d.ts @mjbvz @alexr00
src/vs/workbench/services/extensions/common/extensionPoints.json @mjbvz @alexr00
src/vscode-dts/vscode.d.ts @TylerLeonhardt @alexr00
src/vs/workbench/services/extensions/common/extensionPoints.json @TylerLeonhardt @alexr00

# Allowlist for the `local/code-no-new-javascript-files` lint rule.
# Adding entries here lets a new .js/.cjs/.mjs file land in the repo;
# review is required to make sure TypeScript is not a better choice.
.eslint-allowed-javascript-files @alexr00 @alexdima @sbatten
.eslint-allowed-javascript-files @alexr00 @alexdima @sbatten @TylerLeonhardt
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/extensions/markdown-language-features"
schedule:
interval: "daily"
allow:
- dependency-name: "@vscode/markdown-editor"
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { IEndpointProvider } from '../../../../platform/endpoint/common/endpoint
import { IOctoKitService } from '../../../../platform/github/common/githubService';
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
import { ILogService } from '../../../../platform/log/common/logService';
import { IChatWebSocketManager } from '../../../../platform/networking/node/chatWebSocketManager';
import { IRequestLogger } from '../../../../platform/requestLogger/common/requestLogger';
import { ISurveyService } from '../../../../platform/survey/common/surveyService';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
Expand Down Expand Up @@ -243,8 +244,9 @@ class RequestHandler extends DefaultIntentRequestHandler {
@IChatHookService chatHookService: IChatHookService,
@IOctoKitService octoKitService: IOctoKitService,
@IConfigurationService configurationService: IConfigurationService,
@IChatWebSocketManager chatWebSocketManager: IChatWebSocketManager,
) {
super(intent, conversation, request, stream, token, documentContext, location, chatTelemetry, undefined, undefined, instantiationService, conversationOptions, telemetryService, logService, surveyService, requestLogger, editSurvivalTrackerService, authenticationService, chatHookService, octoKitService, configurationService);
super(intent, conversation, request, stream, token, documentContext, location, chatTelemetry, undefined, undefined, instantiationService, conversationOptions, telemetryService, logService, surveyService, requestLogger, editSurvivalTrackerService, authenticationService, chatHookService, octoKitService, configurationService, chatWebSocketManager);
}

/**
Expand Down
12 changes: 9 additions & 3 deletions extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
* Note: the returned array of strings may be less than `n` (e.g., in case there were errors during streaming)
*/
public async fetchMany(opts: IFetchMLOptions, token: CancellationToken): Promise<ChatResponses> {
let { debugName, endpoint: chatEndpoint, finishedCb, location, messages, requestOptions, source, telemetryProperties, userInitiatedRequest, interactionTypeOverride, conversationId, turnId, topLevelTurnId, useWebSocket, ignoreStatefulMarker } = opts;
let { debugName, endpoint: chatEndpoint, finishedCb, location, messages, requestOptions, source, telemetryProperties, userInitiatedRequest, interactionTypeOverride, conversationId, webSocketConnectionId, turnId, topLevelTurnId, useWebSocket, ignoreStatefulMarker } = opts;
const interactionType = interactionTypeOverride ?? locationToIntent(location);
if (useWebSocket && this._consecutiveWebSocketRetryFallbacks >= ChatMLFetcherImpl._maxConsecutiveWebSocketFallbacks) {
this._logService.debug(`[ChatWebSocketManager] Disabling WebSocket for request due to ${this._consecutiveWebSocketRetryFallbacks} consecutive WebSocket failures with successful HTTP fallback.`);
Expand Down Expand Up @@ -243,6 +243,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
useWebSocket,
turnId,
conversationId,
webSocketConnectionId,
telemetryProperties,
opts.useFetcher,
canRetryOnce,
Expand Down Expand Up @@ -914,7 +915,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
this._logService.info(`[ChatWebSocketManager] WebSocket request failed with successful HTTP fallback (${this._consecutiveWebSocketRetryFallbacks} consecutive).`);
if (opts.conversationId) {
// Closing here because the retry is transparent.
this._webSocketManager.closeConnection(opts.conversationId);
this._webSocketManager.closeConnection(opts.conversationId, opts.webSocketConnectionId);
}
}
return { retryResult, connectivityTestError, connectivityTestErrorGitHubRequestId };
Expand All @@ -936,6 +937,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
useWebSocket?: boolean,
turnId?: string,
conversationId?: string,
webSocketConnectionId?: string,
telemetryProperties?: TelemetryProperties | undefined,
useFetcher?: FetcherId,
canRetryOnce?: boolean,
Expand Down Expand Up @@ -976,6 +978,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
useWebSocket,
turnId,
conversationId,
webSocketConnectionId,
telemetryProperties,
useFetcher,
canRetryOnce,
Expand Down Expand Up @@ -1015,6 +1018,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
useWebSocket?: boolean,
turnId?: string,
conversationId?: string,
webSocketConnectionId?: string,
telemetryProperties?: TelemetryProperties | undefined,
useFetcher?: FetcherId,
canRetryOnce?: boolean,
Expand Down Expand Up @@ -1093,6 +1097,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
ourRequestId,
turnId,
conversationId,
webSocketConnectionId,
cancellationToken,
countTokens,
userInitiatedRequest,
Expand Down Expand Up @@ -1152,6 +1157,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
ourRequestId: string,
turnId: string,
conversationId: string,
webSocketConnectionId: string | undefined,
cancellationToken: CancellationToken,
countTokens: () => Promise<number>,
userInitiatedRequest: boolean | undefined,
Expand All @@ -1175,7 +1181,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
if (request.messages?.some((m: CAPIChatMessage) => Array.isArray(m.content) ? m.content.some(c => 'image_url' in c) : false) && chatEndpointInfo.supportsVision) {
additionalHeaders['Copilot-Vision-Request'] = 'true';
}
const connection = this._webSocketManager.getOrCreateConnection(conversationId, additionalHeaders, ourRequestId);
const connection = this._webSocketManager.getOrCreateConnection({ conversationId, modelId: chatEndpointInfo.model, connectionId: webSocketConnectionId }, additionalHeaders, ourRequestId);
try {
await connection.connect();
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { HAS_IGNORED_FILES_MESSAGE } from '../../../platform/ignore/common/ignor
import { ILogService } from '../../../platform/log/common/logService';
import { isAnthropicContextEditingEnabled } from '../../../platform/networking/common/anthropic';
import { FilterReason } from '../../../platform/networking/common/openai';
import { IChatWebSocketManager } from '../../../platform/networking/node/chatWebSocketManager';
import { IOTelService } from '../../../platform/otel/common/otelService';
import { CapturingToken } from '../../../platform/requestLogger/common/capturingToken';
import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';
Expand Down Expand Up @@ -101,6 +102,7 @@ export class DefaultIntentRequestHandler {
@IChatHookService private readonly _chatHookService: IChatHookService,
@IOctoKitService private readonly _octoKitService: IOctoKitService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IChatWebSocketManager private readonly _chatWebSocketManager: IChatWebSocketManager,
) {
// Initialize properties
this.turn = conversation.getLatestTurn();
Expand All @@ -110,6 +112,9 @@ export class DefaultIntentRequestHandler {
if (isToolCallLimitCancellation(this.request)) {
// Just some friendly text instead of an empty message on cancellation:
this.stream.markdown(l10n.t("Let me know if there's anything else I can help with!"));
if (this.request.subAgentInvocationId) {
this._chatWebSocketManager.closeConnection(this.conversation.sessionId, this.request.subAgentInvocationId);
}
return {};
}

Expand Down Expand Up @@ -189,6 +194,10 @@ export class DefaultIntentRequestHandler {
const chatResult = { errorDetails: { message: errorMessage } };
this.turn.setResponse(TurnStatus.Error, { message: errorMessage, type: 'meta' }, undefined, chatResult);
return chatResult;
} finally {
if (this.request.subAgentInvocationId) {
this._chatWebSocketManager.closeConnection(this.conversation.sessionId, this.request.subAgentInvocationId);
}
}
}

Expand Down Expand Up @@ -703,6 +712,7 @@ class DefaultToolCallingLoop extends ToolCallingLoop<IDefaultToolLoopOptions> {
},
debugName,
conversationId: this.options.conversation.sessionId,
webSocketConnectionId: this.options.request.subAgentInvocationId,
turnId: opts.turnId,
finishedCb: (text, index, delta) => {
this.telemetry.markReceivedToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { StaticChatMLFetcher } from '../../../../platform/chat/test/common/stati
import { MockEndpoint } from '../../../../platform/endpoint/test/node/mockEndpoint';
import { IResponseDelta } from '../../../../platform/networking/common/fetch';
import { IChatEndpoint } from '../../../../platform/networking/common/networking';
import { IChatWebSocketManager } from '../../../../platform/networking/node/chatWebSocketManager';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
import { SpyingTelemetryService } from '../../../../platform/telemetry/node/spyingTelemetryService';
import { ITestingServicesAccessor } from '../../../../platform/test/node/services';
Expand Down Expand Up @@ -191,6 +192,24 @@ suite('defaultIntentRequestHandler', () => {
expect(getDerandomizedTelemetry()).toMatchSnapshot();
});

test('isolates and closes a subagent WebSocket connection', async () => {
const request = new TestChatRequest();
Object.assign(request, { subAgentInvocationId: 'subagent-1', subAgentName: 'Reviewer' });
const requestSpy = vi.spyOn(endpoint, 'makeChatRequest2');
const closeConnectionSpy = vi.spyOn(accessor.get(IChatWebSocketManager), 'closeConnection');
const handler = makeHandler({ request });
chatResponse[0] = 'some response here :)';
promptResult = {
...nullRenderPromptResult(),
messages: [{ role: Raw.ChatRole.User, content: [toTextPart('hello world!')] }],
};

await handler.getResult();

expect(requestSpy.mock.calls[0][0].webSocketConnectionId).toBe('subagent-1');
expect(closeConnectionSpy).toHaveBeenCalledWith(sessionId, 'subagent-1');
});

test('propagates resolvedModel into result metadata from a successful response', async () => {
fetcher.resolvedModel = 'gpt-4o-resolved';
const handler = makeHandler();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,11 @@ export async function applyEdit(

const ALWAYS_CHECKED_EDIT_PATTERNS: Readonly<Record<string, boolean>> = {
'**/.vscode/*.json': false,
// Markdown files in these folders are loaded as custom agents; their
// frontmatter can declare a `hooks:` block that runs shell commands during
// the agent lifecycle, so writing them must always be confirmed.
'**/.github/agents/**': false,
'**/.claude/agents/**': false,
};

const allPlatformPatterns: (glob.ParsedPattern | string)[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1113,3 +1113,35 @@ describe('makeUriConfirmationChecker', async () => {
});
});
});

describe('agent definition files require confirmation', () => {
let configService: InMemoryConfigurationService;
let workspaceService: TestWorkspaceService;
let customInstructionsService: MockCustomInstructionsService;

beforeEach(() => {
configService = new InMemoryConfigurationService(new DefaultsOnlyConfigurationService());
workspaceService = new TestWorkspaceService([URI.file('/workspace')], []);
customInstructionsService = new MockCustomInstructionsService();
});

test('requires confirmation for files in agent folders', async () => {
const checker = makeUriConfirmationChecker(configService, workspaceService.getWorkspaceFolder.bind(workspaceService), customInstructionsService);
const files = [
'/workspace/.github/agents/dev-helper.md',
'/workspace/.github/agents/dev-helper.agent.md',
'/workspace/.claude/agents/reviewer.md',
'/workspace/.github/agents/nested/deep.md',
];
for (const file of files) {
expect(await checker(URI.file(file))).toBe(ConfirmationCheckResult.Sensitive);
}
});

test('does not require confirmation for similarly-named files outside agent folders', async () => {
const checker = makeUriConfirmationChecker(configService, workspaceService.getWorkspaceFolder.bind(workspaceService), customInstructionsService);
expect(await checker(URI.file('/workspace/src/agents/agent.md'))).toBe(ConfirmationCheckResult.NoConfirmation);
expect(await checker(URI.file('/workspace/.github/workflows/ci.md'))).toBe(ConfirmationCheckResult.NoConfirmation);
});
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type * as vscode from 'vscode';
import { describe, expect, it, vi } from 'vitest';
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
import type { ILogService } from '../../../../platform/log/common/logService';
import type { IToolDeferralService } from '../../../../platform/networking/common/toolDeferralService';
import type { IToolEmbeddingsComputer } from '../../common/virtualTools/toolEmbeddingsComputer';
import type { IToolsService } from '../../common/toolsService';
import { ToolSearchTool } from '../toolSearchTool';

function text(result: vscode.LanguageModelToolResult): string {
return result.content.map(part => {
const value = part as { value?: unknown };
return value.value === undefined ? '' : String(value.value);
}).join('');
}

describe('ToolSearchTool', () => {
it('uses an injected Agent Host corpus instead of the extension registry', async () => {
const searchToolsByQuery = vi.fn(async (_query: string, tools: readonly vscode.LanguageModelToolInformation[]) => tools.map(tool => tool.name));
const embeddings = { _serviceBrand: undefined, searchToolsByQuery } as unknown as IToolEmbeddingsComputer;
const toolsService = {
_serviceBrand: undefined,
tools: [{ name: 'extension-only', description: 'Extension tool', inputSchema: undefined, tags: [], source: undefined }],
} as unknown as IToolsService;
const deferral = { _serviceBrand: undefined, isNonDeferredTool: () => false } as IToolDeferralService;
const log = { _serviceBrand: undefined, trace: vi.fn() } as unknown as ILogService;
const tool = new ToolSearchTool(embeddings, toolsService, deferral, log);

const result = await tool.invoke({
input: {
query: 'add numbers',
candidateTools: [{ name: 'everything-get-sum', description: 'Adds numbers' }],
},
toolInvocationToken: undefined,
} as vscode.LanguageModelToolInvocationOptions<any>, CancellationToken.None);

expect(searchToolsByQuery).toHaveBeenCalledOnce();
expect(searchToolsByQuery.mock.calls[0][1].map(tool => tool.name)).toEqual(['everything-get-sum']);
expect(text(result)).toBe('["everything-get-sum"]');
});

it('preserves the extension registry path when no corpus is injected', async () => {
const searchToolsByQuery = vi.fn(async (_query: string, tools: readonly vscode.LanguageModelToolInformation[]) => tools.map(tool => tool.name));
const embeddings = { _serviceBrand: undefined, searchToolsByQuery } as unknown as IToolEmbeddingsComputer;
const toolsService = {
_serviceBrand: undefined,
tools: [
{ name: 'deferred-extension-tool', description: 'Deferred', inputSchema: undefined, tags: [], source: undefined },
{ name: 'core-tool', description: 'Core', inputSchema: undefined, tags: [], source: undefined },
],
} as unknown as IToolsService;
const deferral = { _serviceBrand: undefined, isNonDeferredTool: (name: string) => name === 'core-tool' } as IToolDeferralService;
const log = { _serviceBrand: undefined, trace: vi.fn() } as unknown as ILogService;
const tool = new ToolSearchTool(embeddings, toolsService, deferral, log);

await tool.invoke({ input: { query: 'deferred' }, toolInvocationToken: undefined } as vscode.LanguageModelToolInvocationOptions<any>, CancellationToken.None);

expect(searchToolsByQuery.mock.calls[0][1].map(tool => tool.name)).toEqual(['deferred-extension-tool']);
});
});
23 changes: 19 additions & 4 deletions extensions/copilot/src/extension/tools/node/toolSearchTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import { IToolEmbeddingsComputer } from '../common/virtualTools/toolEmbeddingsCo
export interface IToolSearchParams {
query: string;
limit?: number;
/** Internal Agent Host corpus; not part of the model-facing schema. */
candidateTools?: readonly IToolSearchCandidate[];
}

export interface IToolSearchCandidate {
name: string;
description: string;
}

const DEFAULT_SEARCH_LIMIT = 5;
Expand All @@ -29,17 +36,25 @@ export class ToolSearchTool implements ICopilotModelSpecificTool<IToolSearchPara
) { }

async invoke(options: vscode.LanguageModelToolInvocationOptions<IToolSearchParams>, token: vscode.CancellationToken) {
const { query, limit } = options.input;
const { query, limit, candidateTools } = options.input;

if (!query) {
return new LanguageModelToolResult([
new LanguageModelTextPart('Error: query parameter is required'),
]);
}

const availableTools = this._toolsService.tools.filter(
tool => !this._toolDeferralService.isNonDeferredTool(tool.name),
);
const availableTools: readonly vscode.LanguageModelToolInformation[] = candidateTools !== undefined
? candidateTools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: undefined,
tags: [],
source: undefined,
}))
: this._toolsService.tools.filter(
tool => !this._toolDeferralService.isNonDeferredTool(tool.name),
);
const matchedToolNames = await this._toolEmbeddingsComputer.searchToolsByQuery(
query,
availableTools,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ export class ChatEndpoint implements IChatEndpoint {
useWebSocket
&& options.conversationId
&& options.turnId
&& this._chatWebSocketService.hasActiveConnection(options.conversationId)
&& this._chatWebSocketService.hasActiveConnection({ conversationId: options.conversationId, modelId: this.model, connectionId: options.webSocketConnectionId })
);
const response = await this._makeChatRequest2({
...options,
Expand Down
Loading
Loading