From e0c5a93c45a129edb39299643f62d68cddab1824 Mon Sep 17 00:00:00 2001 From: yogeshwaran-c Date: Mon, 6 Apr 2026 19:35:46 +0530 Subject: [PATCH 001/589] Add copy button to exception peek view in debugger Adds a copy icon button next to the close button in the exception widget, allowing users to copy the exception details (title, description, and stack trace) to clipboard with a single click. Fixes #225086 --- .../contrib/debug/browser/exceptionWidget.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts index 25aa89b6c6faa6..0c0d408619575b 100644 --- a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts @@ -21,6 +21,8 @@ import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { Action } from '../../../../base/common/actions.js'; import { widgetClose } from '../../../../platform/theme/common/iconRegistry.js'; import { Range } from '../../../../editor/common/core/range.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; const $ = dom.$; @@ -39,7 +41,8 @@ export class ExceptionWidget extends ZoneWidget { private debugSession: IDebugSession | undefined, private readonly shouldScroll: () => boolean, @IThemeService themeService: IThemeService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IClipboardService private readonly clipboardService: IClipboardService ) { super(editor, { showFrame: true, showArrow: true, isAccessible: true, frameWidth: 1, className: 'exception-widget-container' }); @@ -84,6 +87,10 @@ export class ExceptionWidget extends ZoneWidget { let ariaLabel = label.textContent; const actionBar = this._disposables.add(new ActionBar(actions)); + actionBar.push(new Action('editor.copyExceptionInfo', nls.localize('copy', "Copy"), ThemeIcon.asClassName(Codicon.copy), true, async () => { + const clipboardText = this.buildExceptionText(); + await this.clipboardService.writeText(clipboardText); + }), { label: false, icon: true }); actionBar.push(new Action('editor.closeExceptionWidget', nls.localize('close', "Close"), ThemeIcon.asClassName(widgetClose), true, async () => { const contribution = this.editor.getContribution(EDITOR_CONTRIBUTION_ID); contribution?.closeExceptionWidget(); @@ -132,6 +139,22 @@ export class ExceptionWidget extends ZoneWidget { } } + private buildExceptionText(): string { + const parts: string[] = []; + if (this.exceptionInfo.id) { + parts.push(nls.localize('exceptionThrownWithId', 'Exception has occurred: {0}', this.exceptionInfo.id)); + } else { + parts.push(nls.localize('exceptionThrown', 'Exception has occurred.')); + } + if (this.exceptionInfo.description) { + parts.push(this.exceptionInfo.description); + } + if (this.exceptionInfo.details?.stackTrace) { + parts.push(this.exceptionInfo.details.stackTrace); + } + return parts.join('\n'); + } + focus(): void { // Focus into the container for accessibility purposes so the exception and stack trace gets read this.container?.focus(); From f7c07eeb5cc1305c877f5f2a09799d99687d05d6 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Mon, 22 Jun 2026 19:30:18 -0700 Subject: [PATCH 002/589] Add proxy service, translation, and tests --- .../agentHost/common/agentHostByokLm.ts | 103 +++++++ .../agentHost/node/byokLmBridgeRegistry.ts | 66 +++++ .../node/copilot/byokLmProxyService.ts | 258 ++++++++++++++++++ .../node/copilot/byokOpenAiTranslation.ts | 234 ++++++++++++++++ .../test/node/byokLmProxyService.test.ts | 243 +++++++++++++++++ .../test/node/byokOpenAiTranslation.test.ts | 105 +++++++ 6 files changed, 1009 insertions(+) create mode 100644 src/vs/platform/agentHost/common/agentHostByokLm.ts create mode 100644 src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts create mode 100644 src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts create mode 100644 src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts create mode 100644 src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts create mode 100644 src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts diff --git a/src/vs/platform/agentHost/common/agentHostByokLm.ts b/src/vs/platform/agentHost/common/agentHostByokLm.ts new file mode 100644 index 00000000000000..c454a562e813a9 --- /dev/null +++ b/src/vs/platform/agentHost/common/agentHostByokLm.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { createDecorator } from '../../instantiation/common/instantiation.js'; + +/** + * Serializable bridge contract between the node agent host (where the + * {@link IByokLmProxyService} OpenAI-compatible proxy runs) and the renderer + * (which owns the extension-provided BYOK language models via the LM API). + * + * These shapes are deliberately wire-friendly (plain JSON, no `VSBuffer`, + * `URI`, or `workbench/contrib/chat` types) so they survive both the local + * utility-process IPC channel and the remote JSON-RPC transport without a + * translation step. The node side converts OpenAI Chat Completions wire + * payloads to/from these; the renderer side converts these to/from the VS Code + * LM API (`ILanguageModelsService`). + */ + +/** A single tool/function call requested by the assistant. */ +export interface IByokLmToolCall { + /** Stable id correlating the call with its later `tool` result message. */ + readonly id: string; + /** Tool/function name. */ + readonly name: string; + /** JSON-encoded arguments object. */ + readonly argumentsJson: string; +} + +/** A tool/function the model may call. */ +export interface IByokLmTool { + readonly name: string; + readonly description?: string; + /** JSON schema for the tool parameters. */ + readonly parametersSchema?: object; +} + +/** One chat message in a BYOK request. */ +export interface IByokLmChatMessage { + readonly role: 'system' | 'user' | 'assistant' | 'tool'; + /** Flattened text content. Empty string when the message carries only tool calls/results. */ + readonly content: string; + /** Present on `assistant` messages that requested tool calls. */ + readonly toolCalls?: IByokLmToolCall[]; + /** Present on `tool` messages: the {@link IByokLmToolCall.id} this result answers. */ + readonly toolCallId?: string; +} + +/** A chat request forwarded from the proxy to the renderer LM API. */ +export interface IByokLmChatRequest { + /** Provider/vendor name (the LM API vendor that registered the model). */ + readonly vendor: string; + /** Provider-local model id (the wire id the runtime sent on the OpenAI request). */ + readonly modelId: string; + readonly messages: IByokLmChatMessage[]; + readonly tools?: IByokLmTool[]; + /** Opaque per-request model options forwarded to the LM provider. */ + readonly modelOptions?: Record; +} + +/** The (buffered) completion produced by the renderer LM API. */ +export interface IByokLmChatResult { + /** Concatenated assistant text. */ + readonly content: string; + /** Tool calls the assistant requested, if any. */ + readonly toolCalls?: IByokLmToolCall[]; + /** Best-effort token usage, when the provider reports it. */ + readonly usage?: { + readonly promptTokens?: number; + readonly completionTokens?: number; + }; + /** Set when the LM call failed; `content` is then empty. */ + readonly error?: string; +} + +export const IAgentHostByokLmHandler = createDecorator('agentHostByokLmHandler'); + +/** + * Renderer-side handler that services {@link IByokLmChatRequest}s by calling + * the VS Code Language Model API. Implemented in the workbench (where + * `ILanguageModelsService` lives) and reached from the node agent host over + * the reverse bridge. + */ +export interface IAgentHostByokLmHandler { + readonly _serviceBrand: undefined; + + /** + * Run a BYOK chat completion against the extension-registered model that + * matches `request.vendor` + `request.modelId`. Rejects (or resolves with + * {@link IByokLmChatResult.error}) when no such model is available. + */ + chat(request: IByokLmChatRequest, token: CancellationToken): Promise; +} + +/** + * Node-side connection to a single renderer's {@link IAgentHostByokLmHandler}. + * Mirrors `IRemoteFilesystemConnection` for the reverse FS bridge. + */ +export interface IByokLmBridgeConnection { + chat(request: IByokLmChatRequest): Promise; +} diff --git a/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts b/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts new file mode 100644 index 00000000000000..4bd15ffdc684f7 --- /dev/null +++ b/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { createDecorator } from '../../instantiation/common/instantiation.js'; +import { IByokLmBridgeConnection } from '../common/agentHostByokLm.js'; + +export const IByokLmBridgeRegistry = createDecorator('byokLmBridgeRegistry'); + +/** + * Node-side registry of renderer {@link IByokLmBridgeConnection}s keyed by + * client id. Populated by the agent host's connection lifecycle (one entry per + * connected renderer) and consumed by {@link IByokLmProxyService} when it needs + * to service an inbound OpenAI request against the renderer LM API. + * + * Single-tenant assumption (matching the Claude/Codex proxies): the most + * recently registered connection is treated as the active one. + */ +export interface IByokLmBridgeRegistry { + readonly _serviceBrand: undefined; + + /** Register a renderer connection. Disposing the result removes it. */ + register(clientId: string, connection: IByokLmBridgeConnection): IDisposable; + + /** The connection for `clientId`, or `undefined` if none is registered. */ + get(clientId: string): IByokLmBridgeConnection | undefined; + + /** The most recently registered, still-live connection, if any. */ + getActive(): IByokLmBridgeConnection | undefined; +} + +export class ByokLmBridgeRegistry implements IByokLmBridgeRegistry { + + declare readonly _serviceBrand: undefined; + + private readonly _connections = new Map(); + private _activeClientId: string | undefined; + + register(clientId: string, connection: IByokLmBridgeConnection): IDisposable { + this._connections.set(clientId, connection); + this._activeClientId = clientId; + return toDisposable(() => { + if (this._connections.get(clientId) === connection) { + this._connections.delete(clientId); + if (this._activeClientId === clientId) { + // Fall back to any remaining connection. + const next = this._connections.keys().next(); + this._activeClientId = next.done ? undefined : next.value; + } + } + }); + } + + get(clientId: string): IByokLmBridgeConnection | undefined { + return this._connections.get(clientId); + } + + getActive(): IByokLmBridgeConnection | undefined { + if (this._activeClientId !== undefined) { + return this._connections.get(this._activeClientId); + } + return undefined; + } +} diff --git a/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts b/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts new file mode 100644 index 00000000000000..a2a2e3e05d81b3 --- /dev/null +++ b/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts @@ -0,0 +1,258 @@ +/*--------------------------------------------------------------------------------------------- + * 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 http from 'http'; +import { createDecorator } from '../../../instantiation/common/instantiation.js'; +import { ILogService } from '../../../log/common/log.js'; +import { IByokLmBridgeRegistry } from '../byokLmBridgeRegistry.js'; +import { parseProxyBearer } from '../claude/claudeProxyAuth.js'; +import { + ILoopbackProxyHandle, + ILoopbackProxyRuntime, + IProxyInFlight, + LoopbackProxyServer, + readProxyRequestBody, +} from '../shared/loopbackProxyServer.js'; +import { + IOpenAiChatRequest, + OpenAiTranslationError, + bridgeResultToSseFrames, + openAiErrorBody, + openAiRequestToBridge, +} from './byokOpenAiTranslation.js'; + +// #region Public types + +/** + * Handle returned by {@link IByokLmProxyService.start}. Refcounts the shared + * loopback server (see {@link LoopbackProxyServer}): when every handle is + * disposed the listener closes and the nonce is destroyed; the next `start()` + * rebinds with a fresh port and nonce. + * + * **Subprocess ownership invariant.** Callers that hand `baseUrl`/`nonce` to + * the Copilot SDK runtime subprocess MUST kill that subprocess before calling + * `dispose()` — after disposal the proxy may rebind on a different port and the + * subprocess would silently lose its endpoint (same contract as the Claude and + * Codex proxies). + */ +export interface IByokLmProxyHandle extends ILoopbackProxyHandle { + /** e.g. `http://127.0.0.1:54321` — no trailing slash. */ + readonly baseUrl: string; + /** 256-bit hex string. Combine with a session id as `Bearer .`. */ + readonly nonce: string; + /** + * Build the provider `baseUrl` for a given BYOK vendor. The vendor is + * encoded into the path so a single proxy can serve every vendor; the + * runtime appends `/chat/completions` to this URL. + */ + providerBaseUrl(vendor: string): string; +} + +export const IByokLmProxyService = createDecorator('byokLmProxyService'); + +export interface IByokLmProxyService { + readonly _serviceBrand: undefined; + + /** Start the proxy (if not already running) and return a refcounted handle. */ + start(): Promise; + + /** + * Force-close the proxy regardless of refcount and abort in-flight + * requests. Idempotent; subsequent `start()` calls rebind. + */ + dispose(): void; +} + +// #endregion + +const PROXY_USER_FACING_NAME = 'ByokLmProxyService'; +const VENDOR_PATH_PREFIX = '/v/'; +const CHAT_COMPLETIONS_SUFFIX = '/chat/completions'; + +/** + * The BYOK proxy keeps no per-bind mutable state: the active renderer bridge is + * resolved from {@link IByokLmBridgeRegistry} at request time, and the nonce + * lives on the runtime owned by {@link LoopbackProxyServer}. + */ +type ByokLmProxyState = undefined; + +/** + * Local OpenAI-compatible HTTP proxy that lets the Copilot SDK runtime run + * BYOK models provided by VS Code extensions. The runtime is configured with a + * `type: 'openai'`, `wireApi: 'completions'` provider whose `baseUrl` points + * here; inbound `POST /v//chat/completions` requests are authenticated, + * translated, and forwarded to the renderer LM API via + * {@link IByokLmBridgeRegistry}, and the buffered completion is streamed back + * as OpenAI Chat Completions SSE. + * + * The server lifecycle — lazy bind on `127.0.0.1`, nonce minting, refcounted + * handles, in-flight tracking, and teardown — is inherited from + * {@link LoopbackProxyServer}; this subclass only implements request routing. + */ +export class ByokLmProxyService extends LoopbackProxyServer implements IByokLmProxyService { + + declare readonly _serviceBrand: undefined; + + constructor( + @ILogService logService: ILogService, + @IByokLmBridgeRegistry private readonly _bridgeRegistry: IByokLmBridgeRegistry, + ) { + super(PROXY_USER_FACING_NAME, logService); + } + + protected createState(): ByokLmProxyState { + // No per-bind state — the bridge is resolved from the registry per request. + return undefined; + } + + async start(): Promise { + const { runtime, release } = await this.acquire(); + + let disposed = false; + return { + baseUrl: runtime.baseUrl, + nonce: runtime.nonce, + providerBaseUrl: (vendor: string) => `${runtime.baseUrl}${VENDOR_PATH_PREFIX}${encodeURIComponent(vendor)}`, + dispose: () => { + if (disposed) { + return; + } + disposed = true; + release(); + }, + }; + } + + /** Emit the base's fallback failure using the OpenAI error envelope. */ + protected override writeInternalError(res: http.ServerResponse): void { + this._writeJsonError(res, 500, 'Internal proxy error'); + } + + protected override async handleRequest(req: http.IncomingMessage, res: http.ServerResponse, runtime: ILoopbackProxyRuntime): Promise { + const method = req.method ?? 'GET'; + const pathname = new URL(req.url ?? '/', 'http://127.0.0.1').pathname; + this._logService.trace(`[${PROXY_USER_FACING_NAME}] ${method} ${pathname}`); + + if (method === 'GET' && pathname === '/') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); + return; + } + + // Inbound requests carry `Bearer .`; the runtime is + // handed `.` at session launch. + const auth = parseProxyBearer(req.headers, runtime.nonce); + if (!auth.valid || !auth.sessionId) { + this._writeJsonError(res, 401, 'Invalid authentication', 'authentication_error'); + return; + } + + const vendor = this._parseVendorFromChatPath(pathname); + if (method === 'POST' && vendor !== undefined) { + await this._handleChatCompletions(req, res, runtime, vendor); + return; + } + + this._writeJsonError(res, 404, `No route for ${method} ${pathname}`, 'not_found_error'); + } + + /** + * Extract the vendor from a `/v//chat/completions` path, or return + * `undefined` when the path is not a chat-completions route. + */ + private _parseVendorFromChatPath(pathname: string): string | undefined { + if (!pathname.startsWith(VENDOR_PATH_PREFIX) || !pathname.endsWith(CHAT_COMPLETIONS_SUFFIX)) { + return undefined; + } + const vendorSegment = pathname.slice(VENDOR_PATH_PREFIX.length, pathname.length - CHAT_COMPLETIONS_SUFFIX.length); + if (!vendorSegment || vendorSegment.includes('/')) { + return undefined; + } + try { + return decodeURIComponent(vendorSegment); + } catch { + return undefined; + } + } + + private async _handleChatCompletions(req: http.IncomingMessage, res: http.ServerResponse, runtime: ILoopbackProxyRuntime, vendor: string): Promise { + const connection = this._bridgeRegistry.getActive(); + if (!connection) { + this._writeJsonError(res, 503, 'No renderer connection available to service BYOK models', 'api_error'); + return; + } + + let body: IOpenAiChatRequest; + try { + const raw = await readProxyRequestBody(req); + body = JSON.parse(raw) as IOpenAiChatRequest; + } catch (err) { + this._writeJsonError(res, 400, `Invalid request body: ${err instanceof Error ? err.message : String(err)}`, 'invalid_request_error'); + return; + } + + let bridgeRequest; + try { + bridgeRequest = openAiRequestToBridge(vendor, body); + } catch (err) { + const message = err instanceof OpenAiTranslationError ? err.message : String(err); + this._writeJsonError(res, 400, message, 'invalid_request_error'); + return; + } + + // Register the request so {@link LoopbackProxyServer} aborts it on + // teardown; a client-side disconnect also flips `clientGone` and aborts. + // Both surface through the shared `AbortController`, which we re-check + // after the async bridge hop before touching the response. + const entry: IProxyInFlight = { ac: new AbortController(), res, clientGone: false }; + runtime.inFlight.add(entry); + const onClose = () => { + entry.clientGone = true; + entry.ac.abort(); + }; + res.on('close', onClose); + + try { + const result = await connection.chat(bridgeRequest); + if (entry.ac.signal.aborted || res.writableEnded) { + return; + } + if (result.error) { + this._writeJsonError(res, 502, result.error, 'api_error'); + return; + } + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }); + for (const frame of bridgeResultToSseFrames(result, bridgeRequest.modelId)) { + res.write(frame); + } + res.end(); + } catch (err) { + if (entry.ac.signal.aborted || res.writableEnded) { + return; + } + const message = err instanceof Error ? err.message : String(err); + if (!res.headersSent) { + this._writeJsonError(res, 502, message, 'api_error'); + } else { + try { res.end(); } catch { /* ignore */ } + } + } finally { + res.removeListener('close', onClose); + runtime.inFlight.delete(entry); + } + } + + private _writeJsonError(res: http.ServerResponse, status: number, message: string, type = 'api_error'): void { + if (res.headersSent || res.writableEnded) { + return; + } + res.writeHead(status, { 'Content-Type': 'application/json' }); + res.end(openAiErrorBody(message, type)); + } +} diff --git a/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts b/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts new file mode 100644 index 00000000000000..39457177d6a130 --- /dev/null +++ b/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts @@ -0,0 +1,234 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + IByokLmChatMessage, + IByokLmChatRequest, + IByokLmChatResult, + IByokLmTool, + IByokLmToolCall, +} from '../../common/agentHostByokLm.js'; + +/** + * Minimal subset of the OpenAI Chat Completions wire format the Copilot SDK + * runtime emits for a `type: 'openai'`, `wireApi: 'completions'` provider + * (verified against the runtime's `chat_completion_transport.rs`, which POSTs + * to `{baseUrl}/chat/completions`). Only the fields this proxy understands are + * modeled; unknown fields are ignored. + */ + +interface IOpenAiTextContentPart { + readonly type: 'text'; + readonly text: string; +} + +type IOpenAiContentPart = IOpenAiTextContentPart | { readonly type: string;[k: string]: unknown }; + +interface IOpenAiToolCall { + readonly id?: string; + readonly type?: string; + readonly function?: { + readonly name?: string; + readonly arguments?: string; + }; +} + +interface IOpenAiRequestMessage { + readonly role?: string; + readonly content?: string | IOpenAiContentPart[] | null; + readonly tool_calls?: IOpenAiToolCall[]; + readonly tool_call_id?: string; +} + +interface IOpenAiToolDefinition { + readonly type?: string; + readonly function?: { + readonly name?: string; + readonly description?: string; + readonly parameters?: object; + }; +} + +export interface IOpenAiChatRequest { + readonly model?: string; + readonly messages?: IOpenAiRequestMessage[]; + readonly tools?: IOpenAiToolDefinition[]; + readonly stream?: boolean; + readonly temperature?: number; + readonly top_p?: number; + readonly max_tokens?: number; + readonly [k: string]: unknown; +} + +/** Thrown when the inbound body cannot be mapped to a bridge request. */ +export class OpenAiTranslationError extends Error { } + +function flattenContent(content: string | IOpenAiContentPart[] | null | undefined): string { + if (typeof content === 'string') { + return content; + } + if (Array.isArray(content)) { + let out = ''; + for (const part of content) { + if (part && part.type === 'text' && typeof (part as IOpenAiTextContentPart).text === 'string') { + out += (part as IOpenAiTextContentPart).text; + } + } + return out; + } + return ''; +} + +function toBridgeRole(role: string | undefined): IByokLmChatMessage['role'] { + switch (role) { + case 'system': + case 'developer': + return 'system'; + case 'assistant': + return 'assistant'; + case 'tool': + case 'function': + return 'tool'; + case 'user': + default: + return 'user'; + } +} + +function toBridgeToolCalls(toolCalls: IOpenAiToolCall[] | undefined): IByokLmToolCall[] | undefined { + if (!toolCalls || toolCalls.length === 0) { + return undefined; + } + const mapped: IByokLmToolCall[] = []; + for (let i = 0; i < toolCalls.length; i++) { + const call = toolCalls[i]; + mapped.push({ + id: call.id ?? `call_${i}`, + name: call.function?.name ?? '', + argumentsJson: call.function?.arguments ?? '{}', + }); + } + return mapped; +} + +function toBridgeTools(tools: IOpenAiToolDefinition[] | undefined): IByokLmTool[] | undefined { + if (!tools || tools.length === 0) { + return undefined; + } + const mapped: IByokLmTool[] = []; + for (const tool of tools) { + const fn = tool.function; + if (!fn?.name) { + continue; + } + mapped.push({ + name: fn.name, + description: fn.description, + parametersSchema: fn.parameters, + }); + } + return mapped.length ? mapped : undefined; +} + +/** + * Convert a parsed OpenAI Chat Completions request into the serializable + * bridge request. `vendor` is the synthesized provider name the runtime used + * (it is not present in the OpenAI body); `model` becomes the provider-local + * wire model id resolved on the renderer. + */ +export function openAiRequestToBridge(vendor: string, body: IOpenAiChatRequest): IByokLmChatRequest { + const model = typeof body.model === 'string' ? body.model : ''; + if (!model) { + throw new OpenAiTranslationError('Request is missing the "model" field'); + } + const sourceMessages = Array.isArray(body.messages) ? body.messages : []; + const messages: IByokLmChatMessage[] = sourceMessages.map(message => ({ + role: toBridgeRole(message.role), + content: flattenContent(message.content), + toolCalls: toBridgeToolCalls(message.tool_calls), + toolCallId: message.tool_call_id, + })); + + const modelOptions: Record = {}; + if (typeof body.temperature === 'number') { + modelOptions.temperature = body.temperature; + } + if (typeof body.top_p === 'number') { + modelOptions.top_p = body.top_p; + } + if (typeof body.max_tokens === 'number') { + modelOptions.max_tokens = body.max_tokens; + } + + return { + vendor, + modelId: model, + messages, + tools: toBridgeTools(body.tools), + modelOptions: Object.keys(modelOptions).length ? modelOptions : undefined, + }; +} + +let chunkCounter = 0; + +function nextCompletionId(): string { + chunkCounter = (chunkCounter + 1) % Number.MAX_SAFE_INTEGER; + return `chatcmpl-byok-${Date.now().toString(36)}-${chunkCounter.toString(36)}`; +} + +/** Serialize a single SSE `data:` frame. */ +function sseFrame(payload: unknown): string { + return `data: ${JSON.stringify(payload)}\n\n`; +} + +/** + * Encode a buffered {@link IByokLmChatResult} as a sequence of OpenAI + * `chat.completion.chunk` SSE frames terminated by `data: [DONE]`. + * + * The whole completion is emitted in one content delta (Stage 1 is + * non-streaming end-to-end); the runtime's SSE parser accepts this shape. + */ +export function bridgeResultToSseFrames(result: IByokLmChatResult, model: string): string[] { + const id = nextCompletionId(); + const created = Math.floor(Date.now() / 1000); + const base = { id, object: 'chat.completion.chunk', created, model }; + const frames: string[] = []; + + // Role delta first, matching the OpenAI streaming contract. + frames.push(sseFrame({ ...base, choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }] })); + + if (result.content) { + frames.push(sseFrame({ ...base, choices: [{ index: 0, delta: { content: result.content }, finish_reason: null }] })); + } + + let finishReason: 'stop' | 'tool_calls' = 'stop'; + if (result.toolCalls && result.toolCalls.length > 0) { + finishReason = 'tool_calls'; + const toolCallsDelta = result.toolCalls.map((call, index) => ({ + index, + id: call.id, + type: 'function', + function: { name: call.name, arguments: call.argumentsJson }, + })); + frames.push(sseFrame({ ...base, choices: [{ index: 0, delta: { tool_calls: toolCallsDelta }, finish_reason: null }] })); + } + + const finalChunk: Record = { ...base, choices: [{ index: 0, delta: {}, finish_reason: finishReason }] }; + if (result.usage) { + finalChunk.usage = { + prompt_tokens: result.usage.promptTokens ?? 0, + completion_tokens: result.usage.completionTokens ?? 0, + total_tokens: (result.usage.promptTokens ?? 0) + (result.usage.completionTokens ?? 0), + }; + } + frames.push(sseFrame(finalChunk)); + frames.push('data: [DONE]\n\n'); + return frames; +} + +/** Build an OpenAI-style error envelope body. */ +export function openAiErrorBody(message: string, type = 'api_error'): string { + return JSON.stringify({ error: { message, type } }); +} diff --git a/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts b/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts new file mode 100644 index 00000000000000..da608108d5cdf3 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts @@ -0,0 +1,243 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../log/common/log.js'; +import type { IByokLmChatRequest, IByokLmChatResult } from '../../common/agentHostByokLm.js'; +import { ByokLmBridgeRegistry } from '../../node/byokLmBridgeRegistry.js'; +import { ByokLmProxyService, type IByokLmProxyHandle } from '../../node/copilot/byokLmProxyService.js'; + +/** + * Exercises the inference path end-to-end without the Copilot SDK runtime: + * the test plays the runtime's role by POSTing OpenAI Chat Completions + * requests at the loopback proxy, and plays the renderer's role with a fake + * {@link IByokLmChatRequest} -> {@link IByokLmChatResult} bridge function. The + * only contract under test is the OpenAI wire format in, the bridge DTO out, + * and the SSE wire format back. + */ +suite('ByokLmProxyService', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const sessionId = 'sess-1'; + + async function withProxy( + chat: (request: IByokLmChatRequest) => Promise, + run: (handle: IByokLmProxyHandle) => Promise, + ): Promise { + const registry = new ByokLmBridgeRegistry(); + const registration = registry.register('client-1', { chat }); + const service = new ByokLmProxyService(new NullLogService(), registry); + const handle = await service.start(); + try { + await run(handle); + } finally { + handle.dispose(); + registration.dispose(); + service.dispose(); + } + } + + function chatUrl(handle: IByokLmProxyHandle, vendor: string): string { + return `${handle.providerBaseUrl(vendor)}/chat/completions`; + } + + function authHeaders(handle: IByokLmProxyHandle): Record { + return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${handle.nonce}.${sessionId}` }; + } + + test('serves the unauthenticated health check', async () => { + await withProxy( + async () => ({ content: 'unused' }), + async (handle) => { + const response = await fetch(`${handle.baseUrl}/`); + assert.strictEqual(response.status, 200); + assert.strictEqual(await response.text(), 'ok'); + }, + ); + }); + + test('rejects requests without a valid bearer token', async () => { + await withProxy( + async () => ({ content: 'unused' }), + async (handle) => { + const response = await fetch(chatUrl(handle, 'acme'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: 'm', messages: [] }), + }); + assert.strictEqual(response.status, 401); + }, + ); + }); + + test('rejects a nonce-only bearer token (no session id)', async () => { + await withProxy( + async () => ({ content: 'unused' }), + async (handle) => { + const response = await fetch(chatUrl(handle, 'acme'), { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${handle.nonce}` }, + body: JSON.stringify({ model: 'm', messages: [] }), + }); + assert.strictEqual(response.status, 401); + }, + ); + }); + + test('returns 404 for an authenticated but unknown route', async () => { + await withProxy( + async () => ({ content: 'unused' }), + async (handle) => { + const response = await fetch(`${handle.baseUrl}/v/acme/responses`, { + method: 'POST', + headers: authHeaders(handle), + body: '{}', + }); + assert.strictEqual(response.status, 404); + }, + ); + }); + + test('forwards a chat request to the bridge and streams an SSE completion', async () => { + let captured: IByokLmChatRequest | undefined; + await withProxy( + async (request) => { + captured = request; + return { content: 'hello from byok' }; + }, + async (handle) => { + const response = await fetch(chatUrl(handle, 'acme'), { + method: 'POST', + headers: authHeaders(handle), + body: JSON.stringify({ model: 'claude', messages: [{ role: 'user', content: 'hi' }] }), + }); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.headers.get('content-type'), 'text/event-stream'); + const text = await response.text(); + assert.ok(text.includes('hello from byok'), `expected content in SSE: ${text}`); + assert.ok(text.trimEnd().endsWith('data: [DONE]')); + }, + ); + assert.strictEqual(captured?.vendor, 'acme'); + assert.strictEqual(captured?.modelId, 'claude'); + assert.deepStrictEqual(captured?.messages, [{ role: 'user', content: 'hi', toolCalls: undefined, toolCallId: undefined }]); + }); + + test('decodes a url-encoded vendor path segment', async () => { + let captured: IByokLmChatRequest | undefined; + await withProxy( + async (request) => { captured = request; return { content: 'ok' }; }, + async (handle) => { + const response = await fetch(chatUrl(handle, 'acme corp'), { + method: 'POST', + headers: authHeaders(handle), + body: JSON.stringify({ model: 'm', messages: [] }), + }); + assert.strictEqual(response.status, 200); + await response.text(); + }, + ); + assert.strictEqual(captured?.vendor, 'acme corp'); + }); + + test('streams assistant tool calls as OpenAI tool_call deltas', async () => { + await withProxy( + async () => ({ content: '', toolCalls: [{ id: 'call_1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }] }), + async (handle) => { + const response = await fetch(chatUrl(handle, 'acme'), { + method: 'POST', + headers: authHeaders(handle), + body: JSON.stringify({ model: 'm', messages: [{ role: 'user', content: 'weather?' }] }), + }); + const text = await response.text(); + assert.ok(text.includes('"tool_calls"'), `expected tool_calls in SSE: ${text}`); + assert.ok(text.includes('"finish_reason":"tool_calls"'), `expected tool_calls finish reason: ${text}`); + assert.ok(text.includes('getWeather')); + }, + ); + }); + + test('returns a 502 when the bridge reports an error', async () => { + await withProxy( + async () => ({ content: '', error: 'model unavailable' }), + async (handle) => { + const response = await fetch(chatUrl(handle, 'acme'), { + method: 'POST', + headers: authHeaders(handle), + body: JSON.stringify({ model: 'm', messages: [] }), + }); + assert.strictEqual(response.status, 502); + const body = await response.json() as { error?: { message?: string } }; + assert.strictEqual(body.error?.message, 'model unavailable'); + }, + ); + }); + + test('returns a 502 when the bridge throws', async () => { + await withProxy( + async () => { throw new Error('bridge exploded'); }, + async (handle) => { + const response = await fetch(chatUrl(handle, 'acme'), { + method: 'POST', + headers: authHeaders(handle), + body: JSON.stringify({ model: 'm', messages: [] }), + }); + assert.strictEqual(response.status, 502); + const body = await response.json() as { error?: { message?: string } }; + assert.strictEqual(body.error?.message, 'bridge exploded'); + }, + ); + }); + + test('rejects a malformed JSON body with 400', async () => { + await withProxy( + async () => ({ content: 'unused' }), + async (handle) => { + const response = await fetch(chatUrl(handle, 'acme'), { + method: 'POST', + headers: authHeaders(handle), + body: 'not json', + }); + assert.strictEqual(response.status, 400); + }, + ); + }); + + test('returns a 503 when no renderer bridge is connected', async () => { + const registry = new ByokLmBridgeRegistry(); + const service = new ByokLmProxyService(new NullLogService(), registry); + const handle = await service.start(); + try { + const response = await fetch(chatUrl(handle, 'acme'), { + method: 'POST', + headers: authHeaders(handle), + body: JSON.stringify({ model: 'm', messages: [] }), + }); + assert.strictEqual(response.status, 503); + } finally { + handle.dispose(); + service.dispose(); + } + }); + + test('rebinds with a fresh nonce after every handle is disposed', async () => { + const registry = new ByokLmBridgeRegistry(); + const registration = registry.register('client-1', { chat: async () => ({ content: 'ok' }) }); + const service = new ByokLmProxyService(new NullLogService(), registry); + const first = await service.start(); + const firstNonce = first.nonce; + first.dispose(); + const second = await service.start(); + try { + assert.notStrictEqual(second.nonce, firstNonce); + } finally { + second.dispose(); + registration.dispose(); + service.dispose(); + } + }); +}); diff --git a/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts b/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts new file mode 100644 index 00000000000000..f60b8acdc09e83 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import type { IByokLmChatResult } from '../../common/agentHostByokLm.js'; +import { + bridgeResultToSseFrames, + openAiRequestToBridge, + OpenAiTranslationError, + type IOpenAiChatRequest, +} from '../../node/copilot/byokOpenAiTranslation.js'; + +suite('byokOpenAiTranslation', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('openAiRequestToBridge', () => { + + test('maps roles, text content, tools and options', () => { + const body: IOpenAiChatRequest = { + model: 'claude-sonnet', + temperature: 0.5, + max_tokens: 256, + messages: [ + { role: 'system', content: 'be helpful' }, + { role: 'user', content: [{ type: 'text', text: 'hi ' }, { type: 'text', text: 'there' }] }, + { + role: 'assistant', + content: '', + tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'getWeather', arguments: '{"city":"NYC"}' } }], + }, + { role: 'tool', tool_call_id: 'call_1', content: 'sunny' }, + ], + tools: [{ type: 'function', function: { name: 'getWeather', description: 'weather', parameters: { type: 'object' } } }], + }; + + const result = openAiRequestToBridge('acme', body); + + assert.deepStrictEqual(result, { + vendor: 'acme', + modelId: 'claude-sonnet', + messages: [ + { role: 'system', content: 'be helpful', toolCalls: undefined, toolCallId: undefined }, + { role: 'user', content: 'hi there', toolCalls: undefined, toolCallId: undefined }, + { role: 'assistant', content: '', toolCalls: [{ id: 'call_1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }], toolCallId: undefined }, + { role: 'tool', content: 'sunny', toolCalls: undefined, toolCallId: 'call_1' }, + ], + tools: [{ name: 'getWeather', description: 'weather', parametersSchema: { type: 'object' } }], + modelOptions: { temperature: 0.5, max_tokens: 256 }, + }); + }); + + test('throws when model is missing', () => { + assert.throws(() => openAiRequestToBridge('acme', { messages: [] }), OpenAiTranslationError); + }); + + test('omits tools and options when absent', () => { + const result = openAiRequestToBridge('acme', { model: 'm', messages: [{ role: 'user', content: 'hello' }] }); + assert.strictEqual(result.tools, undefined); + assert.strictEqual(result.modelOptions, undefined); + }); + }); + + suite('bridgeResultToSseFrames', () => { + + function parseFrames(frames: string[]): unknown[] { + return frames + .map(frame => frame.replace(/^data: /, '').trim()) + .filter(payload => payload !== '[DONE]') + .map(payload => JSON.parse(payload)); + } + + test('emits role, content and stop frames terminated by [DONE]', () => { + const result: IByokLmChatResult = { content: 'hello world' }; + const frames = bridgeResultToSseFrames(result, 'm'); + + assert.strictEqual(frames[frames.length - 1], 'data: [DONE]\n\n'); + const parsed = parseFrames(frames) as Array<{ choices: Array<{ delta: Record; finish_reason: string | null }> }>; + assert.deepStrictEqual(parsed.map(p => p.choices[0].delta), [ + { role: 'assistant' }, + { content: 'hello world' }, + {}, + ]); + assert.strictEqual(parsed[parsed.length - 1].choices[0].finish_reason, 'stop'); + }); + + test('encodes tool calls and a tool_calls finish reason', () => { + const result: IByokLmChatResult = { + content: '', + toolCalls: [{ id: 'call_1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }], + }; + const frames = bridgeResultToSseFrames(result, 'm'); + const parsed = parseFrames(frames) as Array<{ choices: Array<{ delta: Record; finish_reason: string | null }> }>; + + const toolDelta = parsed.find(p => p.choices[0].delta.tool_calls !== undefined); + assert.deepStrictEqual(toolDelta?.choices[0].delta.tool_calls, [ + { index: 0, id: 'call_1', type: 'function', function: { name: 'getWeather', arguments: '{"city":"NYC"}' } }, + ]); + assert.strictEqual(parsed[parsed.length - 1].choices[0].finish_reason, 'tool_calls'); + }); + }); +}); From 26b7c6b22c545bae1fbada8cbcfcda96e0f4bb8e Mon Sep 17 00:00:00 2001 From: vritant24 Date: Tue, 23 Jun 2026 10:09:21 -0700 Subject: [PATCH 003/589] agentHost: add opt-in BYOK language-model bridge for agent-host sessions --- .../common/agentHostClientByokLmChannel.ts | 64 +++++++ .../agentHostStarter.config.contribution.ts | 7 + .../platform/agentHost/common/agentService.ts | 21 +++ .../electron-browser/localAgentHostService.ts | 35 +++- .../electron-main/electronAgentHostStarter.ts | 3 +- .../node/agentHostClientReverseRpc.ts | 44 +++++ .../platform/agentHost/node/agentHostMain.ts | 36 +++- .../agentHost/node/nodeAgentHostStarter.ts | 3 +- .../test/common/agentService.test.ts | 27 ++- .../node/agentHostClientByokLmChannel.test.ts | 68 +++++++ .../node/agentHostClientReverseRpc.test.ts | 61 +++++++ .../agentHost/agentHostByokLmHandler.ts | 154 ++++++++++++++++ .../electron-browser/chat.contribution.ts | 6 + .../agentHostByokLmHandler.test.ts | 168 ++++++++++++++++++ 14 files changed, 681 insertions(+), 16 deletions(-) create mode 100644 src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts create mode 100644 src/vs/platform/agentHost/node/agentHostClientReverseRpc.ts create mode 100644 src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts create mode 100644 src/vs/platform/agentHost/test/node/agentHostClientReverseRpc.test.ts create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts diff --git a/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts b/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts new file mode 100644 index 00000000000000..49f2ebca10af20 --- /dev/null +++ b/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { Event } from '../../../base/common/event.js'; +import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { + IAgentHostByokLmHandler, + IByokLmBridgeConnection, + IByokLmChatRequest, + IByokLmChatResult, +} from './agentHostByokLm.js'; + +/** + * IPC channel name used for in-process agent-host → renderer reverse BYOK + * language-model RPCs. The renderer registers a server channel under this + * name on its `MessagePortClient`; the agent host reaches it via + * `server.getChannel(name, c => c.ctx === clientId)` on its + * `UtilityProcessServer`. + * + * Mirrors {@link AGENT_HOST_CLIENT_RESOURCE_CHANNEL} for the reverse FS bridge. + */ +export const AGENT_HOST_CLIENT_BYOK_LM_CHANNEL = 'agentHostClientByokLm'; + +/** + * Wraps an {@link IChannel} (obtained from the agent host's + * `UtilityProcessServer.getChannel`) into an {@link IByokLmBridgeConnection} + * suitable for the node-side {@link IByokLmProxyService}. This is the node end + * of the bridge: `chat()` ships the request to the renderer and resolves with + * the buffered completion the renderer produced from the LM API. + */ +export function createAgentHostClientByokLmConnection(channel: IChannel): IByokLmBridgeConnection { + return { + chat: (request) => channel.call('chat', request) as Promise, + }; +} + +/** + * Server-side channel for in-process reverse BYOK LM RPCs from the local agent + * host. Thin adapter — forwards `chat` calls to the renderer's + * {@link IAgentHostByokLmHandler} (backed by `ILanguageModelsService`). + */ +export class AgentHostClientByokLmChannel implements IServerChannel { + + constructor( + @IAgentHostByokLmHandler private readonly _handler: IAgentHostByokLmHandler, + ) { } + + listen(_ctx: unknown, event: string): Event { + throw new Error(`No event '${event}' on AgentHostClientByokLmChannel`); + } + + async call(_ctx: unknown, command: string, arg?: unknown): Promise { + switch (command) { + case 'chat': { + const result = await this._handler.chat(arg as IByokLmChatRequest, CancellationToken.None); + return result as T; + } + } + throw new Error(`Unknown command '${command}' on AgentHostClientByokLmChannel`); + } +} diff --git a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts index 2d79db08f74716..129cf2c8c691c4 100644 --- a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts +++ b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts @@ -9,6 +9,7 @@ import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurati import product from '../../product/common/product.js'; import { Registry } from '../../registry/common/platform.js'; import { + AgentHostByokModelsEnabledSettingId, AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, @@ -54,6 +55,12 @@ configurationRegistry.registerConfiguration({ name: 'Claude3PIntegration', }, }, + [AgentHostByokModelsEnabledSettingId]: { + type: 'boolean', + description: nls.localize('chat.agentHost.byokModels.enabled', "When enabled, the agent host wires up the BYOK ('bring your own key') language-model bridge so extension-provided BYOK models can run in agent-host sessions. Requires `#chat.agentHost.enabled#`. The agent host process must be restarted for changes to take effect."), + default: false, + tags: ['experimental', 'advanced'], + }, [AgentHostCodexAgentEnabledSettingId]: { type: 'boolean', description: nls.localize('chat.agentHost.codexAgent.enabled', "When enabled, the agent host registers the Codex provider (subject to the Codex SDK being reachable). Requires `#chat.agentHost.enabled#`. The agent host process must be restarted for changes to take effect."), diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 3de8693ee6d391..ceda4fe022ca22 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -86,6 +86,16 @@ export const AgentHostClaudeAgentEnabledSettingId = 'chat.agentHost.claudeAgent. */ export const AgentHostCodexAgentEnabledSettingId = 'chat.agentHost.codexAgent.enabled'; +/** + * Configuration key controlling whether the agent host wires up the BYOK + * ("bring your own key") language-model bridge: the renderer LM handler, the + * reverse-RPC channel, and the node-side OpenAI proxy + bridge registry. When + * `false` (the default), none of the BYOK additions are registered on either + * side, so extension-provided BYOK models are never reachable from agent-host + * sessions. The agent host process must be restarted for changes to take effect. + */ +export const AgentHostByokModelsEnabledSettingId = 'chat.agentHost.byokModels.enabled'; + /** * Optional override that points at an **SDK root directory** containing a * `node_modules/@anthropic-ai/claude-agent-sdk` subtree. When set, the agent @@ -111,6 +121,13 @@ export const AgentHostClaudeAgentEnabledEnvVar = 'VSCODE_AGENT_HOST_CLAUDE_AGENT */ export const AgentHostCodexAgentEnabledEnvVar = 'VSCODE_AGENT_HOST_CODEX_AGENT_ENABLED'; +/** + * Environment variable form of {@link AgentHostByokModelsEnabledSettingId}. + * Set by the agent host starters from the setting. Accepts `'true'` / + * `'false'`; absent means "default" (`false`). + */ +export const AgentHostByokModelsEnabledEnvVar = 'VSCODE_AGENT_HOST_BYOK_MODELS_ENABLED'; + /** * Resolves the effective enable state for a Claude/Codex provider from the * env-var value forwarded by the starter. Recognized values (case- and @@ -386,6 +403,7 @@ export interface IAgentSdkStarterSettings { readonly codexBinaryArgs?: readonly string[]; readonly claudeAgentEnabled?: boolean; readonly codexAgentEnabled?: boolean; + readonly byokModelsEnabled?: boolean; } export function buildAgentSdkEnv( @@ -410,6 +428,9 @@ export function buildAgentSdkEnv( if (settings.codexAgentEnabled !== undefined) { setIfMissing(AgentHostCodexAgentEnabledEnvVar, settings.codexAgentEnabled ? 'true' : 'false'); } + if (settings.byokModelsEnabled !== undefined) { + setIfMissing(AgentHostByokModelsEnabledEnvVar, settings.byokModelsEnabled ? 'true' : 'false'); + } return out; } diff --git a/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts b/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts index 45e744031786f6..ff9a7a44858c7b 100644 --- a/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts +++ b/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts @@ -8,14 +8,14 @@ import { Emitter, Relay } from '../../../base/common/event.js'; import { Disposable, DisposableStore, IReference } from '../../../base/common/lifecycle.js'; import { IObservable, ISettableObservable, observableValue } from '../../../base/common/observable.js'; import { generateUuid } from '../../../base/common/uuid.js'; -import { getDelayedChannel, ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { getDelayedChannel, IChannelServer, ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; import { Client as MessagePortClient } from '../../../base/parts/ipc/common/ipc.mp.js'; import { acquirePort } from '../../../base/parts/ipc/electron-browser/ipc.mp.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { ILogService } from '../../log/common/log.js'; -import { AgentHostAhpJsonlLoggingSettingId, AgentHostIpcChannels, IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentHostInspectInfo, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IAgentHostSocketInfo, IConnectionTrackerService, isAgentHostEnabled, IMcpNotification } from '../common/agentService.js'; +import { AgentHostAhpJsonlLoggingSettingId, AgentHostByokModelsEnabledSettingId, AgentHostIpcChannels, IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentHostInspectInfo, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IAgentHostSocketInfo, IConnectionTrackerService, isAgentHostEnabled, IMcpNotification } from '../common/agentService.js'; import { AhpJsonlLogger } from '../common/ahpJsonlLogger.js'; import { wrapAgentServiceWithAhpLogging } from './localAhpJsonlLogging.js'; import { AgentSubscriptionManager, type IActiveSubscriptionInfo, type IAgentSubscription } from '../common/state/agentSubscription.js'; @@ -28,6 +28,7 @@ import { StateComponents, ROOT_STATE_URI, parseChatUri, type RootState } from '. import { revive } from '../../../base/common/marshalling.js'; import { URI } from '../../../base/common/uri.js'; import { AGENT_HOST_CLIENT_RESOURCE_CHANNEL, AgentHostClientResourceChannel } from '../common/agentHostClientResourceChannel.js'; +import { AGENT_HOST_CLIENT_BYOK_LM_CHANNEL, AgentHostClientByokLmChannel } from '../common/agentHostClientByokLmChannel.js'; import { TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from '../../telemetry/common/telemetry.js'; import { getTelemetryLevel } from '../../telemetry/common/telemetryUtils.js'; import { AgentHostTelemetryLevelConfigKey, AgentHostSessionSyncEnabledConfigKey, SESSION_SYNC_ENABLED_SETTING_ID, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js'; @@ -143,10 +144,7 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos // calls (vscode-agent-client filesystem reads) back to this renderer // via `IPCServer.getChannel(name, c => c.ctx === clientId)`. const client = store.add(new MessagePortClient(port, this.clientId)); - // Serve filesystem reverse-RPCs from the local file service. The - // agent host registers an authority on its - // AgentHostClientFileSystemProvider that calls back through this channel. - client.registerChannel(AGENT_HOST_CLIENT_RESOURCE_CHANNEL, this._instantiationService.createInstance(AgentHostClientResourceChannel, this._ahpLogger)); + registerAgentHostClientChannels(client, this._instantiationService, this._ahpLogger, this._configurationService.getValue(AgentHostByokModelsEnabledSettingId) === true); this._clientEventually.complete(client); this._updateTelemetryLevel(); this._updateSessionSyncEnabled(); @@ -342,3 +340,28 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos return this._connectionTracker.getInspectInfo(tryEnable); } } + +/** + * Register the reverse-RPC server channels every in-process renderer exposes to + * the agent host's {@link UtilityProcessServer}: the filesystem resource bridge + * ({@link AGENT_HOST_CLIENT_RESOURCE_CHANNEL}) and the BYOK language-model + * bridge ({@link AGENT_HOST_CLIENT_BYOK_LM_CHANNEL}). The agent host reaches + * these via `server.getChannel(name, c => c.ctx === clientId)`. + */ +export function registerAgentHostClientChannels( + client: IChannelServer, + instantiationService: IInstantiationService, + ahpLogger: AhpJsonlLogger | undefined, + byokEnabled: boolean, +): void { + // Serve filesystem reverse-RPCs from the local file service. The agent host + // registers an authority on its AgentHostClientFileSystemProvider that calls + // back through this channel. + client.registerChannel(AGENT_HOST_CLIENT_RESOURCE_CHANNEL, instantiationService.createInstance(AgentHostClientResourceChannel, ahpLogger)); + // Serve BYOK language-model reverse-RPCs from the renderer LM API, gated + // behind `chat.agentHost.byokModels.enabled`. When disabled, the node-side + // proxy + registry are also skipped, so the channel would never be called. + if (byokEnabled) { + client.registerChannel(AGENT_HOST_CLIENT_BYOK_LM_CHANNEL, instantiationService.createInstance(AgentHostClientByokLmChannel)); + } +} diff --git a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts index 054f96ca48969c..4dd180c31e0ca0 100644 --- a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts +++ b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts @@ -19,7 +19,7 @@ import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; import { NullTelemetryService } from '../../telemetry/common/telemetryUtils.js'; import { UtilityProcess } from '../../utilityProcess/electron-main/utilityProcess.js'; import { IAgentHostConnection, IAgentHostStarter } from '../common/agent.js'; -import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; +import { AgentHostByokModelsEnabledSettingId, AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; import { deepClone } from '../../../base/common/objects.js'; import '../common/agentHost.config.contribution.js'; import '../common/agentHostStarter.config.contribution.js'; @@ -75,6 +75,7 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt codexBinaryArgs: this._configurationService.getValue(AgentHostCodexAgentBinaryArgsSettingId), claudeAgentEnabled: this._configurationService.getValue(AgentHostClaudeAgentEnabledSettingId), codexAgentEnabled: this._configurationService.getValue(AgentHostCodexAgentEnabledSettingId), + byokModelsEnabled: this._configurationService.getValue(AgentHostByokModelsEnabledSettingId), }, process.env); // Translate `chat.agentHost.otel.*` settings into the env vars consumed by diff --git a/src/vs/platform/agentHost/node/agentHostClientReverseRpc.ts b/src/vs/platform/agentHost/node/agentHostClientReverseRpc.ts new file mode 100644 index 00000000000000..6f29a58d65010b --- /dev/null +++ b/src/vs/platform/agentHost/node/agentHostClientReverseRpc.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; +import { IChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { AGENT_HOST_CLIENT_RESOURCE_CHANNEL, createAgentHostClientResourceConnection } from '../common/agentHostClientResourceChannel.js'; +import { AGENT_HOST_CLIENT_BYOK_LM_CHANNEL, createAgentHostClientByokLmConnection } from '../common/agentHostClientByokLmChannel.js'; +import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js'; +import { IByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; + +/** + * Wire the per-connection reverse-RPC bridges every in-process renderer exposes + * to the agent host's `UtilityProcessServer`: the filesystem resource bridge + * ({@link AGENT_HOST_CLIENT_RESOURCE_CHANNEL}) and the BYOK language-model + * bridge ({@link AGENT_HOST_CLIENT_BYOK_LM_CHANNEL}). The filesystem bridge lets + * the agent host read files from the connected renderer's workspace; the BYOK + * bridge lets the node-side OpenAI proxy reach the renderer's LM API for + * extension-provided models. Disposing the result tears both bridges down for + * that connection. + * + * @param getChannel Resolves a renderer server channel by name, already scoped + * to the connection's `clientId` (e.g. `name => server.getChannel(name, c => c.ctx === clientId)`). + */ +export function registerAgentHostClientReverseRpc( + clientId: string, + getChannel: (channelName: string) => IChannel, + clientFileSystemProvider: Pick, + byokLmBridgeRegistry: IByokLmBridgeRegistry | undefined, +): IDisposable { + const store = new DisposableStore(); + const fsConnection = createAgentHostClientResourceConnection(getChannel(AGENT_HOST_CLIENT_RESOURCE_CHANNEL)); + store.add(clientFileSystemProvider.registerAuthority(clientId, fsConnection)); + // The BYOK bridge is wired only when the feature is enabled — the caller + // passes `undefined` when `chat.agentHost.byokModels.enabled` is off. In that + // case the renderer also skips registering the BYOK server channel, so there + // is nothing to bridge. + if (byokLmBridgeRegistry) { + const byokLmConnection = createAgentHostClientByokLmConnection(getChannel(AGENT_HOST_CLIENT_BYOK_LM_CHANNEL)); + store.add(byokLmBridgeRegistry.register(clientId, byokLmConnection)); + } + return store; +} diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index 20796dd4debdb7..14cfe9dd019396 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -15,7 +15,7 @@ import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import * as os from 'os'; import * as inspector from 'inspector'; -import { AgentHostClaudeAgentEnabledEnvVar, AgentHostCodexAgentEnabledEnvVar, AgentHostIpcChannels, IAgentHostInspectInfo, IAgentHostSocketInfo, IAgentService, IConnectionTrackerService, isAgentEnabled } from '../common/agentService.js'; +import { AgentHostByokModelsEnabledEnvVar, AgentHostClaudeAgentEnabledEnvVar, AgentHostCodexAgentEnabledEnvVar, AgentHostIpcChannels, IAgentHostInspectInfo, IAgentHostSocketInfo, IAgentService, IConnectionTrackerService, isAgentEnabled } from '../common/agentService.js'; import { AgentService } from './agentService.js'; import { IAgentConfigurationService } from './agentConfigurationService.js'; import { IAgentHostCompletions } from './agentHostCompletions.js'; @@ -28,6 +28,8 @@ import { ClaudeAgentSdkService, ClaudeSdkPackage, IClaudeAgentSdkService } from import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxyService.js'; import { CodexAgent, CodexSdkPackage } from './codex/codexAgent.js'; import { CodexProxyService, ICodexProxyService } from './codex/codexProxyService.js'; +import { ByokLmProxyService, IByokLmProxyService } from './copilot/byokLmProxyService.js'; +import { ByokLmBridgeRegistry, IByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; import { AgentSdkDownloader, IAgentSdkDownloader } from './agentSdkDownloader.js'; import { IAgentHostOTelService } from '../common/otel/agentHostOTelService.js'; import { AgentHostOTelService } from './otel/agentHostOTelService.js'; @@ -63,7 +65,7 @@ import { NodeWorkerDiffComputeService } from './diffComputeService.js'; import { IEditSurvivalReporterFactory, EditSurvivalReporterFactory } from './shared/editSurvivalReporter.js'; import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js'; import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js'; -import { AGENT_HOST_CLIENT_RESOURCE_CHANNEL, createAgentHostClientResourceConnection } from '../common/agentHostClientResourceChannel.js'; +import { registerAgentHostClientReverseRpc } from './agentHostClientReverseRpc.js'; import { IAgentPluginManager } from '../common/agentPluginManager.js'; import { AgentPluginManager } from './agentPluginManager.js'; import { AgentHostGitService } from './agentHostGitService.js'; @@ -131,6 +133,11 @@ async function startAgentHost(): Promise { // Create the real service implementation that lives in this process let agentService: AgentService; let instantiationService: IInstantiationService; + // Gate all BYOK agent-host additions (proxy, bridge registry, reverse-RPC + // bridge) behind the opt-in `chat.agentHost.byokModels.enabled` setting, + // forwarded from the renderer as an env var. When off, none of the BYOK + // wiring is created here and the renderer skips the BYOK server channel. + const byokLmEnabled = isAgentEnabled(process.env[AgentHostByokModelsEnabledEnvVar], false); try { // Build the DI container early so the git service can be created via // `createInstance` (it needs IFileService + INativeEnvironmentService). @@ -172,6 +179,16 @@ async function startAgentHost(): Promise { diServices.set(IClaudeAgentSdkService, claudeAgentSdkService); const codexProxyService = disposables.add(instantiationService.createInstance(CodexProxyService)); diServices.set(ICodexProxyService, codexProxyService); + // BYOK language-model proxy + bridge registry, gated behind + // `chat.agentHost.byokModels.enabled`. The registry is populated per + // renderer connection below; the proxy lazily binds when the session + // launcher starts it for a session that selected a forwarded BYOK model. + if (byokLmEnabled) { + const byokLmBridgeRegistry = new ByokLmBridgeRegistry(); + diServices.set(IByokLmBridgeRegistry, byokLmBridgeRegistry); + const byokLmProxyService = disposables.add(instantiationService.createInstance(ByokLmProxyService)); + diServices.set(IByokLmProxyService, byokLmProxyService); + } const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService)); diServices.set(IAgentHostOTelService, agentHostOTelService); agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, checkpointService, rootConfigResource, telemetryService, fileMonitorService); @@ -217,10 +234,12 @@ async function startAgentHost(): Promise { // in-process renderer-to-utility-process MessagePort transport). const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider()); disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, clientFileSystemProvider)); + const byokLmBridgeRegistry = byokLmEnabled ? instantiationService.invokeFunction(accessor => accessor.get(IByokLmBridgeRegistry)) : undefined; // Wire reverse-RPC for in-process renderer connections. The renderer's - // `MessagePortClient` ctx is its `clientId`, and it exposes - // `AGENT_HOST_CLIENT_RESOURCE_CHANNEL` for filesystem reads. + // `MessagePortClient` ctx is its `clientId`, and it exposes the + // `AGENT_HOST_CLIENT_RESOURCE_CHANNEL` (filesystem reads) and + // `AGENT_HOST_CLIENT_BYOK_LM_CHANNEL` (BYOK language-model calls). if (server instanceof UtilityProcessServer) { const authorityRegistrations = new Map(); const registerConnection = (connection: (typeof server.connections)[number]) => { @@ -231,9 +250,12 @@ async function startAgentHost(): Promise { if (typeof clientId !== 'string' || !clientId) { return; } - const channel = server.getChannel(AGENT_HOST_CLIENT_RESOURCE_CHANNEL, c => c.ctx === clientId); - const fsConnection = createAgentHostClientResourceConnection(channel); - authorityRegistrations.set(connection, clientFileSystemProvider.registerAuthority(clientId, fsConnection)); + authorityRegistrations.set(connection, registerAgentHostClientReverseRpc( + clientId, + channelName => server.getChannel(channelName, c => c.ctx === clientId), + clientFileSystemProvider, + byokLmBridgeRegistry, + )); }; disposables.add(server.onDidAddConnection(registerConnection)); disposables.add(server.onDidRemoveConnection(connection => { diff --git a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts index 8016d0f293cbc8..b4b2f8cc43b57a 100644 --- a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts +++ b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts @@ -13,7 +13,7 @@ import { parseAgentHostDebugPort } from '../../environment/node/environmentServi import { ILogService } from '../../log/common/log.js'; import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; import { IAgentHostConnection, IAgentHostStarter } from '../common/agent.js'; -import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; +import { AgentHostByokModelsEnabledSettingId, AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; import '../common/agentHostStarter.config.contribution.js'; /** @@ -83,6 +83,7 @@ export class NodeAgentHostStarter extends Disposable implements IAgentHostStarte codexBinaryArgs: this._configurationService.getValue(AgentHostCodexAgentBinaryArgsSettingId), claudeAgentEnabled: this._configurationService.getValue(AgentHostClaudeAgentEnabledSettingId), codexAgentEnabled: this._configurationService.getValue(AgentHostCodexAgentEnabledSettingId), + byokModelsEnabled: this._configurationService.getValue(AgentHostByokModelsEnabledSettingId), }, process.env); Object.assign(env, sdkEnv); diff --git a/src/vs/platform/agentHost/test/common/agentService.test.ts b/src/vs/platform/agentHost/test/common/agentService.test.ts index cd29d5fdfc9f92..1d2114f9cd3f5a 100644 --- a/src/vs/platform/agentHost/test/common/agentService.test.ts +++ b/src/vs/platform/agentHost/test/common/agentService.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { AgentSession, isAgentEnabled } from '../../common/agentService.js'; +import { AgentHostByokModelsEnabledEnvVar, AgentSession, buildAgentSdkEnv, isAgentEnabled } from '../../common/agentService.js'; suite('AgentSession namespace', () => { @@ -66,3 +66,28 @@ suite('isAgentEnabled', () => { }); } }); + +suite('buildAgentSdkEnv (BYOK gate forwarding)', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('forwards byokModelsEnabled=true as the enable env var', () => { + const env = buildAgentSdkEnv({ byokModelsEnabled: true }, {}); + assert.strictEqual(env[AgentHostByokModelsEnabledEnvVar], 'true'); + }); + + test('forwards byokModelsEnabled=false as the disable env var', () => { + const env = buildAgentSdkEnv({ byokModelsEnabled: false }, {}); + assert.strictEqual(env[AgentHostByokModelsEnabledEnvVar], 'false'); + }); + + test('omits the env var when byokModelsEnabled is undefined', () => { + const env = buildAgentSdkEnv({}, {}); + assert.strictEqual(env[AgentHostByokModelsEnabledEnvVar], undefined); + }); + + test('lets an inherited env var win over the setting (developer override)', () => { + const env = buildAgentSdkEnv({ byokModelsEnabled: true }, { [AgentHostByokModelsEnabledEnvVar]: 'false' }); + assert.strictEqual(env[AgentHostByokModelsEnabledEnvVar], undefined); + }); +}); diff --git a/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts b/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts new file mode 100644 index 00000000000000..732798c5d450d2 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Event } from '../../../../base/common/event.js'; +import type { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import type { IAgentHostByokLmHandler, IByokLmChatRequest, IByokLmChatResult } from '../../common/agentHostByokLm.js'; +import { AgentHostClientByokLmChannel, createAgentHostClientByokLmConnection } from '../../common/agentHostClientByokLmChannel.js'; + +suite('agentHostClientByokLmChannel', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + function handlerOf(impl: (request: IByokLmChatRequest) => Promise): IAgentHostByokLmHandler { + return { _serviceBrand: undefined, chat: (request) => impl(request) }; + } + + /** + * Wire the node-side connection straight to the renderer server channel, + * standing in for the MessagePort transport so the full request → handler → + * response round-trip can be exercised without the renderer or the SDK. + */ + function bridge(handler: IAgentHostByokLmHandler) { + const server = new AgentHostClientByokLmChannel(handler); + const channel: IChannel = { + call(command: string, arg?: unknown): Promise { + return server.call(null, command, arg); + }, + listen(event: string): Event { + return server.listen(null, event); + }, + }; + return createAgentHostClientByokLmConnection(channel); + } + + test('round-trips a chat request to the handler and back', async () => { + let seen: IByokLmChatRequest | undefined; + const connection = bridge(handlerOf(async (request) => { + seen = request; + return { content: 'pong', toolCalls: [{ id: 'c1', name: 'noop', argumentsJson: '{}' }] }; + })); + + const request: IByokLmChatRequest = { vendor: 'acme', modelId: 'm', messages: [{ role: 'user', content: 'ping' }] }; + const result = await connection.chat(request); + + assert.deepStrictEqual(seen, request); + assert.deepStrictEqual(result, { content: 'pong', toolCalls: [{ id: 'c1', name: 'noop', argumentsJson: '{}' }] }); + }); + + test('forwards a bridge error result unchanged', async () => { + const connection = bridge(handlerOf(async () => ({ content: '', error: 'no model' }))); + const result = await connection.chat({ vendor: 'v', modelId: 'm', messages: [] }); + assert.strictEqual(result.error, 'no model'); + }); + + test('rejects unknown channel commands', async () => { + const server = new AgentHostClientByokLmChannel(handlerOf(async () => ({ content: '' }))); + await assert.rejects(() => server.call(null, 'frobnicate'), /Unknown command/); + }); + + test('exposes no events', () => { + const server = new AgentHostClientByokLmChannel(handlerOf(async () => ({ content: '' }))); + assert.throws(() => server.listen(null, 'anything'), /No event/); + }); +}); diff --git a/src/vs/platform/agentHost/test/node/agentHostClientReverseRpc.test.ts b/src/vs/platform/agentHost/test/node/agentHostClientReverseRpc.test.ts new file mode 100644 index 00000000000000..2e736a8bcb129a --- /dev/null +++ b/src/vs/platform/agentHost/test/node/agentHostClientReverseRpc.test.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { type IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import type { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import type { IByokLmBridgeConnection } from '../../common/agentHostByokLm.js'; +import type { AgentHostClientFileSystemProvider } from '../../common/agentHostClientFileSystemProvider.js'; +import { registerAgentHostClientReverseRpc } from '../../node/agentHostClientReverseRpc.js'; +import type { IByokLmBridgeRegistry } from '../../node/byokLmBridgeRegistry.js'; + +suite('registerAgentHostClientReverseRpc', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + class NullChannel extends mock() { } + const getChannel = (_channelName: string): IChannel => new NullChannel(); + + function fsProvider(): { provider: Pick; registered: string[] } { + const registered: string[] = []; + return { + registered, + provider: { registerAuthority: (clientId: string): IDisposable => { registered.push(clientId); return toDisposable(() => { }); } }, + }; + } + + function bridgeRegistry(): { registry: IByokLmBridgeRegistry; registered: string[] } { + const registered: string[] = []; + return { + registered, + registry: { + _serviceBrand: undefined, + register: (clientId: string, _connection: IByokLmBridgeConnection): IDisposable => { registered.push(clientId); return toDisposable(() => { }); }, + get: () => undefined, + getActive: () => undefined, + }, + }; + } + + test('registers both the filesystem and BYOK bridges when a registry is provided', () => { + const fs = fsProvider(); + const reg = bridgeRegistry(); + const store = registerAgentHostClientReverseRpc('client-1', getChannel, fs.provider, reg.registry); + assert.deepStrictEqual(fs.registered, ['client-1']); + assert.deepStrictEqual(reg.registered, ['client-1']); + store.dispose(); + }); + + test('registers only the filesystem bridge when the registry is undefined (BYOK gated off)', () => { + const fs = fsProvider(); + const reg = bridgeRegistry(); + const store = registerAgentHostClientReverseRpc('client-1', getChannel, fs.provider, undefined); + assert.deepStrictEqual(fs.registered, ['client-1']); + assert.deepStrictEqual(reg.registered, []); + store.dispose(); + }); +}); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts new file mode 100644 index 00000000000000..df95d758ba7c07 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { + IAgentHostByokLmHandler, + IByokLmChatMessage, + IByokLmChatRequest, + IByokLmChatResult, + IByokLmToolCall, +} from '../../../../../../platform/agentHost/common/agentHostByokLm.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { + ChatMessageRole, + IChatMessage, + IChatMessagePart, + ILanguageModelChatRequestOptions, + ILanguageModelsService, +} from '../../../common/languageModels.js'; + +/** + * Renderer-side {@link IAgentHostByokLmHandler}. Services BYOK chat requests + * forwarded by the node agent host's OpenAI proxy by calling the VS Code LM + * API for the matching extension-registered model. + * + * The bridge DTOs are plain/serializable; this class is the single place that + * translates them to and from the `workbench/contrib/chat` LM types. + */ +export class AgentHostByokLmHandler extends Disposable implements IAgentHostByokLmHandler { + + declare readonly _serviceBrand: undefined; + + constructor( + @ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + } + + async chat(request: IByokLmChatRequest, token: CancellationToken): Promise { + const modelIdentifier = this._resolveModelIdentifier(request.vendor, request.modelId); + if (!modelIdentifier) { + return { content: '', error: `No BYOK model found for ${request.vendor}/${request.modelId}` }; + } + + const messages = request.messages.map(message => this._toChatMessage(message)); + const tools = request.tools?.length + ? request.tools.map(tool => ({ + name: tool.name, + description: tool.description ?? '', + inputSchema: tool.parametersSchema, + })) + : undefined; + const options: ILanguageModelChatRequestOptions = { + modelOptions: request.modelOptions, + ...(tools ? { tools } : {}), + }; + + try { + const response = await this._languageModelsService.sendChatRequest(modelIdentifier, undefined, messages, options, token); + + let content = ''; + const toolCalls: IByokLmToolCall[] = []; + const streaming = (async () => { + for await (const part of response.stream) { + const parts = Array.isArray(part) ? part : [part]; + for (const p of parts) { + if (p.type === 'text') { + content += p.value; + } else if (p.type === 'tool_use') { + toolCalls.push({ + id: p.toolCallId, + name: p.name, + argumentsJson: JSON.stringify(p.parameters ?? {}), + }); + } + } + } + })(); + + await Promise.all([response.result, streaming]); + return { content, toolCalls: toolCalls.length ? toolCalls : undefined }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + this._logService.warn(`[AgentHostByokLmHandler] chat request failed for ${request.vendor}/${request.modelId}: ${message}`); + return { content: '', error: message }; + } + } + + /** + * Find the LM API identifier for a BYOK model addressed by its vendor and + * provider-local id (the `provider/id` selection id the picker surfaced). + */ + private _resolveModelIdentifier(vendor: string, modelId: string): string | undefined { + for (const identifier of this._languageModelsService.getLanguageModelIds()) { + const metadata = this._languageModelsService.lookupLanguageModel(identifier); + if (metadata?.isBYOK && metadata.vendor === vendor && metadata.id === modelId) { + return identifier; + } + } + return undefined; + } + + private _toChatMessage(message: IByokLmChatMessage): IChatMessage { + const content: IChatMessagePart[] = []; + if (message.content) { + content.push({ type: 'text', value: message.content }); + } + + if (message.role === 'assistant' && message.toolCalls?.length) { + for (const call of message.toolCalls) { + content.push({ + type: 'tool_use', + name: call.name, + toolCallId: call.id, + parameters: this._safeParseJson(call.argumentsJson), + }); + } + } + + if (message.role === 'tool' && message.toolCallId) { + content.push({ + type: 'tool_result', + toolCallId: message.toolCallId, + value: [{ type: 'text', value: message.content }], + }); + // Tool results ride on a user-role message in the LM API. + return { role: ChatMessageRole.User, content }; + } + + return { role: this._toChatRole(message.role), content }; + } + + private _toChatRole(role: IByokLmChatMessage['role']): ChatMessageRole { + switch (role) { + case 'system': return ChatMessageRole.System; + case 'assistant': return ChatMessageRole.Assistant; + case 'user': + case 'tool': + default: return ChatMessageRole.User; + } + } + + private _safeParseJson(json: string): unknown { + try { + return JSON.parse(json); + } catch { + return {}; + } + } +} diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index 39b6e252c2ecd2..eb1e86429b6f32 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -32,6 +32,7 @@ import { IWorkbenchLayoutService } from '../../../services/layout/browser/layout import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js'; import { ACTION_ID_NEW_CHAT, CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../browser/actions/chatActions.js'; import { AgentHostContribution } from '../browser/agentSessions/agentHost/agentHostChatContribution.js'; +import { AgentHostByokLmHandler } from '../browser/agentSessions/agentHost/agentHostByokLmHandler.js'; import { AgentHostSessionListContribution } from '../browser/agentSessions/agentHost/agentHostSessionListContribution.js'; import { AgentHostTerminalContribution } from '../browser/agentSessions/agentHost/agentHostTerminalContribution.js'; import { AgentHostCopilotPromptContribution } from '../browser/agentSessions/agentHost/agentHostCopilotPromptContribution.js'; @@ -40,6 +41,7 @@ import { IAgentSessionsService } from '../browser/agentSessions/agentSessionsSer import { ChatViewPaneTarget, IChatWidgetService } from '../browser/chat.js'; import { ChatSessionPosition, openChatSession } from '../browser/chatSessions/chatSessions.contribution.js'; import { IAgentHostService } from '../../../../platform/agentHost/common/agentService.js'; +import { IAgentHostByokLmHandler } from '../../../../platform/agentHost/common/agentHostByokLm.js'; import { type AgentInfo, type RootState } from '../../../../platform/agentHost/common/state/sessionState.js'; import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { IChatService } from '../common/chatService/chatService.js'; @@ -268,6 +270,10 @@ registerWorkbenchContribution2(AgentHostCopilotPromptContribution.ID, AgentHostC registerWorkbenchContribution2(OpenWorkspaceInAgentsContribution.ID, OpenWorkspaceInAgentsContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(AgentsHandoffInputTipContribution.ID, AgentsHandoffInputTipContribution, WorkbenchPhase.Eventually); +// Renderer-side BYOK language-model handler that backs the node agent host's +// OpenAI proxy. Lazily instantiated when AgentHostClientByokLmChannel resolves it. +registerSingleton(IAgentHostByokLmHandler, AgentHostByokLmHandler, InstantiationType.Delayed); + // How long to wait for the agent host to surface an AgentInfo before // throwing an error. Long enough for normal startup, short enough to avoid // hanging automation indefinitely if the agent host is disabled or fails diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts new file mode 100644 index 00000000000000..a16a68bac67f79 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { mock } from '../../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; +import { NullLogService } from '../../../../../../platform/log/common/log.js'; +import type { IByokLmChatRequest } from '../../../../../../platform/agentHost/common/agentHostByokLm.js'; +import { AgentHostByokLmHandler } from '../../../browser/agentSessions/agentHost/agentHostByokLmHandler.js'; +import { ChatMessageRole, IChatMessage, IChatResponsePart, ILanguageModelChatMetadata, ILanguageModelChatRequestOptions, ILanguageModelChatResponse, ILanguageModelsService } from '../../../common/languageModels.js'; + +interface ICapturedRequest { + modelId: string; + messages: IChatMessage[]; + options: ILanguageModelChatRequestOptions; +} + +/** + * Fake LM API service: resolves a small fixed model set and replays a + * scripted response stream, capturing what the handler forwarded. Stands in + * for the renderer's real `ILanguageModelsService` so the bridge handler can be + * exercised without any extension or model provider. + */ +class TestLanguageModelsService extends mock() { + + captured: ICapturedRequest | undefined; + + constructor( + private readonly _models: ReadonlyMap, + private readonly _respond: (request: ICapturedRequest) => ILanguageModelChatResponse, + ) { + super(); + } + + override getLanguageModelIds(): string[] { + return [...this._models.keys()]; + } + + override lookupLanguageModel(modelId: string): ILanguageModelChatMetadata | undefined { + return this._models.get(modelId); + } + + override async sendChatRequest(modelId: string, _from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, _token: CancellationToken): Promise { + this.captured = { modelId, messages, options }; + return this._respond(this.captured); + } +} + +function byokModel(vendor: string, id: string): ILanguageModelChatMetadata { + return { + extension: new ExtensionIdentifier('test.byok'), + name: `${vendor} ${id}`, + id, + vendor, + version: '1.0.0', + family: 'test', + maxInputTokens: 1000, + maxOutputTokens: 1000, + isDefaultForLocation: {}, + isBYOK: true, + }; +} + +function responseOf(parts: IChatResponsePart[]): ILanguageModelChatResponse { + return { + stream: (async function* () { + for (const part of parts) { + yield part; + } + })(), + result: Promise.resolve(undefined), + }; +} + +suite('AgentHostByokLmHandler', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + function createHandler(service: ILanguageModelsService): AgentHostByokLmHandler { + return store.add(new AgentHostByokLmHandler(service, new NullLogService())); + } + + test('resolves the BYOK model and buffers text + tool calls', async () => { + const service = new TestLanguageModelsService( + new Map([['id-acme-claude', byokModel('acme', 'claude')]]), + () => responseOf([ + { type: 'text', value: 'hello ' }, + { type: 'text', value: 'world' }, + { type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }, + ]), + ); + const handler = createHandler(service); + + const result = await handler.chat( + { vendor: 'acme', modelId: 'claude', messages: [{ role: 'user', content: 'hi' }] }, + CancellationToken.None, + ); + + assert.strictEqual(service.captured?.modelId, 'id-acme-claude'); + assert.deepStrictEqual(result, { + content: 'hello world', + toolCalls: [{ id: 't1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }], + }); + }); + + test('maps bridge messages to LM API chat messages', async () => { + const service = new TestLanguageModelsService( + new Map([['id', byokModel('acme', 'claude')]]), + () => responseOf([{ type: 'text', value: 'ok' }]), + ); + const handler = createHandler(service); + + await handler.chat( + { + vendor: 'acme', + modelId: 'claude', + messages: [ + { role: 'system', content: 'be helpful' }, + { role: 'user', content: 'hi' }, + { role: 'assistant', content: '', toolCalls: [{ id: 't1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }] }, + { role: 'tool', content: 'sunny', toolCallId: 't1' }, + ], + }, + CancellationToken.None, + ); + + assert.deepStrictEqual(service.captured?.messages, [ + { role: ChatMessageRole.System, content: [{ type: 'text', value: 'be helpful' }] }, + { role: ChatMessageRole.User, content: [{ type: 'text', value: 'hi' }] }, + { role: ChatMessageRole.Assistant, content: [{ type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }] }, + // A `tool` message rides on a User-role message; its text is emitted both as a + // leading text part (shared `if (message.content)` branch) and inside the tool_result. + { role: ChatMessageRole.User, content: [{ type: 'text', value: 'sunny' }, { type: 'tool_result', toolCallId: 't1', value: [{ type: 'text', value: 'sunny' }] }] }, + ]); + }); + + test('returns an error result when no BYOK model matches', async () => { + const service = new TestLanguageModelsService(new Map(), () => responseOf([])); + const handler = createHandler(service); + + const result = await handler.chat( + { vendor: 'acme', modelId: 'missing', messages: [] } satisfies IByokLmChatRequest, + CancellationToken.None, + ); + + assert.strictEqual(result.content, ''); + assert.ok(result.error?.includes('acme/missing'), `expected error to name the model: ${result.error}`); + }); + + test('returns an error result when the LM request throws', async () => { + const service = new TestLanguageModelsService( + new Map([['id', byokModel('acme', 'claude')]]), + () => { throw new Error('provider exploded'); }, + ); + const handler = createHandler(service); + + const result = await handler.chat( + { vendor: 'acme', modelId: 'claude', messages: [{ role: 'user', content: 'hi' }] }, + CancellationToken.None, + ); + + assert.deepStrictEqual(result, { content: '', error: 'provider exploded' }); + }); +}); From 309b5b5a7010ef6d666336245751abf2d45c21fe Mon Sep 17 00:00:00 2001 From: vritant24 Date: Tue, 23 Jun 2026 10:58:57 -0700 Subject: [PATCH 004/589] propagate model list to the agent host --- .../agentHost/common/agentHostByokLm.ts | 23 +++++++ .../common/agentHostClientByokLmChannel.ts | 6 ++ .../platform/agentHost/node/agentHostMain.ts | 26 ++++---- .../node/copilot/copilotSessionLauncher.ts | 62 ++++++++++++++++++- .../node/agentHostClientByokLmChannel.test.ts | 15 ++++- .../test/node/byokLmProxyService.test.ts | 4 +- .../agentHost/agentHostByokLmHandler.ts | 19 ++++++ .../agentHostByokLmHandler.test.ts | 18 ++++++ 8 files changed, 155 insertions(+), 18 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHostByokLm.ts b/src/vs/platform/agentHost/common/agentHostByokLm.ts index c454a562e813a9..e8fb506dcc3e0b 100644 --- a/src/vs/platform/agentHost/common/agentHostByokLm.ts +++ b/src/vs/platform/agentHost/common/agentHostByokLm.ts @@ -75,6 +75,21 @@ export interface IByokLmChatResult { readonly error?: string; } +/** + * Metadata for a renderer BYOK model, enumerated over the bridge so the node + * agent host can advertise it to the SDK runtime without any host-side config. + */ +export interface IByokLmModelInfo { + /** Provider/vendor name (the LM API vendor that registered the model). */ + readonly vendor: string; + /** Provider-local model id. */ + readonly id: string; + /** Display name, when the provider supplies one. */ + readonly name?: string; + /** Maximum context window tokens, when known. */ + readonly maxContextWindowTokens?: number; +} + export const IAgentHostByokLmHandler = createDecorator('agentHostByokLmHandler'); /** @@ -92,6 +107,13 @@ export interface IAgentHostByokLmHandler { * {@link IByokLmChatResult.error}) when no such model is available. */ chat(request: IByokLmChatRequest, token: CancellationToken): Promise; + + /** + * Enumerate the renderer's BYOK models (vendor `isBYOK`, excluding + * session-scoped agent-host copies) so the node agent host can synthesize + * provider/model config for the SDK runtime. + */ + listModels(token: CancellationToken): Promise; } /** @@ -100,4 +122,5 @@ export interface IAgentHostByokLmHandler { */ export interface IByokLmBridgeConnection { chat(request: IByokLmChatRequest): Promise; + listModels(): Promise; } diff --git a/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts b/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts index 49f2ebca10af20..e029f48af559bf 100644 --- a/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts +++ b/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts @@ -11,6 +11,7 @@ import { IByokLmBridgeConnection, IByokLmChatRequest, IByokLmChatResult, + IByokLmModelInfo, } from './agentHostByokLm.js'; /** @@ -34,6 +35,7 @@ export const AGENT_HOST_CLIENT_BYOK_LM_CHANNEL = 'agentHostClientByokLm'; export function createAgentHostClientByokLmConnection(channel: IChannel): IByokLmBridgeConnection { return { chat: (request) => channel.call('chat', request) as Promise, + listModels: () => channel.call('listModels') as Promise, }; } @@ -58,6 +60,10 @@ export class AgentHostClientByokLmChannel implements IServerChannel { const result = await this._handler.chat(arg as IByokLmChatRequest, CancellationToken.None); return result as T; } + case 'listModels': { + const models = await this._handler.listModels(CancellationToken.None); + return models as T; + } } throw new Error(`Unknown command '${command}' on AgentHostClientByokLmChannel`); } diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index ab34263cf93fc5..fec21d60457b83 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -179,16 +179,15 @@ async function startAgentHost(): Promise { diServices.set(IClaudeAgentSdkService, claudeAgentSdkService); const codexProxyService = disposables.add(instantiationService.createInstance(CodexProxyService)); diServices.set(ICodexProxyService, codexProxyService); - // BYOK language-model proxy + bridge registry, gated behind - // `chat.agentHost.byokModels.enabled`. The registry is populated per - // renderer connection below; the proxy lazily binds when the session - // launcher starts it for a session that selected a forwarded BYOK model. - if (byokLmEnabled) { - const byokLmBridgeRegistry = new ByokLmBridgeRegistry(); - diServices.set(IByokLmBridgeRegistry, byokLmBridgeRegistry); - const byokLmProxyService = disposables.add(instantiationService.createInstance(ByokLmProxyService)); - diServices.set(IByokLmProxyService, byokLmProxyService); - } + // BYOK language-model proxy + bridge registry. Always registered so the + // session launcher can inject them, but BYOK *use* is gated: the + // per-connection bridge below (and the renderer's server channel) are only + // wired when `chat.agentHost.byokModels.enabled` is on, so the registry + // stays empty and the proxy never binds when the feature is off. + const byokLmBridgeRegistry = new ByokLmBridgeRegistry(); + diServices.set(IByokLmBridgeRegistry, byokLmBridgeRegistry); + const byokLmProxyService = disposables.add(instantiationService.createInstance(ByokLmProxyService)); + diServices.set(IByokLmProxyService, byokLmProxyService); const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService)); diServices.set(IAgentHostOTelService, agentHostOTelService); agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, checkpointService, rootConfigResource, telemetryService, fileMonitorService); @@ -235,7 +234,7 @@ async function startAgentHost(): Promise { // in-process renderer-to-utility-process MessagePort transport). const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider()); disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, clientFileSystemProvider)); - const byokLmBridgeRegistry = byokLmEnabled ? instantiationService.invokeFunction(accessor => accessor.get(IByokLmBridgeRegistry)) : undefined; + const byokLmBridgeRegistry = instantiationService.invokeFunction(accessor => accessor.get(IByokLmBridgeRegistry)); // Wire reverse-RPC for in-process renderer connections. The renderer's // `MessagePortClient` ctx is its `clientId`, and it exposes the @@ -255,7 +254,10 @@ async function startAgentHost(): Promise { clientId, channelName => server.getChannel(channelName, c => c.ctx === clientId), clientFileSystemProvider, - byokLmBridgeRegistry, + // BYOK bridge is gated: only wire it when the feature is enabled, so + // the registry stays empty (and the launcher synthesizes no BYOK + // providers/models) when `chat.agentHost.byokModels.enabled` is off. + byokLmEnabled ? byokLmBridgeRegistry : undefined, )); }; disposables.add(server.onDidAddConnection(registerConnection)); diff --git a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts index a28716b1e4e99e..4ca27076e1ac7d 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { CopilotClient, ExitPlanModeRequest, ExitPlanModeResult, PermissionRequestResult, ResumeSessionConfig, SessionConfig, Tool } from '@github/copilot-sdk'; +import type { CopilotClient, ExitPlanModeRequest, ExitPlanModeResult, NamedProviderConfig, PermissionRequestResult, ProviderModelConfig, ResumeSessionConfig, SessionConfig, Tool } from '@github/copilot-sdk'; import { coalesce } from '../../../../base/common/arrays.js'; import { Schemas } from '../../../../base/common/network.js'; import { URI } from '../../../../base/common/uri.js'; @@ -14,6 +14,9 @@ import { AgentHostSessionSyncEnabledConfigKey, platformRootSchema, type AgentHos import { AgentHostSandboxConfigKey, sandboxConfigSchema } from '../../common/sandboxConfigSchema.js'; import { IAgentConfigurationService } from '../agentConfigurationService.js'; import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js'; +import { IByokLmBridgeRegistry } from '../byokLmBridgeRegistry.js'; +import { IByokLmProxyService, type IByokLmProxyHandle } from './byokLmProxyService.js'; +import type { IByokLmModelInfo } from '../../common/agentHostByokLm.js'; import type { ModelSelection, ToolDefinition } from '../../common/state/protocol/state.js'; import type { ActiveClientState } from '../activeClientState.js'; import { CopilotSessionWrapper } from './copilotSessionWrapper.js'; @@ -195,11 +198,15 @@ export function getCopilotContextTier(model: ModelSelection | undefined): Sessio export class CopilotSessionLauncher implements ICopilotSessionLauncher { + private _byokProxyHandle: Promise | undefined; + constructor( @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService, @IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager, @ILogService private readonly _logService: ILogService, @IFileService private readonly _fileService: IFileService, + @IByokLmProxyService private readonly _byokLmProxyService: IByokLmProxyService, + @IByokLmBridgeRegistry private readonly _byokLmBridgeRegistry: IByokLmBridgeRegistry, ) { } async launch(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise { @@ -298,8 +305,60 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher { } } + /** + * Synthesize SDK provider/model config for the renderer's BYOK models so the + * runtime issues their inference against the node-side loopback proxy (which + * bridges back to the renderer LM API). Returns empty when BYOK is gated off + * (no active bridge), the renderer reports no BYOK models, or enumeration + * fails — in each case the session simply launches without BYOK models. + * + * Each provider points at `proxy.providerBaseUrl(vendor)` and authenticates + * with the session-scoped `Bearer .`; each model surfaces + * under the provider-qualified selection id `vendor/id`, matching what the + * renderer's `AgentHostByokLmHandler` resolves. + */ + private async _resolveByokSessionConfig(sessionId: string): Promise<{ providers?: NamedProviderConfig[]; models?: ProviderModelConfig[] }> { + const connection = this._byokLmBridgeRegistry.getActive(); + if (!connection) { + return {}; + } + let byokModels: IByokLmModelInfo[]; + try { + byokModels = await connection.listModels(); + } catch (err) { + this._logService.warn(`[Copilot:${sessionId}] Failed to enumerate BYOK models from renderer bridge`, err); + return {}; + } + if (byokModels.length === 0) { + return {}; + } + if (!this._byokProxyHandle) { + this._byokProxyHandle = this._byokLmProxyService.start(); + } + const handle = await this._byokProxyHandle; + const providers: NamedProviderConfig[] = [...new Set(byokModels.map(m => m.vendor))].map(vendor => ({ + name: vendor, + type: 'openai', + wireApi: 'completions', + baseUrl: handle.providerBaseUrl(vendor), + bearerToken: `${handle.nonce}.${sessionId}`, + })); + const models: ProviderModelConfig[] = byokModels.map(m => ({ + id: m.id, + provider: m.vendor, + ...(m.name !== undefined ? { name: m.name } : {}), + ...(m.maxContextWindowTokens !== undefined ? { maxContextWindowTokens: m.maxContextWindowTokens } : {}), + })); + this._logService.info(`[Copilot:${sessionId}] Wired ${models.length} BYOK model(s) across ${providers.length} provider(s) via loopback proxy ${handle.baseUrl}`); + return { providers, models }; + } + private async _buildSessionConfig(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise { const plugins = plan.snapshot.plugins; + // Synthesize BYOK provider/model config (empty when BYOK is gated off or the + // renderer reports no BYOK models). Merged into the returned config so both + // createSession and resumeSession advertise the models to the runtime. + const byok = await this._resolveByokSessionConfig(plan.sessionId); const enableCustomTerminalTool = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.EnableCustomTerminalTool) === true; let shellTools: Awaited> = []; if (enableCustomTerminalTool) { @@ -335,6 +394,7 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher { this._logService.trace(`[Copilot:${plan.sessionId}] System message config: ${JSON.stringify(systemMessage, (_key, value) => typeof value === 'function' ? '[transform fn]' : value)}`); } return { + ...byok, clientName: 'vscode', enableMcpApps: true, onPermissionRequest: request => runtime.handlePermissionRequest(request), diff --git a/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts b/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts index 732798c5d450d2..0912e644c04a67 100644 --- a/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts @@ -7,15 +7,18 @@ import assert from 'assert'; import { Event } from '../../../../base/common/event.js'; import type { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import type { IAgentHostByokLmHandler, IByokLmChatRequest, IByokLmChatResult } from '../../common/agentHostByokLm.js'; +import type { IAgentHostByokLmHandler, IByokLmChatRequest, IByokLmChatResult, IByokLmModelInfo } from '../../common/agentHostByokLm.js'; import { AgentHostClientByokLmChannel, createAgentHostClientByokLmConnection } from '../../common/agentHostClientByokLmChannel.js'; suite('agentHostClientByokLmChannel', () => { ensureNoDisposablesAreLeakedInTestSuite(); - function handlerOf(impl: (request: IByokLmChatRequest) => Promise): IAgentHostByokLmHandler { - return { _serviceBrand: undefined, chat: (request) => impl(request) }; + function handlerOf( + chat: (request: IByokLmChatRequest) => Promise, + listModels: () => Promise = async () => [], + ): IAgentHostByokLmHandler { + return { _serviceBrand: undefined, chat: (request) => chat(request), listModels: () => listModels() }; } /** @@ -56,6 +59,12 @@ suite('agentHostClientByokLmChannel', () => { assert.strictEqual(result.error, 'no model'); }); + test('round-trips listModels to the handler', async () => { + const models: IByokLmModelInfo[] = [{ vendor: 'acme', id: 'claude', name: 'Acme Claude', maxContextWindowTokens: 128000 }]; + const connection = bridge(handlerOf(async () => ({ content: '' }), async () => models)); + assert.deepStrictEqual(await connection.listModels(), models); + }); + test('rejects unknown channel commands', async () => { const server = new AgentHostClientByokLmChannel(handlerOf(async () => ({ content: '' }))); await assert.rejects(() => server.call(null, 'frobnicate'), /Unknown command/); diff --git a/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts b/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts index da608108d5cdf3..02201decead110 100644 --- a/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts +++ b/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts @@ -29,7 +29,7 @@ suite('ByokLmProxyService', () => { run: (handle: IByokLmProxyHandle) => Promise, ): Promise { const registry = new ByokLmBridgeRegistry(); - const registration = registry.register('client-1', { chat }); + const registration = registry.register('client-1', { chat, listModels: async () => [] }); const service = new ByokLmProxyService(new NullLogService(), registry); const handle = await service.start(); try { @@ -226,7 +226,7 @@ suite('ByokLmProxyService', () => { test('rebinds with a fresh nonce after every handle is disposed', async () => { const registry = new ByokLmBridgeRegistry(); - const registration = registry.register('client-1', { chat: async () => ({ content: 'ok' }) }); + const registration = registry.register('client-1', { chat: async () => ({ content: 'ok' }), listModels: async () => [] }); const service = new ByokLmProxyService(new NullLogService(), registry); const first = await service.start(); const firstNonce = first.nonce; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts index df95d758ba7c07..4a28fb1828fe14 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -10,6 +10,7 @@ import { IByokLmChatMessage, IByokLmChatRequest, IByokLmChatResult, + IByokLmModelInfo, IByokLmToolCall, } from '../../../../../../platform/agentHost/common/agentHostByokLm.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; @@ -90,6 +91,24 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok } } + async listModels(_token: CancellationToken): Promise { + const models: IByokLmModelInfo[] = []; + for (const identifier of this._languageModelsService.getLanguageModelIds()) { + const metadata = this._languageModelsService.lookupLanguageModel(identifier); + // Only genuine renderer BYOK models — exclude agent-host copies, which + // carry a `targetChatSessionType` and would otherwise re-enter the bridge. + if (metadata?.isBYOK && !metadata.targetChatSessionType) { + models.push({ + vendor: metadata.vendor, + id: metadata.id, + name: metadata.name, + maxContextWindowTokens: metadata.maxInputTokens, + }); + } + } + return models; + } + /** * Find the LM API identifier for a BYOK model addressed by its vendor and * provider-local id (the `provider/id` selection id the picker surfaced). diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts index a16a68bac67f79..9a27f6d5c61f87 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts @@ -84,6 +84,24 @@ suite('AgentHostByokLmHandler', () => { return store.add(new AgentHostByokLmHandler(service, new NullLogService())); } + test('listModels enumerates renderer BYOK models and excludes agent-host copies', async () => { + const service = new TestLanguageModelsService( + new Map([ + ['id-acme', byokModel('acme', 'claude')], + ['id-copy', { ...byokModel('acme', 'claude'), targetChatSessionType: 'copilotcli' }], + ['id-capi', { ...byokModel('copilot', 'gpt-4'), isBYOK: false }], + ]), + () => responseOf([]), + ); + const handler = createHandler(service); + + const models = await handler.listModels(CancellationToken.None); + + assert.deepStrictEqual(models, [ + { vendor: 'acme', id: 'claude', name: 'acme claude', maxContextWindowTokens: 1000 }, + ]); + }); + test('resolves the BYOK model and buffers text + tool calls', async () => { const service = new TestLanguageModelsService( new Map([['id-acme-claude', byokModel('acme', 'claude')]]), From 594a3e5d4818ba8a8c2c705c576c3af4203dc99e Mon Sep 17 00:00:00 2001 From: vritant24 Date: Tue, 23 Jun 2026 11:36:44 -0700 Subject: [PATCH 005/589] add tests for the launcher --- .../node/copilot/copilotSessionLauncher.ts | 109 ++++++++------ .../test/node/copilotSessionLauncher.test.ts | 141 ++++++++++++++++++ 2 files changed, 206 insertions(+), 44 deletions(-) create mode 100644 src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts diff --git a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts index 4ca27076e1ac7d..53cc4497310324 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts @@ -196,6 +196,61 @@ export function getCopilotContextTier(model: ModelSelection | undefined): Sessio return isContextTier(contextTier) ? contextTier : undefined; } +/** + * Resolve the BYOK provider/model session config for `sessionId` from the + * renderer's active bridge. Returns empty — the session launches without BYOK + * models — when BYOK is gated off (no active bridge), when the renderer reports + * no BYOK models, or when enumeration fails; `startProxy` is invoked only once + * at least one model is present. + * + * Each vendor maps to one `type: 'openai'` / `wireApi: 'completions'` provider + * whose `baseUrl` points at the proxy and authenticates with the session-scoped + * `Bearer .`; each model is surfaced under the + * provider-qualified selection id `vendor/id`, matching what the renderer's + * `AgentHostByokLmHandler` resolves. + * + * Extracted from {@link CopilotSessionLauncher} so the synthesis and gating are + * unit-testable without instantiating the launcher; the launcher passes a + * `startProxy` thunk that memoizes the single shared proxy handle. + */ +export async function resolveByokSessionConfig( + sessionId: string, + bridgeRegistry: IByokLmBridgeRegistry, + startProxy: () => Promise, + logService: ILogService, +): Promise<{ providers?: NamedProviderConfig[]; models?: ProviderModelConfig[] }> { + const connection = bridgeRegistry.getActive(); + if (!connection) { + return {}; + } + let byokModels: IByokLmModelInfo[]; + try { + byokModels = await connection.listModels(); + } catch (err) { + logService.warn(`[Copilot:${sessionId}] Failed to enumerate BYOK models from renderer bridge`, err); + return {}; + } + if (byokModels.length === 0) { + return {}; + } + const handle = await startProxy(); + const providers: NamedProviderConfig[] = [...new Set(byokModels.map(m => m.vendor))].map(vendor => ({ + name: vendor, + type: 'openai', + wireApi: 'completions', + baseUrl: handle.providerBaseUrl(vendor), + bearerToken: `${handle.nonce}.${sessionId}`, + })); + const models: ProviderModelConfig[] = byokModels.map(m => ({ + id: m.id, + provider: m.vendor, + ...(m.name !== undefined ? { name: m.name } : {}), + ...(m.maxContextWindowTokens !== undefined ? { maxContextWindowTokens: m.maxContextWindowTokens } : {}), + })); + logService.info(`[Copilot:${sessionId}] Wired ${models.length} BYOK model(s) across ${providers.length} provider(s) via loopback proxy ${handle.baseUrl}`); + return { providers, models }; +} + export class CopilotSessionLauncher implements ICopilotSessionLauncher { private _byokProxyHandle: Promise | undefined; @@ -306,51 +361,17 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher { } /** - * Synthesize SDK provider/model config for the renderer's BYOK models so the - * runtime issues their inference against the node-side loopback proxy (which - * bridges back to the renderer LM API). Returns empty when BYOK is gated off - * (no active bridge), the renderer reports no BYOK models, or enumeration - * fails — in each case the session simply launches without BYOK models. - * - * Each provider points at `proxy.providerBaseUrl(vendor)` and authenticates - * with the session-scoped `Bearer .`; each model surfaces - * under the provider-qualified selection id `vendor/id`, matching what the - * renderer's `AgentHostByokLmHandler` resolves. + * Launcher-bound wrapper over {@link resolveByokSessionConfig}: supplies the + * active bridge registry and a `startProxy` thunk that memoizes the single + * shared proxy handle for this launcher (started lazily on first use). */ - private async _resolveByokSessionConfig(sessionId: string): Promise<{ providers?: NamedProviderConfig[]; models?: ProviderModelConfig[] }> { - const connection = this._byokLmBridgeRegistry.getActive(); - if (!connection) { - return {}; - } - let byokModels: IByokLmModelInfo[]; - try { - byokModels = await connection.listModels(); - } catch (err) { - this._logService.warn(`[Copilot:${sessionId}] Failed to enumerate BYOK models from renderer bridge`, err); - return {}; - } - if (byokModels.length === 0) { - return {}; - } - if (!this._byokProxyHandle) { - this._byokProxyHandle = this._byokLmProxyService.start(); - } - const handle = await this._byokProxyHandle; - const providers: NamedProviderConfig[] = [...new Set(byokModels.map(m => m.vendor))].map(vendor => ({ - name: vendor, - type: 'openai', - wireApi: 'completions', - baseUrl: handle.providerBaseUrl(vendor), - bearerToken: `${handle.nonce}.${sessionId}`, - })); - const models: ProviderModelConfig[] = byokModels.map(m => ({ - id: m.id, - provider: m.vendor, - ...(m.name !== undefined ? { name: m.name } : {}), - ...(m.maxContextWindowTokens !== undefined ? { maxContextWindowTokens: m.maxContextWindowTokens } : {}), - })); - this._logService.info(`[Copilot:${sessionId}] Wired ${models.length} BYOK model(s) across ${providers.length} provider(s) via loopback proxy ${handle.baseUrl}`); - return { providers, models }; + private _resolveByokSessionConfig(sessionId: string): Promise<{ providers?: NamedProviderConfig[]; models?: ProviderModelConfig[] }> { + return resolveByokSessionConfig(sessionId, this._byokLmBridgeRegistry, () => { + if (!this._byokProxyHandle) { + this._byokProxyHandle = this._byokLmProxyService.start(); + } + return this._byokProxyHandle; + }, this._logService); } private async _buildSessionConfig(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise { diff --git a/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts b/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts new file mode 100644 index 00000000000000..d7005588d4ebe3 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../log/common/log.js'; +import type { IByokLmChatRequest, IByokLmChatResult, IByokLmModelInfo } from '../../common/agentHostByokLm.js'; +import { ByokLmBridgeRegistry } from '../../node/byokLmBridgeRegistry.js'; +import { ByokLmProxyService, type IByokLmProxyHandle } from '../../node/copilot/byokLmProxyService.js'; +import { resolveByokSessionConfig } from '../../node/copilot/copilotSessionLauncher.js'; + +/** + * Covers the BYOK provider/model synthesis the launcher feeds into + * `createSession` / `resumeSession`. The first four tests pin the gating and + * graceful-degradation branches plus the exact SDK config shape using a real + * {@link ByokLmBridgeRegistry} and a counting proxy thunk (no real proxy). The + * last test wires the synthesized config straight into a live + * {@link ByokLmProxyService} and POSTs at it, proving the launcher's output is + * consumable end-to-end: provider `baseUrl` + `Bearer .` + + * `model = id` route through the proxy to the renderer bridge. + */ +suite('resolveByokSessionConfig', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const sessionId = 'sess-1'; + const log = new NullLogService(); + + /** Minimal bridge connection: a scripted `listModels` and an unused `chat`. */ + function connectionOf(listModels: () => Promise) { + return { chat: async (): Promise => ({ content: '' }), listModels }; + } + + /** A fake proxy handle plus a `startProxy` thunk that records its call count. */ + function countingProxy() { + let starts = 0; + const handle: IByokLmProxyHandle = { + baseUrl: 'http://127.0.0.1:1', + nonce: 'NONCE', + providerBaseUrl: vendor => `http://127.0.0.1:1/v/${vendor}`, + dispose: () => { }, + }; + return { + get starts() { return starts; }, + startProxy: async () => { starts++; return handle; }, + }; + } + + test('returns empty and never starts the proxy when no bridge is active', async () => { + const registry = new ByokLmBridgeRegistry(); + const proxy = countingProxy(); + + const config = await resolveByokSessionConfig(sessionId, registry, proxy.startProxy, log); + + assert.deepStrictEqual(config, {}); + assert.strictEqual(proxy.starts, 0); + }); + + test('returns empty and never starts the proxy when the bridge reports no models', async () => { + const registry = new ByokLmBridgeRegistry(); + const registration = registry.register('client-1', connectionOf(async () => [])); + const proxy = countingProxy(); + + const config = await resolveByokSessionConfig(sessionId, registry, proxy.startProxy, log); + registration.dispose(); + + assert.deepStrictEqual(config, {}); + assert.strictEqual(proxy.starts, 0); + }); + + test('returns empty and never starts the proxy when enumeration fails', async () => { + const registry = new ByokLmBridgeRegistry(); + const registration = registry.register('client-1', connectionOf(async () => { throw new Error('renderer gone'); })); + const proxy = countingProxy(); + + const config = await resolveByokSessionConfig(sessionId, registry, proxy.startProxy, log); + registration.dispose(); + + assert.deepStrictEqual(config, {}); + assert.strictEqual(proxy.starts, 0); + }); + + test('synthesizes deduped providers and per-model config from the active bridge', async () => { + const registry = new ByokLmBridgeRegistry(); + const registration = registry.register('client-1', connectionOf(async () => [ + { vendor: 'acme', id: 'claude', name: 'Acme Claude', maxContextWindowTokens: 200000 }, + { vendor: 'acme', id: 'gpt', name: undefined, maxContextWindowTokens: undefined }, + { vendor: 'globex', id: 'llama', name: 'Globex Llama' }, + ])); + const proxy = countingProxy(); + + const config = await resolveByokSessionConfig(sessionId, registry, proxy.startProxy, log); + registration.dispose(); + + assert.strictEqual(proxy.starts, 1); + assert.deepStrictEqual(config, { + providers: [ + { name: 'acme', type: 'openai', wireApi: 'completions', baseUrl: 'http://127.0.0.1:1/v/acme', bearerToken: 'NONCE.sess-1' }, + { name: 'globex', type: 'openai', wireApi: 'completions', baseUrl: 'http://127.0.0.1:1/v/globex', bearerToken: 'NONCE.sess-1' }, + ], + models: [ + { id: 'claude', provider: 'acme', name: 'Acme Claude', maxContextWindowTokens: 200000 }, + { id: 'gpt', provider: 'acme' }, + { id: 'llama', provider: 'globex', name: 'Globex Llama' }, + ], + }); + }); + + test('synthesized provider config routes through a live proxy to the bridge', async () => { + const registry = new ByokLmBridgeRegistry(); + let captured: IByokLmChatRequest | undefined; + const registration = registry.register('client-1', { + chat: async (request) => { captured = request; return { content: 'hello from byok' }; }, + listModels: async () => [{ vendor: 'acme', id: 'claude' }], + }); + const service = new ByokLmProxyService(log, registry); + let handle: IByokLmProxyHandle | undefined; + + const config = await resolveByokSessionConfig(sessionId, registry, async () => (handle = await service.start()), log); + const provider = config.providers![0]; + const model = config.models![0]; + try { + const response = await fetch(`${provider.baseUrl}/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${provider.bearerToken}` }, + body: JSON.stringify({ model: model.id, messages: [{ role: 'user', content: 'hi' }] }), + }); + assert.strictEqual(response.status, 200); + const text = await response.text(); + assert.ok(text.includes('hello from byok'), `expected content in SSE: ${text}`); + } finally { + handle?.dispose(); + registration.dispose(); + service.dispose(); + } + assert.strictEqual(captured?.vendor, 'acme'); + assert.strictEqual(captured?.modelId, 'claude'); + }); +}); From fb1a6981b7a51a2033fb7c440cd1c6d5cd9814b7 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Wed, 24 Jun 2026 10:35:29 -0700 Subject: [PATCH 006/589] Implement BYOK model change events and registry updates in agent host --- .../agentHost/common/agentHostByokLm.ts | 13 ++ .../common/agentHostClientByokLmChannel.ts | 4 + .../agentHost/node/agentHostServerMain.ts | 2 + .../agentHost/node/byokLmBridgeRegistry.ts | 25 ++++ .../agentHost/node/copilot/copilotAgent.ts | 118 ++++++++++++++++-- .../node/agentHostClientReverseRpc.test.ts | 1 + .../agentHost/test/node/copilotAgent.test.ts | 8 +- .../agentHost/agentHostByokLmHandler.ts | 11 ++ .../agentHostByokLmHandler.test.ts | 3 + 9 files changed, 176 insertions(+), 9 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHostByokLm.ts b/src/vs/platform/agentHost/common/agentHostByokLm.ts index e8fb506dcc3e0b..95f8f9e31917e5 100644 --- a/src/vs/platform/agentHost/common/agentHostByokLm.ts +++ b/src/vs/platform/agentHost/common/agentHostByokLm.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../base/common/cancellation.js'; +import { Event } from '../../../base/common/event.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; /** @@ -101,6 +102,13 @@ export const IAgentHostByokLmHandler = createDecorator( export interface IAgentHostByokLmHandler { readonly _serviceBrand: undefined; + /** + * Fires when the renderer's set of BYOK models changes, so the node agent + * host can re-enumerate them for the model picker. Optional: test fakes may + * omit it. + */ + readonly onDidChangeModels?: Event; + /** * Run a BYOK chat completion against the extension-registered model that * matches `request.vendor` + `request.modelId`. Rejects (or resolves with @@ -123,4 +131,9 @@ export interface IAgentHostByokLmHandler { export interface IByokLmBridgeConnection { chat(request: IByokLmChatRequest): Promise; listModels(): Promise; + /** + * Fires when the renderer's set of BYOK models changes, so the agent host + * can re-enumerate. Optional: test fakes may omit it. + */ + readonly onDidChangeModels?: Event; } diff --git a/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts b/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts index e029f48af559bf..b2bc0a348a4583 100644 --- a/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts +++ b/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts @@ -36,6 +36,7 @@ export function createAgentHostClientByokLmConnection(channel: IChannel): IByokL return { chat: (request) => channel.call('chat', request) as Promise, listModels: () => channel.call('listModels') as Promise, + onDidChangeModels: channel.listen('onDidChangeModels'), }; } @@ -51,6 +52,9 @@ export class AgentHostClientByokLmChannel implements IServerChannel { ) { } listen(_ctx: unknown, event: string): Event { + if (event === 'onDidChangeModels') { + return (this._handler.onDidChangeModels ?? Event.None) as Event; + } throw new Error(`No event '${event}' on AgentHostClientByokLmChannel`); } diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index 6b26b821fbf482..cf0f007143f1ee 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -34,6 +34,7 @@ import { InstantiationService } from '../../instantiation/common/instantiationSe import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; import { registerAgentHostNetworkServices } from './agentHostBootstrap.js'; import { CopilotAgent } from './copilot/copilotAgent.js'; +import { ByokLmBridgeRegistry, IByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; import { CopilotBranchNameGenerator, ICopilotBranchNameGenerator } from './copilot/copilotBranchNameGenerator.js'; import { CopilotApiService, ICopilotApiService } from './shared/copilotApiService.js'; import { ClaudeAgent } from './claude/claudeAgent.js'; @@ -283,6 +284,7 @@ async function main(): Promise { diServices.set(ICodexProxyService, codexProxyService); const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService)); diServices.set(IAgentHostOTelService, agentHostOTelService); + diServices.set(IByokLmBridgeRegistry, new ByokLmBridgeRegistry()); const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent)); agentService.registerProvider(copilotAgent); log('CopilotAgent registered'); diff --git a/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts b/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts index 4bd15ffdc684f7..39e4b7c6cf88b9 100644 --- a/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts +++ b/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts @@ -29,6 +29,13 @@ export interface IByokLmBridgeRegistry { /** The most recently registered, still-live connection, if any. */ getActive(): IByokLmBridgeConnection | undefined; + + /** + * Subscribe to changes in the set of registered connections (a renderer + * connecting or disconnecting), so consumers can re-enumerate models. + * Disposing the result removes the listener. + */ + onDidChangeActive(listener: () => void): IDisposable; } export class ByokLmBridgeRegistry implements IByokLmBridgeRegistry { @@ -37,10 +44,27 @@ export class ByokLmBridgeRegistry implements IByokLmBridgeRegistry { private readonly _connections = new Map(); private _activeClientId: string | undefined; + private readonly _changeListeners = new Set<() => void>(); + + onDidChangeActive(listener: () => void): IDisposable { + this._changeListeners.add(listener); + return toDisposable(() => { + this._changeListeners.delete(listener); + }); + } + + private _notifyActiveChanged(): void { + // Snapshot first: a listener may unsubscribe (mutating the set) while it + // is being notified. + for (const listener of [...this._changeListeners]) { + listener(); + } + } register(clientId: string, connection: IByokLmBridgeConnection): IDisposable { this._connections.set(clientId, connection); this._activeClientId = clientId; + this._notifyActiveChanged(); return toDisposable(() => { if (this._connections.get(clientId) === connection) { this._connections.delete(clientId); @@ -49,6 +73,7 @@ export class ByokLmBridgeRegistry implements IByokLmBridgeRegistry { const next = this._connections.keys().next(); this._activeClientId = next.done ? undefined : next.value; } + this._notifyActiveChanged(); } }); } diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index a186cc08ccf685..4e2c94552ac9fd 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -48,6 +48,7 @@ import { IAgentConfigurationService } from '../agentConfigurationService.js'; import { IAgentHostCompletions } from '../agentHostCompletions.js'; import { IAgentHostGitService, META_DIFF_BASE_BRANCH } from '../../common/agentHostGitService.js'; import { findMcpChildId } from '../shared/mcpCustomizationController.js'; +import { IByokLmBridgeRegistry } from '../byokLmBridgeRegistry.js'; import { ICopilotBranchNameGenerator } from './copilotBranchNameGenerator.js'; import { CopilotAgentSession, type CopilotSdkMode } from './copilotAgentSession.js'; import { ICopilotSessionContext, projectFromCopilotContext } from './copilotGitProject.js'; @@ -275,6 +276,14 @@ export class CopilotAgent extends Disposable implements IAgent { readonly onMcpNotification = this._onMcpNotification.event; private readonly _models = observableValue(this, []); readonly models = this._models; + /** + * The two sources merged into {@link _models}: CAPI models from the CLI's + * `models.list` and BYOK models enumerated from the active renderer bridge. + * Tracked separately so each can refresh independently without clobbering + * the other; {@link _publishModels} concatenates them for the picker. + */ + private _capiModels: readonly IAgentModelInfo[] = []; + private _byokModels: readonly IAgentModelInfo[] = []; /** * Bounded exponential-backoff retry for {@link _refreshModels}. The SDK's @@ -289,6 +298,10 @@ export class CopilotAgent extends Disposable implements IAgent { protected readonly _modelRefreshMaxDelayMs: number = 30_000; /** Pending model-refresh retry timer; cleared on a fresh refresh, shutdown, or dispose. */ private readonly _modelRefreshRetry = this._register(new MutableDisposable()); + /** Pending BYOK-model enumeration retry timer; cleared on a fresh refresh, shutdown, or dispose. */ + private readonly _byokRefreshRetry = this._register(new MutableDisposable()); + /** Subscription to the active BYOK bridge's model-change event; re-bound when the active bridge changes. */ + private readonly _byokModelsChangeSub = this._register(new MutableDisposable()); private _client: CopilotClient | undefined; private _clientStarting: Promise | undefined; @@ -367,6 +380,7 @@ export class CopilotAgent extends Disposable implements IAgent { @ICopilotBranchNameGenerator private readonly _branchNameGenerator: ICopilotBranchNameGenerator, @IAgentHostCompletions completions: IAgentHostCompletions, @IAgentHostCheckpointService private readonly _checkpointService: IAgentHostCheckpointService, + @IByokLmBridgeRegistry private readonly _byokBridgeRegistry: IByokLmBridgeRegistry, ) { super(); this._plugins = this._register(this._instantiationService.createInstance(PluginController)); @@ -387,6 +401,16 @@ export class CopilotAgent extends Disposable implements IAgent { this._logService.error('[Copilot] Failed to restart client after config change', err) ); })); + + // Surface renderer BYOK models in the picker: re-enumerate them whenever + // a renderer bridge connects or disconnects. The registry is only + // populated when `chat.agentHost.byokModels.enabled` is on, so this stays + // a no-op (empty list) while the feature is off. + this._register(this._byokBridgeRegistry.onDidChangeActive(() => { + this._logService.info('[Copilot] BYOK bridge changed; refreshing models'); + this._subscribeByokModelChanges(); + void this._refreshByokModels(); + })); } private _lastSessionSyncEnabled: boolean = this._isSessionSyncEnabled(); @@ -526,13 +550,15 @@ export class CopilotAgent extends Disposable implements IAgent { const tokenAtRefreshStart = this._githubToken; if (!tokenAtRefreshStart) { - this._models.set([], undefined); + this._capiModels = []; + this._publishModels(); return; } try { const models = await this._listModels(tokenAtRefreshStart); if (this._githubToken === tokenAtRefreshStart) { - this._models.set(models, undefined); + this._capiModels = models; + this._publishModels(); } } catch (err) { // Token rotated mid-flight — a newer refresh owns the result — or @@ -549,13 +575,90 @@ export class CopilotAgent extends Disposable implements IAgent { }, delay); return; } - // Retries exhausted: surface the error. Only blank the list when we - // have nothing to show, so a transient failure never wipes a - // previously loaded, good model list. + // Retries exhausted: surface the error but keep the last-known CAPI + // list so a transient failure never wipes a previously loaded, good + // model list. Republish so a concurrently-updated BYOK list still + // shows through. this._logService.error(err, '[Copilot] Failed to refresh models'); - if (this._models.get().length === 0) { - this._models.set([], undefined); + this._publishModels(); + } + } + + /** + * Re-emit the merged CAPI + BYOK model list to the picker. A fresh array is + * allocated each call so the observable always notifies its consumers. + */ + private _publishModels(): void { + this._models.set([...this._capiModels, ...this._byokModels], undefined); + } + + /** + * (Re)subscribe to the active BYOK bridge's `onDidChangeModels` so the + * picker re-enumerates whenever the renderer's BYOK models change — they + * often register shortly after the bridge connects. Cleared when no bridge + * is active. + */ + private _subscribeByokModelChanges(): void { + const connection = this._byokBridgeRegistry.getActive(); + this._byokModelsChangeSub.value = connection?.onDidChangeModels?.(() => { + this._logService.trace('[Copilot] BYOK models changed; refreshing'); + void this._refreshByokModels(); + }); + } + + /** + * Re-enumerate the renderer's BYOK models from the active bridge and + * republish the merged list. Triggered when a renderer bridge connects or + * disconnects. + * + * Right after a bridge connects the renderer's BYOK channel handler may not + * be registered yet, so the `listModels` RPC times out. We retry with + * bounded backoff (reusing the CAPI refresh schedule) so the models surface + * once the renderer side is ready. No-op once shutdown has begun or the + * bridge has gone away. + * + * Each model is surfaced under the provider-qualified id `vendor/id` so a + * selection round-trips to the per-session provider config synthesized by + * `resolveByokSessionConfig`. + */ + private async _refreshByokModels(attempt = 0): Promise { + // A fresh refresh (e.g. a bridge reconnect) supersedes any scheduled retry. + this._byokRefreshRetry.clear(); + if (this._shutdownPromise) { + return; + } + const connection = this._byokBridgeRegistry.getActive(); + if (!connection) { + this._logService.trace('[Copilot] BYOK refresh: no active bridge connection'); + this._byokModels = []; + this._publishModels(); + return; + } + try { + const models = await connection.listModels(); + this._byokModels = models.map((m): IAgentModelInfo => ({ + provider: this.id, + id: `${m.vendor}/${m.id}`, + name: (m.name ?? m.id) + ' [BYOK]', + maxContextWindow: m.maxContextWindowTokens, + supportsVision: false, + })); + this._logService.trace(`[Copilot] Found ${this._byokModels.length} BYOK models${this._byokModels.length ? ': ' + this._byokModels.map(m => m.name).join(', ') : ''}`); + this._publishModels(); + } catch (err) { + // The bridge went away mid-flight, or teardown began — drop the result. + if (this._shutdownPromise || this._byokBridgeRegistry.getActive() !== connection) { + return; + } + if (attempt + 1 < this._modelRefreshMaxAttempts) { + const delay = this._modelRefreshBackoff(attempt); + this._logService.warn(`[Copilot] Failed to enumerate BYOK models (attempt ${attempt + 1}), retrying in ${delay}ms`, err); + this._byokRefreshRetry.value = disposableTimeout(() => { + void this._refreshByokModels(attempt + 1); + }, delay); + return; } + this._logService.warn('[Copilot] Failed to enumerate BYOK models from renderer bridge', err); } } @@ -1924,6 +2027,7 @@ export class CopilotAgent extends Disposable implements IAgent { // Cancel any pending model-refresh retry so its timer cannot fire // after teardown and resurrect the client. this._modelRefreshRetry.clear(); + this._byokRefreshRetry.clear(); this._logService.info('[Copilot] Shutting down...'); const sessionIds = new Set([...this._sessions.keys(), ...this._createdWorktrees.keys()]); for (const sessionId of sessionIds) { diff --git a/src/vs/platform/agentHost/test/node/agentHostClientReverseRpc.test.ts b/src/vs/platform/agentHost/test/node/agentHostClientReverseRpc.test.ts index 2e736a8bcb129a..4b94762174d079 100644 --- a/src/vs/platform/agentHost/test/node/agentHostClientReverseRpc.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostClientReverseRpc.test.ts @@ -37,6 +37,7 @@ suite('registerAgentHostClientReverseRpc', () => { register: (clientId: string, _connection: IByokLmBridgeConnection): IDisposable => { registered.push(clientId); return toDisposable(() => { }); }, get: () => undefined, getActive: () => undefined, + onDidChangeActive: () => toDisposable(() => { }), }, }; } diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index 108989120593c7..5e0fbe6adcdb79 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -51,6 +51,7 @@ import { ShellManager } from '../../node/copilot/copilotShellTools.js'; import { SessionDatabase } from '../../node/sessionDatabase.js'; import { createNullSessionDataService } from '../common/sessionTestHelpers.js'; import { ActiveClientState } from '../../node/activeClientState.js'; +import { ByokLmBridgeRegistry, IByokLmBridgeRegistry } from '../../node/byokLmBridgeRegistry.js'; import { ICopilotApiService, type ICopilotApiServiceRequestOptions, type ICopilotUtilityChatCompletionRequest } from '../../node/shared/copilotApiService.js'; class TestAgentPluginManager implements IAgentPluginManager { @@ -341,8 +342,9 @@ class ResumePathCopilotAgent extends CopilotAgent { @IAgentConfigurationService configurationService: IAgentConfigurationService, @ICopilotBranchNameGenerator branchNameGenerator: ICopilotBranchNameGenerator, @IAgentHostCompletions completions: IAgentHostCompletions, + @IByokLmBridgeRegistry byokBridgeRegistry: IByokLmBridgeRegistry, ) { - super(logService, instantiationService, sessionDataService, gitService, configurationService, new MockAgentHostOTelService(), branchNameGenerator, completions, NULL_CHECKPOINT_SERVICE); + super(logService, instantiationService, sessionDataService, gitService, configurationService, new MockAgentHostOTelService(), branchNameGenerator, completions, NULL_CHECKPOINT_SERVICE, byokBridgeRegistry); this._enablePlanModeOnClient(this._copilotClient as CopilotClient); } @@ -368,8 +370,9 @@ class TestableCopilotAgent extends CopilotAgent { @IAgentConfigurationService configurationService: IAgentConfigurationService, @ICopilotBranchNameGenerator branchNameGenerator: ICopilotBranchNameGenerator, @IAgentHostCompletions completions: IAgentHostCompletions, + @IByokLmBridgeRegistry byokBridgeRegistry: IByokLmBridgeRegistry, ) { - super(logService, instantiationService, sessionDataService, gitService, configurationService, new MockAgentHostOTelService(), branchNameGenerator, completions, NULL_CHECKPOINT_SERVICE); + super(logService, instantiationService, sessionDataService, gitService, configurationService, new MockAgentHostOTelService(), branchNameGenerator, completions, NULL_CHECKPOINT_SERVICE, byokBridgeRegistry); this._enablePlanModeOnClient(this._copilotClient as CopilotClient); } @@ -439,6 +442,7 @@ function createTestAgentContext(disposables: Pick, optio flush: async () => undefined, }); services.set(IAgentHostCompletions, disposables.add(new AgentHostCompletions(logService))); + services.set(IByokLmBridgeRegistry, new ByokLmBridgeRegistry()); const copilotApiService = options?.copilotApiService ?? new TestCopilotApiService(); services.set(ICopilotApiService, copilotApiService); services.set(ICopilotBranchNameGenerator, new CopilotBranchNameGenerator(copilotApiService, logService)); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts index 4a28fb1828fe14..3e9e232221ef3b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { IAgentHostByokLmHandler, @@ -34,11 +35,21 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok declare readonly _serviceBrand: undefined; + private readonly _onDidChangeModels = this._register(new Emitter()); + /** Fires when the renderer's BYOK models change, so the node agent host re-enumerates. */ + readonly onDidChangeModels = this._onDidChangeModels.event; + constructor( @ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService, @ILogService private readonly _logService: ILogService, ) { super(); + // Re-emit (debounced) whenever the renderer's language models change, so the + // agent host can refresh its BYOK model list — extension-provided BYOK models + // often register shortly after the bridge connects. + this._register(Event.debounce(this._languageModelsService.onDidChangeLanguageModels, () => undefined, 500)(() => { + this._onDidChangeModels.fire(); + })); } async chat(request: IByokLmChatRequest, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts index 9a27f6d5c61f87..2be6d4144a4c3f 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../../base/common/event.js'; import { mock } from '../../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; @@ -29,6 +30,8 @@ class TestLanguageModelsService extends mock() { captured: ICapturedRequest | undefined; + override readonly onDidChangeLanguageModels = Event.None; + constructor( private readonly _models: ReadonlyMap, private readonly _respond: (request: ICapturedRequest) => ILanguageModelChatResponse, From 592a35e81fba7b56c04b5685c46fd27d25ecd7f5 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 25 Jun 2026 10:00:19 -0700 Subject: [PATCH 007/589] fix test --- .../agentHost/common/agentHostClientByokLmChannel.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts b/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts index b2bc0a348a4583..8d66db0443be71 100644 --- a/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts +++ b/src/vs/platform/agentHost/common/agentHostClientByokLmChannel.ts @@ -5,6 +5,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Event } from '../../../base/common/event.js'; +import { Lazy } from '../../../base/common/lazy.js'; import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IAgentHostByokLmHandler, @@ -33,10 +34,16 @@ export const AGENT_HOST_CLIENT_BYOK_LM_CHANNEL = 'agentHostClientByokLm'; * the buffered completion the renderer produced from the LM API. */ export function createAgentHostClientByokLmConnection(channel: IChannel): IByokLmBridgeConnection { + // Reach for `channel.listen` lazily — only when a consumer actually + // subscribes to `onDidChangeModels` — mirroring the deferred `channel.call` + // usage below (and the reverse FS bridge). Touching the channel eagerly at + // construction would force every connection to register an IPC event handler + // up front, even when nothing listens. + const onDidChangeModels = new Lazy(() => channel.listen('onDidChangeModels')); return { chat: (request) => channel.call('chat', request) as Promise, listModels: () => channel.call('listModels') as Promise, - onDidChangeModels: channel.listen('onDidChangeModels'), + onDidChangeModels: (listener, thisArgs, disposables) => onDidChangeModels.value(listener, thisArgs, disposables), }; } From f9a4d252adc6e78628ae2cf07f8f2ef8f554cca2 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 25 Jun 2026 11:03:20 -0700 Subject: [PATCH 008/589] address comments --- .../platform/agentHost/common/agentHostByokLm.ts | 2 +- src/vs/platform/agentHost/common/agentService.ts | 12 +++++++----- src/vs/platform/agentHost/node/agentHostMain.ts | 10 ++++++---- .../agentHost/node/copilot/byokLmProxyService.ts | 12 ++++++++++-- .../node/copilot/byokOpenAiTranslation.ts | 9 ++++++++- .../agentHost/node/copilot/copilotAgent.ts | 2 +- .../test/node/byokLmProxyService.test.ts | 16 ++++++++++++++++ .../test/node/byokOpenAiTranslation.test.ts | 7 +++++++ .../agentHost/agentHostByokLmHandler.ts | 2 +- .../agentSessions/agentHostByokLmHandler.test.ts | 2 +- 10 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHostByokLm.ts b/src/vs/platform/agentHost/common/agentHostByokLm.ts index 95f8f9e31917e5..e4eb94ad8100bd 100644 --- a/src/vs/platform/agentHost/common/agentHostByokLm.ts +++ b/src/vs/platform/agentHost/common/agentHostByokLm.ts @@ -87,7 +87,7 @@ export interface IByokLmModelInfo { readonly id: string; /** Display name, when the provider supplies one. */ readonly name?: string; - /** Maximum context window tokens, when known. */ + /** Maximum context window tokens (prompt + output), when known. */ readonly maxContextWindowTokens?: number; } diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index a9efd062974a50..82e6f5182407bc 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -86,12 +86,14 @@ export const AgentHostClaudeAgentEnabledSettingId = 'chat.agentHost.claudeAgent. export const AgentHostCodexAgentEnabledSettingId = 'chat.agentHost.codexAgent.enabled'; /** - * Configuration key controlling whether the agent host wires up the BYOK + * Configuration key controlling whether the agent host *wires up* the BYOK * ("bring your own key") language-model bridge: the renderer LM handler, the - * reverse-RPC channel, and the node-side OpenAI proxy + bridge registry. When - * `false` (the default), none of the BYOK additions are registered on either - * side, so extension-provided BYOK models are never reachable from agent-host - * sessions. The agent host process must be restarted for changes to take effect. + * reverse-RPC channel, and the per-connection link to the node-side OpenAI + * proxy + bridge registry. When `false` (the default), the proxy and registry + * are still constructed but stay inert — the renderer's BYOK server channel and + * the per-connection bridge are not wired, so the registry stays empty and + * extension-provided BYOK models are never reachable from agent-host sessions. + * The agent host process must be restarted for changes to take effect. */ export const AgentHostByokModelsEnabledSettingId = 'chat.agentHost.byokModels.enabled'; diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index fec21d60457b83..a9ed2bc46491d1 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -133,10 +133,12 @@ async function startAgentHost(): Promise { // Create the real service implementation that lives in this process let agentService: AgentService; let instantiationService: IInstantiationService; - // Gate all BYOK agent-host additions (proxy, bridge registry, reverse-RPC - // bridge) behind the opt-in `chat.agentHost.byokModels.enabled` setting, - // forwarded from the renderer as an env var. When off, none of the BYOK - // wiring is created here and the renderer skips the BYOK server channel. + // Gate BYOK *use* behind the opt-in `chat.agentHost.byokModels.enabled` + // setting, forwarded from the renderer as an env var. The proxy and bridge + // registry are always constructed below (so the session launcher can inject + // them), but when off they stay inert: the per-connection bridge and the + // renderer's BYOK server channel are not wired, so the registry stays empty + // and the proxy never binds. const byokLmEnabled = isAgentEnabled(process.env[AgentHostByokModelsEnabledEnvVar], false); try { // Build the DI container early so the git service can be created via diff --git a/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts b/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts index a2a2e3e05d81b3..86a27f5e00a005 100644 --- a/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts +++ b/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts @@ -167,14 +167,22 @@ export class ByokLmProxyService extends LoopbackProxyServer im return undefined; } const vendorSegment = pathname.slice(VENDOR_PATH_PREFIX.length, pathname.length - CHAT_COMPLETIONS_SUFFIX.length); - if (!vendorSegment || vendorSegment.includes('/')) { + if (!vendorSegment) { return undefined; } + let vendor: string; try { - return decodeURIComponent(vendorSegment); + vendor = decodeURIComponent(vendorSegment); } catch { return undefined; } + // Re-check for a path separator *after* decoding: a `%2F` survives the + // pre-decode prefix/suffix checks but would decode into a second path + // segment, breaking the single-segment `vendor/id` selection-id convention. + if (!vendor || vendor.includes('/')) { + return undefined; + } + return vendor; } private async _handleChatCompletions(req: http.IncomingMessage, res: http.ServerResponse, runtime: ILoopbackProxyRuntime, vendor: string): Promise { diff --git a/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts b/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts index 39457177d6a130..d5deb2786ea32f 100644 --- a/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts +++ b/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts @@ -104,9 +104,16 @@ function toBridgeToolCalls(toolCalls: IOpenAiToolCall[] | undefined): IByokLmToo const mapped: IByokLmToolCall[] = []; for (let i = 0; i < toolCalls.length; i++) { const call = toolCalls[i]; + const name = call.function?.name; + if (!name) { + // A tool call without a function name is malformed: reject at the + // boundary (→ 400) rather than forwarding an invalid `tool_use` part + // that would fail later, deeper in the renderer. + throw new OpenAiTranslationError(`tool_calls[${i}].function.name is required`); + } mapped.push({ id: call.id ?? `call_${i}`, - name: call.function?.name ?? '', + name, argumentsJson: call.function?.arguments ?? '{}', }); } diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index bb1423b5431c4a..2eb9a29f187f77 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -697,7 +697,7 @@ export class CopilotAgent extends Disposable implements IAgent { this._byokModels = models.map((m): IAgentModelInfo => ({ provider: this.id, id: `${m.vendor}/${m.id}`, - name: (m.name ?? m.id) + ' [BYOK]', + name: m.name ?? m.id, maxContextWindow: m.maxContextWindowTokens, supportsVision: false, })); diff --git a/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts b/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts index 02201decead110..0c4e52e43dcf16 100644 --- a/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts +++ b/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts @@ -144,6 +144,22 @@ suite('ByokLmProxyService', () => { assert.strictEqual(captured?.vendor, 'acme corp'); }); + test('rejects a vendor that decodes to a multi-segment path (%2F)', async () => { + await withProxy( + async () => ({ content: 'unused' }), + async (handle) => { + // `encodeURIComponent('a/b')` → `a%2Fb`, which survives the + // pre-decode segment check but decodes back into `a/b`. + const response = await fetch(chatUrl(handle, 'a/b'), { + method: 'POST', + headers: authHeaders(handle), + body: JSON.stringify({ model: 'm', messages: [] }), + }); + assert.strictEqual(response.status, 404); + }, + ); + }); + test('streams assistant tool calls as OpenAI tool_call deltas', async () => { await withProxy( async () => ({ content: '', toolCalls: [{ id: 'call_1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }] }), diff --git a/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts b/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts index f60b8acdc09e83..299a27183bd419 100644 --- a/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts +++ b/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts @@ -57,6 +57,13 @@ suite('byokOpenAiTranslation', () => { assert.throws(() => openAiRequestToBridge('acme', { messages: [] }), OpenAiTranslationError); }); + test('throws when an assistant tool call is missing its function name', () => { + assert.throws(() => openAiRequestToBridge('acme', { + model: 'm', + messages: [{ role: 'assistant', content: '', tool_calls: [{ id: 'call_1', type: 'function', function: { arguments: '{}' } }] }], + }), OpenAiTranslationError); + }); + test('omits tools and options when absent', () => { const result = openAiRequestToBridge('acme', { model: 'm', messages: [{ role: 'user', content: 'hello' }] }); assert.strictEqual(result.tools, undefined); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts index 3e9e232221ef3b..8cb355a255506d 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -113,7 +113,7 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok vendor: metadata.vendor, id: metadata.id, name: metadata.name, - maxContextWindowTokens: metadata.maxInputTokens, + maxContextWindowTokens: metadata.maxInputTokens + metadata.maxOutputTokens, }); } } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts index 2be6d4144a4c3f..8ae9556706d3b5 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts @@ -101,7 +101,7 @@ suite('AgentHostByokLmHandler', () => { const models = await handler.listModels(CancellationToken.None); assert.deepStrictEqual(models, [ - { vendor: 'acme', id: 'claude', name: 'acme claude', maxContextWindowTokens: 1000 }, + { vendor: 'acme', id: 'claude', name: 'acme claude', maxContextWindowTokens: 2000 }, ]); }); From f197291ea19b7f7761a7129b995ab3cb8e5b0fcf Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 25 Jun 2026 13:51:19 -0700 Subject: [PATCH 009/589] fix tool call propagation --- .../agentHost/agentHostByokLmHandler.ts | 22 ++++++++++-------- .../agentHostByokLmHandler.test.ts | 23 ++++++++++++++++--- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts index 8cb355a255506d..0ee85fe9da3839 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -135,6 +135,18 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok } private _toChatMessage(message: IByokLmChatMessage): IChatMessage { + // A tool-result message carries its payload solely in the `tool_result` + // part — the renderer/extension turns that into a wire `role: 'tool'` + // message on its own. Emit it and return early so the shared text branch + // below doesn't also inject a duplicate `role: 'user'` copy of the output. + // Tool messages that lack a `toolCallId` fall through to the plain text branch. + if (message.role === 'tool' && message.toolCallId) { + return { + role: ChatMessageRole.User, + content: [{ type: 'tool_result', toolCallId: message.toolCallId, value: [{ type: 'text', value: message.content }] }], + }; + } + const content: IChatMessagePart[] = []; if (message.content) { content.push({ type: 'text', value: message.content }); @@ -151,16 +163,6 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok } } - if (message.role === 'tool' && message.toolCallId) { - content.push({ - type: 'tool_result', - toolCallId: message.toolCallId, - value: [{ type: 'text', value: message.content }], - }); - // Tool results ride on a user-role message in the LM API. - return { role: ChatMessageRole.User, content }; - } - return { role: this._toChatRole(message.role), content }; } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts index 8ae9556706d3b5..37bda350234248 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostByokLmHandler.test.ts @@ -153,9 +153,26 @@ suite('AgentHostByokLmHandler', () => { { role: ChatMessageRole.System, content: [{ type: 'text', value: 'be helpful' }] }, { role: ChatMessageRole.User, content: [{ type: 'text', value: 'hi' }] }, { role: ChatMessageRole.Assistant, content: [{ type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }] }, - // A `tool` message rides on a User-role message; its text is emitted both as a - // leading text part (shared `if (message.content)` branch) and inside the tool_result. - { role: ChatMessageRole.User, content: [{ type: 'text', value: 'sunny' }, { type: 'tool_result', toolCallId: 't1', value: [{ type: 'text', value: 'sunny' }] }] }, + // A `tool` message (with a toolCallId) rides on a User-role message and carries its + // payload solely in the tool_result part — no duplicate leading text part. + { role: ChatMessageRole.User, content: [{ type: 'tool_result', toolCallId: 't1', value: [{ type: 'text', value: 'sunny' }] }] }, + ]); + }); + + test('maps a tool message without a toolCallId to a plain user text part', async () => { + const service = new TestLanguageModelsService( + new Map([['id', byokModel('acme', 'claude')]]), + () => responseOf([{ type: 'text', value: 'ok' }]), + ); + const handler = createHandler(service); + + await handler.chat( + { vendor: 'acme', modelId: 'claude', messages: [{ role: 'tool', content: 'orphaned tool output' }] }, + CancellationToken.None, + ); + + assert.deepStrictEqual(service.captured?.messages, [ + { role: ChatMessageRole.User, content: [{ type: 'text', value: 'orphaned tool output' }] }, ]); }); From b5df4cb3feb5224b851a6c97a4a3a4de7d1f5a92 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 25 Jun 2026 13:51:53 -0700 Subject: [PATCH 010/589] address lifecycle comment --- src/vs/platform/agentHost/node/agentHostMain.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index a9ed2bc46491d1..7849c4857a515d 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -133,6 +133,7 @@ async function startAgentHost(): Promise { // Create the real service implementation that lives in this process let agentService: AgentService; let instantiationService: IInstantiationService; + let byokLmBridgeRegistry: ByokLmBridgeRegistry; // Gate BYOK *use* behind the opt-in `chat.agentHost.byokModels.enabled` // setting, forwarded from the renderer as an env var. The proxy and bridge // registry are always constructed below (so the session launcher can inject @@ -186,7 +187,7 @@ async function startAgentHost(): Promise { // per-connection bridge below (and the renderer's server channel) are only // wired when `chat.agentHost.byokModels.enabled` is on, so the registry // stays empty and the proxy never binds when the feature is off. - const byokLmBridgeRegistry = new ByokLmBridgeRegistry(); + byokLmBridgeRegistry = new ByokLmBridgeRegistry(); diServices.set(IByokLmBridgeRegistry, byokLmBridgeRegistry); const byokLmProxyService = disposables.add(instantiationService.createInstance(ByokLmProxyService)); diServices.set(IByokLmProxyService, byokLmProxyService); @@ -236,7 +237,6 @@ async function startAgentHost(): Promise { // in-process renderer-to-utility-process MessagePort transport). const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider()); disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, clientFileSystemProvider)); - const byokLmBridgeRegistry = instantiationService.invokeFunction(accessor => accessor.get(IByokLmBridgeRegistry)); // Wire reverse-RPC for in-process renderer connections. The renderer's // `MessagePortClient` ctx is its `clientId`, and it exposes the From ff1db6ebc560614535bed54e21a291364863a31e Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 25 Jun 2026 14:40:06 -0700 Subject: [PATCH 011/589] Enhance BYOK proxy handle management in CopilotAgent and CopilotSessionLauncher - Implemented safe disposal of BYOK proxy handle in CopilotAgent after client shutdown. - Added memoization and lifecycle management for BYOK proxy handle in CopilotSessionLauncher. - Introduced tests to validate the shared proxy handle behavior and ensure proper disposal. --- .../agentHost/node/copilot/copilotAgent.ts | 7 ++ .../node/copilot/copilotSessionLauncher.ts | 32 +++++++ .../test/node/copilotSessionLauncher.test.ts | 85 ++++++++++++++++++- 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 2eb9a29f187f77..d87b99ace7d62c 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -738,6 +738,10 @@ export class CopilotAgent extends Disposable implements IAgent { this._client = undefined; this._clientStarting = undefined; await client?.stop(); + // The runtime subprocess is now dead, so it is safe to release the BYOK + // proxy handle: the next session launch mints a fresh nonce. See the + // ownership invariant on `CopilotSessionLauncher.disposeByokProxyHandle`. + await this._sessionLauncher.disposeByokProxyHandle(); } /** @@ -2238,6 +2242,9 @@ export class CopilotAgent extends Disposable implements IAgent { } await this._client?.stop(); this._client = undefined; + // Release the BYOK proxy handle only after the runtime subprocess is + // gone, mirroring `_stopClient` and the proxy ownership invariant. + await this._sessionLauncher.disposeByokProxyHandle(); })(); return this._shutdownPromise; } diff --git a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts index 929e07d3cd141b..4931cf5cadd59d 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts @@ -288,6 +288,14 @@ export async function resolveByokSessionConfig( export class CopilotSessionLauncher implements ICopilotSessionLauncher { + /** + * Memoized handle for the single shared BYOK loopback proxy, started lazily + * on the first session launch that surfaces BYOK models (see + * {@link _resolveByokSessionConfig}). Held as a promise so concurrent + * launches share one bind. Released and cleared by + * {@link disposeByokProxyHandle} when the owning Copilot client/runtime is + * stopped, so the next start mints a fresh nonce. + */ private _byokProxyHandle: Promise | undefined; constructor( @@ -411,6 +419,30 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher { }, this._logService); } + /** + * Release the memoized BYOK loopback proxy handle (if any) and clear it so + * the next session launch mints a fresh nonce. Idempotent. + * + * **Ownership invariant.** The caller MUST stop the Copilot client/runtime + * subprocess before invoking this: disposing the handle drops the proxy's + * refcount and may rebind it on a different port/nonce, so a still-running + * subprocess would silently lose its endpoint — see {@link IByokLmProxyHandle}. + * Invoked from `CopilotAgent._stopClient` / `CopilotAgent.shutdown` after the + * client has stopped. + */ + async disposeByokProxyHandle(): Promise { + const handle = this._byokProxyHandle; + this._byokProxyHandle = undefined; + if (!handle) { + return; + } + try { + (await handle).dispose(); + } catch { + // The lazy `start()` rejected; there is nothing to release. + } + } + private async _buildSessionConfig(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise { const plugins = plan.snapshot.plugins; // Synthesize BYOK provider/model config (empty when BYOK is gated off or the diff --git a/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts b/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts index d7005588d4ebe3..5e35c67f22e0a8 100644 --- a/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { NullLogService } from '../../../log/common/log.js'; +import { InstantiationService } from '../../../instantiation/common/instantiationService.js'; +import { ServiceCollection } from '../../../instantiation/common/serviceCollection.js'; +import { ILogService, NullLogService } from '../../../log/common/log.js'; import type { IByokLmChatRequest, IByokLmChatResult, IByokLmModelInfo } from '../../common/agentHostByokLm.js'; -import { ByokLmBridgeRegistry } from '../../node/byokLmBridgeRegistry.js'; -import { ByokLmProxyService, type IByokLmProxyHandle } from '../../node/copilot/byokLmProxyService.js'; -import { resolveByokSessionConfig } from '../../node/copilot/copilotSessionLauncher.js'; +import { ByokLmBridgeRegistry, IByokLmBridgeRegistry } from '../../node/byokLmBridgeRegistry.js'; +import { ByokLmProxyService, IByokLmProxyService, type IByokLmProxyHandle } from '../../node/copilot/byokLmProxyService.js'; +import { CopilotSessionLauncher, resolveByokSessionConfig } from '../../node/copilot/copilotSessionLauncher.js'; /** * Covers the BYOK provider/model synthesis the launcher feeds into @@ -139,3 +142,77 @@ suite('resolveByokSessionConfig', () => { assert.strictEqual(captured?.modelId, 'claude'); }); }); + +/** + * Covers the launcher's lazy memoization and disposal of the shared BYOK proxy + * handle: concurrent launches share one bind, and + * {@link CopilotSessionLauncher.disposeByokProxyHandle} (called by the agent + * after the runtime subprocess stops) releases it so the next launch mints a + * fresh nonce. + */ +suite('CopilotSessionLauncher BYOK proxy lifecycle', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const sessionId = 'sess-1'; + + /** Minimal bridge connection: a scripted `listModels` and an unused `chat`. */ + function connectionOf(listModels: () => Promise) { + return { chat: async (): Promise => ({ content: '' }), listModels }; + } + + /** A fake proxy service whose handles carry a unique nonce per `start()`. */ + function fakeProxyService() { + let starts = 0; + let disposes = 0; + const service: IByokLmProxyService = { + _serviceBrand: undefined, + start: async (): Promise => { + const nonce = `NONCE-${++starts}`; + return { + baseUrl: 'http://127.0.0.1:1', + nonce, + providerBaseUrl: vendor => `http://127.0.0.1:1/v/${vendor}`, + dispose: () => { disposes++; }, + }; + }, + dispose: () => { }, + }; + return { service, get starts() { return starts; }, get disposes() { return disposes; } }; + } + + function createLauncher(store: DisposableStore, proxy: IByokLmProxyService, registry: IByokLmBridgeRegistry): CopilotSessionLauncher { + const services = new ServiceCollection(); + services.set(ILogService, new NullLogService()); + services.set(IByokLmProxyService, proxy); + services.set(IByokLmBridgeRegistry, registry); + // The launcher's other dependencies are unused by the BYOK path and + // resolve to `undefined` under the non-strict InstantiationService. + const instantiationService = store.add(new InstantiationService(services)); + return instantiationService.createInstance(CopilotSessionLauncher); + } + + test('memoizes the handle, and disposeByokProxyHandle releases it so the next launch mints a fresh nonce', async () => { + const store = new DisposableStore(); + const proxy = fakeProxyService(); + const registry = new ByokLmBridgeRegistry(); + store.add(registry.register('client-1', connectionOf(async () => [{ vendor: 'acme', id: 'claude' }]))); + const launcher = createLauncher(store, proxy.service, registry); + const resolve = () => (launcher as unknown as { _resolveByokSessionConfig(id: string): Promise<{ providers?: { bearerToken: string }[] }> })._resolveByokSessionConfig(sessionId); + + const first = await resolve(); + const second = await resolve(); + assert.strictEqual(proxy.starts, 1, 'subsequent launches share the memoized bind'); + assert.strictEqual(first.providers![0].bearerToken, second.providers![0].bearerToken, 'the shared bind reuses one nonce'); + + await launcher.disposeByokProxyHandle(); + await launcher.disposeByokProxyHandle(); + assert.strictEqual(proxy.disposes, 1, 'the handle is released exactly once and disposal is idempotent'); + + const third = await resolve(); + assert.strictEqual(proxy.starts, 2, 'a fresh bind is minted after disposal'); + assert.notStrictEqual(third.providers![0].bearerToken, first.providers![0].bearerToken, 'the fresh bind carries a new nonce'); + + store.dispose(); + }); +}); From 0a36dd90528cc4b1aa110cdd9947291f3137f0be Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 26 Jun 2026 14:34:27 +0200 Subject: [PATCH 012/589] adding hover copy button in verbose hover --- .../hover/browser/contentHoverRendered.ts | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts index 7700f6c4215f1c..c82875e205b466 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts @@ -226,6 +226,7 @@ class RenderedContentHoverParts extends Disposable { }); private readonly _renderedParts: IRenderedContentHoverPartOrStatusBar[] = []; + private readonly _perPartDisposables = new Map(); private readonly _fragment: DocumentFragment; private readonly _context: IEditorHoverContext; @@ -320,29 +321,42 @@ class RenderedContentHoverParts extends Disposable { } private _registerListenersOnRenderedParts(): IDisposable { - const disposables = new DisposableStore(); + // Create per-part disposables so that when an individual rendered part is + // updated we can dispose its listeners and copy button without affecting + // the others. this._renderedParts.forEach((renderedPart: IRenderedContentHoverPartOrStatusBar, index: number) => { - const element = renderedPart.hoverElement; - element.tabIndex = 0; - disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_IN, (event: Event) => { - event.stopPropagation(); - this._focusedHoverPartIndex = index; - })); - disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_OUT, (event: Event) => { - event.stopPropagation(); - this._focusedHoverPartIndex = -1; - })); - // Add copy button for marker hovers - if (renderedPart.type === 'hoverPart' && !renderedPart.participant.hideCopyButton) { - disposables.add(new HoverCopyButton( - element, - () => renderedPart.participant.getAccessibleContent(renderedPart.hoverPart), - this._clipboardService, - this._hoverService - )); + this._createListenersForPart(index, renderedPart); + }); + return toDisposable(() => { + for (const d of this._perPartDisposables.values()) { + d.dispose(); } + this._perPartDisposables.clear(); }); - return disposables; + } + + private _createListenersForPart(index: number, renderedPart: IRenderedContentHoverPartOrStatusBar): void { + const partDisposables = new DisposableStore(); + const element = renderedPart.hoverElement; + element.tabIndex = 0; + partDisposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_IN, (event: Event) => { + event.stopPropagation(); + this._focusedHoverPartIndex = index; + })); + partDisposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_OUT, (event: Event) => { + event.stopPropagation(); + this._focusedHoverPartIndex = -1; + })); + // Add copy button for marker hovers + if (renderedPart.type === 'hoverPart' && !renderedPart.participant.hideCopyButton) { + partDisposables.add(new HoverCopyButton( + element, + () => renderedPart.participant.getAccessibleContent(renderedPart.hoverPart), + this._clipboardService, + this._hoverService + )); + } + this._perPartDisposables.set(index, partDisposables); } private _updateMarkdownAndColorParticipantInfo(participants: IEditorHoverParticipant[]) { @@ -409,12 +423,20 @@ class RenderedContentHoverParts extends Disposable { if (!renderedPart) { continue; } + // Dispose any listeners/copy button for the previous part at this index + const prevDisposable = this._perPartDisposables.get(i); + if (prevDisposable) { + prevDisposable.dispose(); + this._perPartDisposables.delete(i); + } this._renderedParts[i] = { type: 'hoverPart', participant: this._markdownHoverParticipant, hoverPart: renderedPart.hoverPart, hoverElement: renderedPart.hoverElement, }; + // Recreate listeners and copy button for the updated part. + this._createListenersForPart(i, this._renderedParts[i]); } if (focus) { if (index >= 0) { From a616d423c767bac7ba343a3e0bdbc0ee58e28b8b Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci Date: Fri, 26 Jun 2026 18:46:53 +0200 Subject: [PATCH 013/589] add kimi prompt --- .../prompts/node/agent/allAgentPrompts.ts | 2 +- .../prompts/node/agent/kimiPrompts.tsx | 146 ++++++++++++++++++ .../node/agent/test/kimiPrompts.spec.tsx | 64 ++++++++ 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx create mode 100644 extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx diff --git a/extensions/copilot/src/extension/prompts/node/agent/allAgentPrompts.ts b/extensions/copilot/src/extension/prompts/node/agent/allAgentPrompts.ts index 098d8460fe8f24..29ff019aa07d2e 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/allAgentPrompts.ts +++ b/extensions/copilot/src/extension/prompts/node/agent/allAgentPrompts.ts @@ -6,6 +6,7 @@ import './anthropicPrompts'; import './familyHPrompts'; import './geminiPrompts'; +import './kimiPrompts'; import './minimaxPrompts'; import './vscModelPrompts'; // vscModelPrompts must be imported before gpt5Prompt to ensure VSC model prompt resolvers are registered first. @@ -21,4 +22,3 @@ import './openai/gpt5Prompt'; import './openai/hiddenModelMPrompt'; import './xAIPrompts'; import './zaiPrompts'; - diff --git a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx new file mode 100644 index 00000000000000..e97392f5f5cfae --- /dev/null +++ b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptElement, PromptSizing } from '@vscode/prompt-tsx'; +import { IChatEndpoint } from '../../../../platform/networking/common/networking'; +import { agenticBrowserTools, ToolName } from '../../../tools/common/toolNames'; +import { InstructionMessage } from '../base/instructionMessage'; +import { ResponseTranslationRules } from '../base/responseTranslationRules'; +import { Tag } from '../base/tag'; +import { EXISTING_CODE_MARKER } from '../panel/codeBlockFormattingRules'; +import { ResponseRenderingRules } from '../panel/editorIntegrationRules'; +import { ApplyPatchInstructions, CodesearchModeInstructions, DefaultAgentPromptProps, DefaultReminderInstructions, detectToolCapabilities, GenericEditingTips, McpToolInstructions, NotebookInstructions } from './defaultAgentInstructions'; +import { FileLinkificationInstructions } from './fileLinkificationInstructions'; +import { IAgentPrompt, PromptRegistry, ReminderInstructionsConstructor, SystemPrompt } from './promptRegistry'; + +function isKimiModel(model: string): boolean { + const normalized = model.toLowerCase(); + return normalized === 'kimi-k2.6' || normalized === 'kimi-k2.7-code'; +} + +class KimiAgentPrompt extends PromptElement { + async render(state: void, sizing: PromptSizing) { + const tools = detectToolCapabilities(this.props.availableTools); + + return + + You are an expert AI programming assistant, working with a user in the VS Code editor. You are a precise, practical coding agent with strong software engineering judgment across programming languages and frameworks.
+ Follow the user's requirements carefully and use the provided workspace context, attachments, and tool results as reference material. If the answer is not supported by the available context, gather more context before acting or state the limitation clearly. +
+ + + Use clear, step-by-step task execution:
+ - For simple questions or code samples, answer directly without unnecessary tool calls.
+ - For codebase questions, gather the smallest sufficient set of relevant context, then answer with concrete references.
+ - For implementation tasks, identify the controlling code path, make focused changes, and validate with the most relevant available checks.
+ - For feature requests without specified files, break the request into concepts and find the files responsible for those concepts before editing.
+ - Do not guess about APIs, file paths, or project conventions. Verify them using context or tools. +
+ + + Avoid excessive looping or repetition:
+ - If you find yourself running similar commands or re-editing the same files without clear progress, stop and reassess rather than continuing to loop.
+ - If an action fails or does not work as expected, do not retry it unchanged. Understand why it failed, then try a different approach.
+ - Never call the same tool with the same arguments more than twice in a row.
+ - If you are stuck or no longer making progress, end the turn with a concise summary of what you tried, what is blocked, and any clarifying question needed. +
+ + + Important: Use built-in tools instead of terminal commands whenever possible.
+ {tools[ToolName.ReadFile] && <>- Use {ToolName.ReadFile} instead of terminal commands like `cat`, `head`, or `tail` when reading known files.
} + {tools[ToolName.FindTextInFiles] && <>- Use {ToolName.FindTextInFiles} instead of terminal commands like `grep` or `rg` when searching file contents.
} + {tools[ToolName.FindFiles] && <>- Use {ToolName.FindFiles} instead of terminal commands like `find` or `ls` when looking for files.
} + {tools.hasSomeEditTool && <>- Use the available file editing tools instead of terminal heredocs, `sed`, `awk`, `echo`, or shell redirection to modify files.
} + {tools[ToolName.CoreRunInTerminal] && <>- Use {ToolName.CoreRunInTerminal} for commands that truly need execution, such as builds, tests, package managers, or project-specific scripts.
} +
+ + + You will be given context and attachments along with the user prompt. Use relevant context and ignore irrelevant context.{tools[ToolName.ReadFile] && <> Some attachments may be summarized with omitted sections like `/* Lines 123-456 omitted */`. Use {ToolName.ReadFile} to read more context if needed. Never pass this omitted line marker to an edit tool.}
+ If you can infer the project type (languages, frameworks, and libraries) from the user's query or the context, keep it in mind when making changes.
+ When reading files, prefer reading large meaningful chunks rather than consecutive small sections to minimize tool calls and gain better context.
+ You do not need to read a file if it is already provided in context. +
+ + + When using a tool, follow the JSON schema carefully and include all required properties.
+ No need to ask permission before using a tool.
+ NEVER say the name of a tool to a user. For example, instead of saying that you'll use the {ToolName.CoreRunInTerminal} tool, say "I'll run the command in a terminal".
+ If multiple independent tool calls can answer the user's question, prefer calling them in parallel whenever possible{tools[ToolName.Codebase] && <>, but do not call {ToolName.Codebase} in parallel}.
+ {(tools[ToolName.SearchSubagent] || tools[ToolName.ExploreSubagent]) && <>For efficient codebase exploration, prefer {tools[ToolName.SearchSubagent] ? ToolName.SearchSubagent : ToolName.ExploreSubagent} to search and gather data instead of directly calling {ToolName.FindTextInFiles}, {ToolName.Codebase} or {ToolName.FindFiles}.
} + {tools[ToolName.ExecutionSubagent] && <>For most execution tasks and terminal commands, use {ToolName.ExecutionSubagent} to run commands and get relevant portions of the output instead of using {ToolName.CoreRunInTerminal}. Use {ToolName.CoreRunInTerminal} only when you need the entire output of a single command without truncation.
} + {tools[ToolName.ReadFile] && <>When using {ToolName.ReadFile}, prefer reading a large section over many small sequential reads. Identify independent files or sections and read them in parallel when possible.
} + {tools[ToolName.Codebase] && <>If {ToolName.Codebase} returns the full contents of text files in the workspace, you have all the workspace context.
} + {tools[ToolName.FindTextInFiles] && <>Use {ToolName.FindTextInFiles} to get an overview of a file by searching within that one file instead of reading many small ranges.
} + {tools[ToolName.Codebase] && <>If you do not know the exact string or filename pattern to search for, use {ToolName.Codebase} for semantic search across the workspace.
} + {tools[ToolName.CoreRunInTerminal] && <>Do not call {ToolName.CoreRunInTerminal} multiple times in parallel. Run one command and wait for the output before running the next command.
} + {tools[ToolName.ExecutionSubagent] && <>Do not call {ToolName.ExecutionSubagent} multiple times in parallel. Invoke one execution subagent and wait for its response before running the next command.
} + When invoking a tool that takes a file path, always use the absolute file path. If the file has a scheme like untitled: or vscode-userdata:, use a URI with the scheme.
+ {tools[ToolName.CoreRunInTerminal] && <>NEVER try to edit a file by running terminal commands unless the user specifically asks for it.
} + {!tools.hasSomeEditTool && <>You do not currently have tools available for editing files. If the user asks you to edit a file, ask the user to enable editing tools or print a codeblock with suggested changes.
} + {!tools[ToolName.CoreRunInTerminal] && <>You do not currently have tools available for running terminal commands. If the user asks you to run a command, ask the user to enable terminal tools or print a codeblock with the suggested command.
} + {tools[ToolName.CoreOpenBrowserPage] && tools.hasAgenticBrowserTools && <>Use the browser tools ({ToolName.CoreOpenBrowserPage}, {agenticBrowserTools.find(k => tools[k])}, etc.) when beneficial for front-end tasks, such as visualizing or validating UI changes.
} + Tools can be disabled by the user. You may see tools used previously in the conversation that are not currently available. Only use tools that are currently available. +
+ + {this.props.codesearchMode && } + + {tools[ToolName.EditFile] && !tools[ToolName.ApplyPatch] && + {tools[ToolName.ReplaceString] ? + <> + Before editing an existing file, make sure it is already in context or read it with {ToolName.ReadFile}.
+ {tools[ToolName.MultiReplaceString] + ? <>Use {ToolName.ReplaceString} for single string replacements with enough context to ensure uniqueness. Prefer {ToolName.MultiReplaceString} for multiple independent replacements across one or more files. Do not announce which tool you're using.
+ : <>Use {ToolName.ReplaceString} to edit files. Include sufficient surrounding context so the replacement is unique. You can use this tool multiple times per file.
} + Use {ToolName.EditFile} to insert code into a file only if {tools[ToolName.MultiReplaceString] ? `${ToolName.MultiReplaceString}/` : ''}{ToolName.ReplaceString} has failed.
+ Group changes by file.
+ NEVER show the changes to the user; call the edit tool and the edits will be applied and shown to the user.
+ NEVER print a codeblock that represents a change to a file. Use {ToolName.ReplaceString}{tools[ToolName.MultiReplaceString] ? `, ${ToolName.MultiReplaceString},` : ''} or {ToolName.EditFile} instead.
+ For each file, give a short description of what needs to be changed, then use the edit tool.
+ : <> + Do not edit an existing file without reading it first.
+ Use {ToolName.EditFile} to edit files. Group changes by file.
+ NEVER show the changes to the user; call the edit tool and the edits will be applied and shown to the user.
+ NEVER print a codeblock that represents a change to a file. Use {ToolName.EditFile} instead.
+ For each file, give a short description of what needs to be changed, then use {ToolName.EditFile}.
+ } + + The {ToolName.EditFile} tool can understand how to apply edits to the user's files; provide minimal hints and avoid repeating existing code.
+ When using {ToolName.EditFile}, use comments to represent unchanged regions. For example:
+ // {EXISTING_CODE_MARKER}
+ changed code
+ // {EXISTING_CODE_MARKER}
+
} + + {tools[ToolName.ApplyPatch] && } + {this.props.availableTools && } + + + + Use proper Markdown formatting. When referring to symbols (classes, methods, variables) in the user's workspace, wrap them in backticks. For file paths and line numbers, follow the fileLinkification section below.
+ + +
+ +
; + } +} + +class KimiPromptResolver implements IAgentPrompt { + static readonly familyPrefixes = ['kimi-k2.6', 'kimi-k2.7-code']; + + static matchesModel(endpoint: IChatEndpoint): boolean { + return isKimiModel(endpoint.family) || isKimiModel(endpoint.model); + } + + resolveSystemPrompt(endpoint: IChatEndpoint): SystemPrompt | undefined { + return KimiAgentPrompt; + } + + resolveReminderInstructions(endpoint: IChatEndpoint): ReminderInstructionsConstructor | undefined { + return DefaultReminderInstructions; + } +} + +PromptRegistry.registerPrompt(KimiPromptResolver); diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx b/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx new file mode 100644 index 00000000000000..315281fffa7f4a --- /dev/null +++ b/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Raw } from '@vscode/prompt-tsx'; +import { afterAll, beforeAll, expect, suite, test } from 'vitest'; +import { IChatMLFetcher } from '../../../../../platform/chat/common/chatMLFetcher'; +import { StaticChatMLFetcher } from '../../../../../platform/chat/test/common/staticChatMLFetcher'; +import { MockEndpoint } from '../../../../../platform/endpoint/test/node/mockEndpoint'; +import { messageToMarkdown } from '../../../../../platform/log/common/messageStringify'; +import { IResponseDelta } from '../../../../../platform/networking/common/fetch'; +import { ITestingServicesAccessor } from '../../../../../platform/test/node/services'; +import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation'; +import { createExtensionUnitTestingServices } from '../../../../test/node/services'; +import { IToolsService } from '../../../../tools/common/toolsService'; +import { PromptRenderer } from '../../base/promptRenderer'; +import '../allAgentPrompts'; +import { PromptRegistry } from '../promptRegistry'; + +suite('KimiPrompts', () => { + let accessor: ITestingServicesAccessor; + + beforeAll(() => { + const services = createExtensionUnitTestingServices(); + const chatResponse: (string | IResponseDelta[])[] = []; + services.define(IChatMLFetcher, new StaticChatMLFetcher(chatResponse)); + accessor = services.createTestingAccessor(); + }); + + afterAll(() => { + accessor.dispose(); + }); + + async function renderSystemPrompt(family: string): Promise { + const instantiationService = accessor.get(IInstantiationService); + const endpoint = instantiationService.createInstance(MockEndpoint, family); + const customizations = await PromptRegistry.resolveAllCustomizations(instantiationService, endpoint); + const renderer = PromptRenderer.create(instantiationService, endpoint, customizations.SystemPrompt, { + availableTools: accessor.get(IToolsService).tools, + modelFamily: family, + codesearchMode: false, + }); + const result = await renderer.render(); + return result.messages + .filter(message => message.role === Raw.ChatRole.System) + .map(message => messageToMarkdown(message)) + .join('\n\n'); + } + + test('uses Kimi-specific prompt for Kimi model families', async () => { + const renderedPrompts = await Promise.all([ + renderSystemPrompt('kimi-k2.6'), + renderSystemPrompt('kimi-k2.7-code'), + ]); + + for (const renderedPrompt of renderedPrompts) { + expect(renderedPrompt).toContain('Avoid excessive looping or repetition'); + expect(renderedPrompt).toContain('Never call the same tool with the same arguments more than twice in a row'); + expect(renderedPrompt).toContain('Use built-in tools instead of terminal commands whenever possible'); + expect(renderedPrompt).toContain('Use the available file editing tools instead of terminal heredocs'); + } + }); +}); From cc96f8fbb69b9841db04a9e65d3af032ab716243 Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci Date: Fri, 26 Jun 2026 18:29:39 +0200 Subject: [PATCH 014/589] agentHost: show download progress for agent SDK binaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VS Code ships the Claude and Codex agents but not their native SDK binaries; those are downloaded on demand from the CDN the first time a session of that provider materializes, then cached on disk. The tarballs are 70-95 MB, so on a cold cache the first session start blocked for several seconds with no user feedback. Surface a progress indicator instead. Wire format: a generic, operation-agnostic `root/progress` notification (mirrors the AHP spec), correlated to its originating request by a `progressToken` the client supplies on `createSession` rather than naming a domain object. Completion is signalled by `progress === total`; the host emits a terminal frame with `total === progress`. Downloader: `_fetch` accumulates received bytes and reads `Content-Length`, firing a throttled host-level `onDidDownloadProgress` event keyed by package id. Adds `isSdkResolvableWithoutDownload` / `canLoadWithoutDownload` so eager/background callers (e.g. `listSessions` at startup) never kick off a cold download. Server (agent host): `createSession` records `{provider -> {session -> token}}`; when a session materializes on first message and triggers the cold download, the host-level event is fanned out as one `root/progress` notification per waiting token (package id === provider id), building the localized `message` ("Downloading {0} agent…"). Tokens are cleared on materialize / dispose. Codex prewarm is gated so it can't trigger a cold download. Client (renderer): the new-session composer supplies a `progressToken` on its eager `createSession`; `_handleProgress` correlates incoming frames by token, shows a determinate (or byte-count) notification, and dismisses it once `progress >= total`. Vendors the protocol copy at AHP d35ed4a. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/remoteAgentHostProtocolClient.ts | 1 + .../platform/agentHost/common/agentService.ts | 7 + .../common/state/protocol/.ahp-version | 2 +- .../protocol/channels-root/notifications.ts | 79 ++++++++ .../protocol/channels-session/commands.ts | 13 ++ .../common/state/protocol/common/messages.ts | 3 +- .../common/state/protocol/version/registry.ts | 1 + .../agentHost/common/state/sessionActions.ts | 5 +- .../platform/agentHost/node/agentHostMain.ts | 27 ++- .../agentHost/node/agentHostServerMain.ts | 23 ++- .../agentHost/node/agentHostStateManager.ts | 19 +- .../agentHost/node/agentSdkDownloader.ts | 180 +++++++++++++++++- .../platform/agentHost/node/agentService.ts | 70 +++++++ .../agentHost/node/claude/claudeAgent.ts | 9 + .../node/claude/claudeAgentSdkService.ts | 22 +++ .../agentHost/node/codex/codexAgent.ts | 23 ++- .../agentHost/node/protocolServerHandler.ts | 1 + .../test/node/agentSdkDownloader.test.ts | 34 +++- .../test/node/claudeAgent.integrationTest.ts | 4 + .../agentHost/test/node/claudeAgent.test.ts | 6 + .../test/node/claudeSubagentResolver.test.ts | 1 + .../test/node/protocolServerHandler.test.ts | 66 ++++++- .../browser/baseAgentHostSessionsProvider.ts | 117 +++++++++++- .../browser/localAgentHostSessionsProvider.ts | 4 +- .../localAgentHostSessionsProvider.test.ts | 2 + .../remoteAgentHostSessionsProvider.ts | 4 +- .../remoteAgentHostSessionsProvider.test.ts | 2 + 27 files changed, 699 insertions(+), 26 deletions(-) diff --git a/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts b/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts index 5a9f832f7af002..56702dba08c769 100644 --- a/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts +++ b/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts @@ -1087,6 +1087,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC case 'root/sessionAdded': case 'root/sessionRemoved': case 'root/sessionSummaryChanged': + case 'root/progress': case 'auth/required': { this._logService.trace(`[RemoteAgentHostProtocol] Notification: ${msg.method}`); // The case narrows `msg.method` to a single literal; the matching params diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 48e60ab8056fdf..b411706d90572e 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -653,6 +653,13 @@ export interface IAgentCreateSessionConfig { */ readonly turnIdMapping?: ReadonlyMap; }; + /** + * MCP-style opt-in progress token from the client's `createSession`. When + * set, the service reports any long-running session bring-up work — chiefly + * the lazy first-use SDK download — as `progress` notifications carrying + * this token, so the client can correlate them to this call. + */ + readonly progressToken?: string; } /** Options for creating an additional chat within a session. */ diff --git a/src/vs/platform/agentHost/common/state/protocol/.ahp-version b/src/vs/platform/agentHost/common/state/protocol/.ahp-version index ec859badb2194d..d3decf3e05a0ba 100644 --- a/src/vs/platform/agentHost/common/state/protocol/.ahp-version +++ b/src/vs/platform/agentHost/common/state/protocol/.ahp-version @@ -1 +1 @@ -0259a7e +d35ed4a diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-root/notifications.ts b/src/vs/platform/agentHost/common/state/protocol/channels-root/notifications.ts index 5b5271dd8924c1..7746387f6dd6e1 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-root/notifications.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-root/notifications.ts @@ -143,3 +143,82 @@ export interface SessionSummaryChangedParams { */ changes: Partial; } + +// ─── progress ──────────────────────────────────────────────────────────────── + +/** + * Generic progress notification for a long-running operation. + * + * A client opts in to progress for a request by including a `progressToken` in + * that request (today: the `progressToken` field on `createSession`). If the + * server does long-running work to service the request — e.g. lazily + * downloading an agent's native SDK the first time a session of that provider + * is materialized — it emits `progress` notifications carrying the same token. + * + * The notification is operation-agnostic: it says nothing about *what* is + * progressing. The client correlates `progressToken` back to the request it + * originated from (and thus the UI surface awaiting it) and renders its own + * localized indicator. The same channel serves any future long-running + * operation without a new method. + * + * Semantics: + * + * - `progress` is monotonically non-decreasing for a given `progressToken`. + * - `total` is present only when the server knows the magnitude up front + * (e.g. a `Content-Length`); when absent the client SHOULD show an + * indeterminate indicator. + * - The operation is complete when `progress === total`. The server MUST emit a + * final frame satisfying `progress === total`; when the total was never + * known, it sets `total` to the final `progress` on that frame. No further + * frames reference the token afterwards. + * - The server MAY emit no progress at all (e.g. the work was already done); + * the client then never shows an indicator. + * - Like all notifications this is ephemeral and is **not** replayed on + * reconnect. A client that never receives the terminal frame SHOULD expire + * the indicator after an idle timeout. + * + * @category Protocol Notifications + * @method root/progress + * @direction Server → Client + * @messageType Notification + * @version 1 + * @example + * ```json + * { + * "jsonrpc": "2.0", + * "method": "root/progress", + * "params": { + * "channel": "ahp-root://", + * "progressToken": "9b2c1f7e-4a0d-4e2b-8b1a-2f7e4a0d4e2b", + * "progress": 18874368, + * "total": 41957498 + * } + * } + * ``` + */ +export interface ProgressParams { + /** Channel URI this notification belongs to (the root channel). */ + channel: URI; + /** + * Echoes the `progressToken` the client supplied on the originating request + * (e.g. the `progressToken` field of `createSession`), correlating this frame + * to that call. Unique across the client's active requests. + */ + progressToken: string; + /** + * Progress so far, in operation-defined units (e.g. bytes received). + * Monotonically non-decreasing for a given `progressToken`. + */ + progress: number; + /** + * Total when known up front (e.g. from a `Content-Length`); omitted ⇒ + * indeterminate. The operation is complete once `progress === total`. + */ + total?: number; + /** + * Optional human-readable progress message. The client owns its own + * (localized) presentation derived from the originating request; generic + * clients that don't track the token MAY display this instead. + */ + message?: string; +} diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-session/commands.ts b/src/vs/platform/agentHost/common/state/protocol/channels-session/commands.ts index 268856b57cc2e9..41fda59f9e374d 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-session/commands.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-session/commands.ts @@ -93,6 +93,19 @@ export interface CreateSessionParams extends BaseParams { * `clientId` the creating client supplied in `initialize`. */ activeClient?: SessionActiveClient; + /** + * Opt-in progress token. When set, the client is offering to receive + * `progress` notifications (see `ProgressParams`) for any long-running work + * the server does to bring this session up — most notably the lazy, + * first-use download of the provider's native SDK. The server echoes this + * exact token on every `progress` frame so the client can correlate it to + * this `createSession` call (and the UI awaiting it). + * + * The token MUST be unique across the client's active requests. The server + * MAY ignore it (e.g. when nothing long-running is needed), in which case no + * `progress` notifications are emitted. + */ + progressToken?: string; } // ─── disposeSession ────────────────────────────────────────────────────────── diff --git a/src/vs/platform/agentHost/common/state/protocol/common/messages.ts b/src/vs/platform/agentHost/common/state/protocol/common/messages.ts index bf010f1d70aa12..3eb000893c75d9 100644 --- a/src/vs/platform/agentHost/common/state/protocol/common/messages.ts +++ b/src/vs/platform/agentHost/common/state/protocol/common/messages.ts @@ -15,7 +15,7 @@ import type { CreateResourceWatchParams, CreateResourceWatchResult } from '../ch import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from '../channels-changeset/commands.js'; import type { ActionEnvelope } from './actions.js'; -import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams } from '../channels-root/notifications.js'; +import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, ProgressParams } from '../channels-root/notifications.js'; import type { AuthRequiredParams } from './notifications.js'; import type { OtlpExportLogsParams, OtlpExportTracesParams, OtlpExportMetricsParams } from '../channels-otlp/notifications.js'; import type { AhpError } from './errors.js'; @@ -166,6 +166,7 @@ export interface ServerNotificationMap { 'root/sessionAdded': { params: SessionAddedParams }; 'root/sessionRemoved': { params: SessionRemovedParams }; 'root/sessionSummaryChanged': { params: SessionSummaryChangedParams }; + 'root/progress': { params: ProgressParams }; 'auth/required': { params: AuthRequiredParams }; 'otlp/exportLogs': { params: OtlpExportLogsParams }; 'otlp/exportTraces': { params: OtlpExportTracesParams }; diff --git a/src/vs/platform/agentHost/common/state/protocol/version/registry.ts b/src/vs/platform/agentHost/common/state/protocol/version/registry.ts index d7bb394ea74c2f..c177e0ce87181e 100644 --- a/src/vs/platform/agentHost/common/state/protocol/version/registry.ts +++ b/src/vs/platform/agentHost/common/state/protocol/version/registry.ts @@ -182,6 +182,7 @@ export const NOTIFICATION_INTRODUCED_IN: { readonly [K in ProtocolNotificationMe 'root/sessionAdded': '0.1.0', 'root/sessionRemoved': '0.1.0', 'root/sessionSummaryChanged': '0.1.0', + 'root/progress': '0.5.0', 'auth/required': '0.1.0', 'otlp/exportLogs': '0.2.0', 'otlp/exportTraces': '0.2.0', diff --git a/src/vs/platform/agentHost/common/state/sessionActions.ts b/src/vs/platform/agentHost/common/state/sessionActions.ts index a8b72183236896..f6cd6c39983d03 100644 --- a/src/vs/platform/agentHost/common/state/sessionActions.ts +++ b/src/vs/platform/agentHost/common/state/sessionActions.ts @@ -79,6 +79,7 @@ export { type SessionAddedParams, type SessionRemovedParams, type SessionSummaryChangedParams, + type ProgressParams, type AuthRequiredParams, } from './protocol/notifications.js'; @@ -92,6 +93,7 @@ export const NotificationType = { SessionAdded: 'root/sessionAdded', SessionRemoved: 'root/sessionRemoved', SessionSummaryChanged: 'root/sessionSummaryChanged', + Progress: 'root/progress', AuthRequired: 'auth/required', } as const; export type NotificationType = typeof NotificationType[keyof typeof NotificationType]; @@ -131,7 +133,7 @@ import type { RootConfigChangedAction, } from './protocol/actions.js'; -import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, AuthRequiredParams } from './protocol/notifications.js'; +import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, ProgressParams, AuthRequiredParams } from './protocol/notifications.js'; import type { RootAction as IRootAction_, SessionAction as ISessionAction_, ChatAction as IChatAction_, ClientSessionAction as IClientSessionAction_, ServerSessionAction as IServerSessionAction_, ClientChatAction as IClientChatAction_, ServerChatAction as IServerChatAction_, TerminalAction as ITerminalAction_, ClientTerminalAction as IClientTerminalAction_, ChangesetAction as IChangesetAction_, AnnotationsAction as IAnnotationsAction_, ClientAnnotationsAction as IClientAnnotationsAction_ } from './protocol/action-origin.generated.js'; /** @@ -144,6 +146,7 @@ export type ProtocolNotification = | ({ type: 'root/sessionAdded' } & SessionAddedParams) | ({ type: 'root/sessionRemoved' } & SessionRemovedParams) | ({ type: 'root/sessionSummaryChanged' } & SessionSummaryChangedParams) + | ({ type: 'root/progress' } & ProgressParams) | ({ type: 'auth/required' } & AuthRequiredParams); export type RootAction = IRootAction_; diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index cc6e9cd1c2b764..629a0cf7444787 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -7,7 +7,7 @@ import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; import { Server as ChildProcessServer } from '../../../base/parts/ipc/node/ipc.cp.js'; import { Server as UtilityProcessServer } from '../../../base/parts/ipc/node/ipc.mp.js'; import { isUtilityProcess } from '../../../base/parts/sandbox/node/electronTypes.js'; -import { Emitter } from '../../../base/common/event.js'; +import { Emitter, type Event } from '../../../base/common/event.js'; import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; import { joinPath } from '../../../base/common/resources.js'; import { isWindows } from '../../../base/common/platform.js'; @@ -29,7 +29,7 @@ import { ClaudeAgentSdkService, ClaudeSdkPackage, IClaudeAgentSdkService } from import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxyService.js'; import { CodexAgent, CodexSdkPackage } from './codex/codexAgent.js'; import { CodexProxyService, ICodexProxyService } from './codex/codexProxyService.js'; -import { AgentSdkDownloader, IAgentSdkDownloader } from './agentSdkDownloader.js'; +import { AgentSdkDownloader, IAgentSdkDownloader, type IAgentSdkDownloadProgress } from './agentSdkDownloader.js'; import { IAgentHostOTelService } from '../common/otel/agentHostOTelService.js'; import { AgentHostOTelService } from './otel/agentHostOTelService.js'; import { ProtocolServerHandler } from './protocolServerHandler.js'; @@ -132,6 +132,9 @@ async function startAgentHost(): Promise { // Create the real service implementation that lives in this process let agentService: AgentService; let instantiationService: IInstantiationService; + // Hoisted out of the `try` below so the protocol handlers (constructed + // after the block) can forward agent-SDK download progress to clients. + let sdkDownloadProgress: Event | undefined; try { // Build the DI container early so the git service can be created via // `createInstance` (it needs IFileService + INativeEnvironmentService). @@ -162,8 +165,9 @@ async function startAgentHost(): Promise { // Register the agent SDK downloader BEFORE any service that injects it // (ClaudeAgentSdkService and CodexAgent below). The downloader resolves // dev-override env var → on-disk cache → product.agentSdks download. - const agentSdkDownloader = instantiationService.createInstance(AgentSdkDownloader); + const agentSdkDownloader = disposables.add(instantiationService.createInstance(AgentSdkDownloader)); diServices.set(IAgentSdkDownloader, agentSdkDownloader); + sdkDownloadProgress = agentSdkDownloader.onDidDownloadProgress; const copilotApiService = instantiationService.createInstance(CopilotApiService, undefined); diServices.set(ICopilotApiService, copilotApiService); diServices.set(ICopilotBranchNameGenerator, instantiationService.createInstance(CopilotBranchNameGenerator)); @@ -227,6 +231,23 @@ async function startAgentHost(): Promise { logService.error('Failed to create AgentService', err); throw err; } + + // Surface agent-SDK download progress to clients as generic `progress` + // notifications. The downloader fires process-global frames keyed by package + // id; the agent service fans each out to the `createSession` progress tokens + // of the sessions waiting on that provider's SDK, routed through the state + // manager so both the local (IPC) and any external (WebSocket) renderer + // receive them via the same path as session updates. + if (sdkDownloadProgress) { + disposables.add(sdkDownloadProgress(p => agentService.emitDownloadProgress( + p.packageId, + p.displayName, + p.receivedBytes, + p.totalBytes, + p.phase === 'completed' || p.phase === 'failed', + ))); + } + const agentChannel = ProxyChannel.fromService(agentService, disposables); server.registerChannel(AgentHostIpcChannels.AgentHost, agentChannel); diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index 6b26b821fbf482..252dd06cda9e59 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -15,6 +15,7 @@ globalThis._VSCODE_FILE_ROOT = fileURLToPath(new URL('../../../..', import.meta. import * as fs from 'fs'; import * as os from 'os'; +import type { Event } from '../../../base/common/event.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; import { raceTimeout } from '../../../base/common/async.js'; import { joinPath } from '../../../base/common/resources.js'; @@ -41,7 +42,7 @@ import { ClaudeAgentSdkService, ClaudeSdkPackage, IClaudeAgentSdkService } from import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxyService.js'; import { CodexAgent, CodexSdkPackage } from './codex/codexAgent.js'; import { CodexProxyService, ICodexProxyService } from './codex/codexProxyService.js'; -import { AgentSdkDownloader, IAgentSdkDownloader } from './agentSdkDownloader.js'; +import { AgentSdkDownloader, IAgentSdkDownloader, type IAgentSdkDownloadProgress } from './agentSdkDownloader.js'; import { IAgentHostOTelService } from '../common/otel/agentHostOTelService.js'; import { AgentHostOTelService } from './otel/agentHostOTelService.js'; import { AgentService } from './agentService.js'; @@ -249,6 +250,7 @@ async function main(): Promise { diServices.set(IAgentService, agentService); // Register agents + let sdkDownloadProgress: Event | undefined; if (!options.quiet) { // Production agents (require DI) const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService); @@ -273,8 +275,9 @@ async function main(): Promise { process.env[AgentHostCodexAgentSdkRootEnvVar] = options.codexSdkRoot; } // Register the agent SDK downloader BEFORE any service that injects it. - const agentSdkDownloader = instantiationService.createInstance(AgentSdkDownloader); + const agentSdkDownloader = disposables.add(instantiationService.createInstance(AgentSdkDownloader)); diServices.set(IAgentSdkDownloader, agentSdkDownloader); + sdkDownloadProgress = agentSdkDownloader.onDidDownloadProgress; const claudeProxyService = disposables.add(instantiationService.createInstance(ClaudeProxyService)); diServices.set(IClaudeProxyService, claudeProxyService); const claudeAgentSdkService = instantiationService.createInstance(ClaudeAgentSdkService); @@ -311,6 +314,22 @@ async function main(): Promise { } } + // Surface agent-SDK download progress to clients as generic `progress` + // notifications. The downloader fires process-global frames keyed by package + // id; the agent service fans each out to the `createSession` progress tokens + // of the sessions waiting on that provider's SDK, routed through the state + // manager so both local (IPC) and remote (WebSocket) renderers receive them + // via the same path as session updates. + if (sdkDownloadProgress) { + disposables.add(sdkDownloadProgress(p => agentService.emitDownloadProgress( + p.packageId, + p.displayName, + p.receivedBytes, + p.totalBytes, + p.phase === 'completed' || p.phase === 'failed', + ))); + } + if (options.enableMockAgent) { // Dynamic import to avoid bundling test code in production import('../test/node/mockAgent.js').then(({ ScriptedMockAgent }) => { diff --git a/src/vs/platform/agentHost/node/agentHostStateManager.ts b/src/vs/platform/agentHost/node/agentHostStateManager.ts index 24305095ab276d..0fc1c1fdb92942 100644 --- a/src/vs/platform/agentHost/node/agentHostStateManager.ts +++ b/src/vs/platform/agentHost/node/agentHostStateManager.ts @@ -9,7 +9,7 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { equals } from '../../../base/common/objects.js'; import { ILogService } from '../../log/common/log.js'; import { TelemetryLevel } from '../../telemetry/common/telemetry.js'; -import { ActionType, ActionEnvelope, ActionOrigin, INotification, IRootConfigChangedAction, SessionAction, ChatAction, RootAction, StateAction, TerminalAction, ChangesetAction, AnnotationsAction, ClientAnnotationsAction, isRootAction, isSessionAction, isChatAction, isChangesetAction, isAnnotationsAction } from '../common/state/sessionActions.js'; +import { ActionType, ActionEnvelope, ActionOrigin, INotification, IRootConfigChangedAction, SessionAction, ChatAction, RootAction, StateAction, TerminalAction, ChangesetAction, AnnotationsAction, ClientAnnotationsAction, isRootAction, isSessionAction, isChatAction, isChangesetAction, isAnnotationsAction, type ProgressParams } from '../common/state/sessionActions.js'; import type { IStateSnapshot } from '../common/state/sessionProtocol.js'; import { rootReducer, sessionReducer, chatReducer, changesetReducer, annotationsReducer } from '../common/state/sessionReducers.js'; import { createRootState, createSessionState, createChatState, createDefaultChatSummary, chatSummaryFromState, buildDefaultChatUri, parseDefaultChatUri, isAhpChatChannel, isDefaultChatUri, mergeSessionWithDefaultChat, isAhpRootChannel, SessionLifecycle, withHostBuildInfo, type Changeset, type ChangesetState, type AnnotationsState, type ChatState, type ChatSummary, type Customization, type ISessionWithDefaultChat, type RootState, type SessionConfigState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus, IHostBuildInfo, SessionStatus, SessionSummaryMeta } from '../common/state/sessionState.js'; @@ -1241,4 +1241,21 @@ export class AgentHostStateManager extends Disposable { }); } } + + /** + * Emit a generic progress notification on the root channel, correlated to + * the originating request by {@link ProgressParams.progressToken}. Routed to + * clients through the same {@link onDidEmitNotification} path as session + * notifications, so both the local (IPC proxy) and remote (WebSocket + * {@link ProtocolServerHandler}) renderers receive it without any + * transport-specific special casing. Progress for host-level work (e.g. a + * shared SDK download) rides the root channel rather than a per-session one. + */ + emitProgress(progress: Omit): void { + this._onDidEmitNotification.fire({ + type: 'root/progress', + channel: ROOT_STATE_URI, + ...progress, + }); + } } diff --git a/src/vs/platform/agentHost/node/agentSdkDownloader.ts b/src/vs/platform/agentHost/node/agentSdkDownloader.ts index 32c86eacaa17e4..55ce50b79f6511 100644 --- a/src/vs/platform/agentHost/node/agentSdkDownloader.ts +++ b/src/vs/platform/agentHost/node/agentSdkDownloader.ts @@ -8,9 +8,12 @@ import * as tar from 'tar'; import { VSBuffer } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { CancellationError } from '../../../base/common/errors.js'; +import { Emitter, Event } from '../../../base/common/event.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; import * as path from '../../../base/common/path.js'; import { format2 } from '../../../base/common/strings.js'; import { URI } from '../../../base/common/uri.js'; +import { generateUuid } from '../../../base/common/uuid.js'; import { detectLibcSync, type LibcFamily } from '../../../base/node/libc.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { FileOperationError, FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js'; @@ -47,6 +50,12 @@ import { IRequestContext } from '../../../base/parts/request/common/request.js'; export interface IAgentSdkPackage { /** Key under `product.agentSdks` — e.g. `'claude'`, `'codex'`. */ readonly id: string; + /** + * Brand display name for user-facing progress, e.g. `'Claude'`, `'Codex'`. + * The downloader puts this on {@link IAgentSdkDownloadProgress.displayName} + * so clients can build a localized "Downloading {displayName} agent…" label. + */ + readonly displayName: string; /** Env var that, when set, becomes the SDK root and short-circuits the download. */ readonly devOverrideEnvVar: string; /** @@ -111,9 +120,46 @@ export function resolveSdkTarget( export const IAgentSdkDownloader = createDecorator('agentSdkDownloader'); +/** Lifecycle phase of a single SDK download (downloader-internal). */ +export type AgentSdkDownloadPhase = 'started' | 'progress' | 'completed' | 'failed'; + +/** + * A process-global download-progress sample fired on + * {@link IAgentSdkDownloader.onDidDownloadProgress}. The downloader owns the + * lifecycle: one `started`, throttled `progress` frames, then exactly one + * terminal `completed` / `failed` — all sharing a `downloadId`. Concurrent + * `loadSdkRoot` callers for the same tarball are deduped, so they observe one + * shared download (one `downloadId`). + */ +export interface IAgentSdkDownloadProgress { + /** Stable id for one download; coalesces frames and distinguishes concurrent fetches. */ + readonly downloadId: string; + /** Package id, e.g. `'claude'` / `'codex'`. */ + readonly packageId: string; + /** Brand display name, e.g. `'Claude'`. */ + readonly displayName: string; + /** Lifecycle phase of this frame. */ + readonly phase: AgentSdkDownloadPhase; + /** Bytes written so far. Monotonically non-decreasing within a `downloadId`. */ + readonly receivedBytes: number; + /** Total bytes from `Content-Length`, or `undefined` when unknown (indeterminate). */ + readonly totalBytes: number | undefined; + /** Short, non-localized failure reason; present only when `phase: 'failed'`. */ + readonly error?: string; +} + export interface IAgentSdkDownloader { readonly _serviceBrand: undefined; + /** + * Fires while a tarball is being fetched (cold cache only): one `started`, + * throttled `progress` samples, then one terminal `completed` / `failed`. + * Never fires for dev-override or cache-hit resolutions (no bytes move). + * Process-global so a single subscriber (the protocol server) can forward + * progress to clients regardless of which session triggered the fetch. + */ + readonly onDidDownloadProgress: Event; + /** * Returns the absolute path of the SDK root directory — the directory that * contains the package's `node_modules/` subtree. Callers resolve the @@ -138,6 +184,20 @@ export interface IAgentSdkDownloader { * download. */ isAvailable(pkg: IAgentSdkPackage): boolean; + + /** + * True iff {@link loadSdkRoot} would resolve WITHOUT a network download — + * the dev override is set, or a completed cache for the configured version + * already exists on disk. False when product config is present but the + * cache is cold (a fetch would be required), and false when neither an + * override nor product config is configured. + * + * Performs at most a single sentinel `exists` check and never downloads. + * Eager / background callers (e.g. a provider listing its sessions at + * startup) use this to avoid kicking off a multi-second cold download + * before the user has asked for anything. + */ + isSdkResolvableWithoutDownload(pkg: IAgentSdkPackage): Promise; } // #endregion @@ -147,9 +207,31 @@ export interface IAgentSdkDownloader { /** How long a `loadSdkRoot` failure latches before we try again. */ const LOAD_FAILURE_NEGATIVE_CACHE_MS = 30_000; -export class AgentSdkDownloader implements IAgentSdkDownloader { +/** + * Minimum gap between download-progress samples. A 70-95MB tarball over a fast + * link produces thousands of chunks; without throttling we'd flood the progress + * channel. ~250ms keeps the percentage visibly moving without spamming. + */ +const PROGRESS_EMIT_THROTTLE_MS = 250; + +/** + * Parses a `Content-Length` header into a positive integer byte count, or + * `undefined` when the header is absent, an array, or not a clean integer. + */ +function parseContentLength(header: string | string[] | undefined): number | undefined { + if (typeof header !== 'string' || !/^\d+$/.test(header)) { + return undefined; + } + const parsed = parseInt(header, 10); + return parsed > 0 ? parsed : undefined; +} + +export class AgentSdkDownloader extends Disposable implements IAgentSdkDownloader { declare readonly _serviceBrand: undefined; + private readonly _onDidDownloadProgress = this._register(new Emitter()); + readonly onDidDownloadProgress: Event = this._onDidDownloadProgress.event; + /** * In-flight downloads keyed by the destination `cacheDir` (which * already encodes `//`). Concurrent @@ -180,7 +262,9 @@ export class AgentSdkDownloader implements IAgentSdkDownloader { @IRequestService private readonly _requestService: IRequestService, @IFileService private readonly _fileService: IFileService, @ILogService private readonly _logService: ILogService, - ) { } + ) { + super(); + } isAvailable(pkg: IAgentSdkPackage): boolean { if (process.env[pkg.devOverrideEnvVar]) { @@ -189,6 +273,22 @@ export class AgentSdkDownloader implements IAgentSdkDownloader { return !!this._productService.agentSdks?.[pkg.id] && resolveSdkTarget(pkg) !== undefined; } + async isSdkResolvableWithoutDownload(pkg: IAgentSdkPackage): Promise { + if (process.env[pkg.devOverrideEnvVar]) { + return true; + } + const config = this._productService.agentSdks?.[pkg.id]; + if (!config) { + return false; + } + const sdkTarget = resolveSdkTarget(pkg); + if (!sdkTarget) { + return false; + } + const sentinel = URI.joinPath(URI.file(this._cacheDir(pkg.id, config.version, sdkTarget)), '.complete'); + return this._fileService.exists(sentinel); + } + async loadSdkRoot(pkg: IAgentSdkPackage, token: CancellationToken): Promise { // 1. Dev override. const override = process.env[pkg.devOverrideEnvVar]; @@ -310,9 +410,21 @@ export class AgentSdkDownloader implements IAgentSdkDownloader { await this._delIgnoringMissing(tmpDirUri); await this._fileService.createFolder(tmpDirUri); + // Fire the download lifecycle on the process-global event so a single + // subscriber (the protocol server) can forward it to clients. One + // `started`, throttled `progress` from `_fetch`, then a terminal frame. + const downloadId = generateUuid(); + let lastReceived = 0; + let lastTotal: number | undefined; + this._fireProgress(pkg, downloadId, 'started', 0, undefined); + try { const tarballPath = path.join(tmpDir, 'sdk.tgz'); - await this._fetch(url, tarballPath, token); + await this._fetch(url, tarballPath, token, (receivedBytes, totalBytes) => { + lastReceived = receivedBytes; + lastTotal = totalBytes; + this._fireProgress(pkg, downloadId, 'progress', receivedBytes, totalBytes); + }); await this._extractTarGz(tarballPath, tmpDir); await this._fileService.del(URI.file(tarballPath)); @@ -334,6 +446,7 @@ export class AgentSdkDownloader implements IAgentSdkDownloader { } catch (err) { if (await this._handleRenameLoser(err, sentinel, tmpDirUri)) { this._logService.info(`[AgentSdkDownloader] ${pkg.id}: lost rename race, using existing cache`); + this._fireProgress(pkg, downloadId, 'completed', lastReceived, lastTotal); return cacheDir; } throw err; @@ -341,21 +454,44 @@ export class AgentSdkDownloader implements IAgentSdkDownloader { const elapsed = Math.round((Date.now() - start) / 1000); this._logService.info(`[AgentSdkDownloader] ${pkg.id}: downloaded in ${elapsed}s`); + this._fireProgress(pkg, downloadId, 'completed', lastTotal ?? lastReceived, lastTotal); return cacheDir; } catch (err) { await this._delIgnoringMissing(tmpDirUri); if (token.isCancellationRequested) { + this._fireProgress(pkg, downloadId, 'failed', lastReceived, lastTotal, 'cancelled'); throw new CancellationError(); } + const message = err instanceof Error ? err.message : String(err); + this._fireProgress(pkg, downloadId, 'failed', lastReceived, lastTotal, message); throw new Error( `Failed to download ${pkg.id} SDK from ${url} ` + `(cache target: ${cacheDir}). ` + `Set ${pkg.devOverrideEnvVar} to a local SDK root to bypass. ` + - `Cause: ${err instanceof Error ? err.message : String(err)}`, + `Cause: ${message}`, ); } } + private _fireProgress( + pkg: IAgentSdkPackage, + downloadId: string, + phase: AgentSdkDownloadPhase, + receivedBytes: number, + totalBytes: number | undefined, + error?: string, + ): void { + this._onDidDownloadProgress.fire({ + downloadId, + packageId: pkg.id, + displayName: pkg.displayName, + phase, + receivedBytes, + totalBytes, + ...(error !== undefined ? { error } : {}), + }); + } + private async _handleRenameLoser( err: unknown, sentinel: URI, @@ -375,7 +511,12 @@ export class AgentSdkDownloader implements IAgentSdkDownloader { return true; } - private async _fetch(url: string, dest: string, token: CancellationToken): Promise { + private async _fetch( + url: string, + dest: string, + token: CancellationToken, + onBytes?: (receivedBytes: number, totalBytes: number | undefined) => void, + ): Promise { // Delegate to IRequestService (corporate proxy, strictSSL, kerberos, // retries, redirect follow). `fs.createWriteStream` (not // `IFileService.writeFile`) so that cancelling a multi-MB download @@ -401,9 +542,31 @@ export class AgentSdkDownloader implements IAgentSdkDownloader { throw new Error(`HTTP ${statusCode} fetching ${url}`); } + // The CDN sends `Content-Length` for these static tarballs, which lets + // us report determinate percentage progress. A missing/garbled header + // degrades gracefully to an indeterminate (byte-count only) report. + const totalBytes = parseContentLength(context.res.headers['content-length']); + await new Promise((resolve, reject) => { const out = fs.createWriteStream(dest); let settled = false; + // Throttle progress so a fast link doesn't fire thousands of + // samples. The first chunk always passes (lastEmit starts at 0) + // and 'end' forces a final sample, so consumers see a start and a + // 100% finish regardless of chunk timing. + let receivedBytes = 0; + let lastEmitTime = 0; + const emitBytes = (force: boolean) => { + if (!onBytes) { + return; + } + const now = Date.now(); + if (!force && now - lastEmitTime < PROGRESS_EMIT_THROTTLE_MS) { + return; + } + lastEmitTime = now; + onBytes(receivedBytes, totalBytes); + }; const settleResolve = () => { if (settled) { return; } settled = true; @@ -428,11 +591,16 @@ export class AgentSdkDownloader implements IAgentSdkDownloader { // resume on 'drain'. out.on('drain', () => context.stream.resume()); context.stream.on('data', chunk => { + receivedBytes += chunk.byteLength; + emitBytes(false); if (!out.write(chunk.buffer)) { context.stream.pause(); } }); - context.stream.on('end', () => out.end()); + context.stream.on('end', () => { + emitBytes(true); + out.end(); + }); context.stream.on('error', settleReject); }); } diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 7cfca6949d2caf..dc9355148a92a6 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -116,6 +116,17 @@ export class AgentService extends Disposable implements IAgentService { private readonly _providers = new Map(); /** Maps each active session URI (toString) to its owning provider. */ private readonly _sessionToProvider = new Map(); + /** + * Pending download-progress tokens, keyed by provider id then session URI + * (toString). A session is registered here when its `createSession` carries + * a {@link IAgentCreateSessionConfig.progressToken} and removed once it + * materializes (the SDK is now resolved) or is disposed. The SDK download is + * host-level and shared across every session of a provider, and its progress + * events are keyed by package id (which equals the provider id) — so on a + * download frame we fan it out as one `progress` notification per waiting + * session's token. See {@link emitDownloadProgress}. + */ + private readonly _downloadProgressTokens = new Map>(); /** Subscriptions to provider progress events; cleared when providers change. */ private readonly _providerSubscriptions = this._register(new DisposableStore()); private readonly _authService: AgentHostAuthenticationService; @@ -599,6 +610,17 @@ export class AgentService extends Disposable implements IAgentService { this._logService.trace(`[AgentService] createSession: provider=${provider.id} model=${config?.model?.id ?? '(default)'}`); this._sessionToProvider.set(session.toString(), provider.id); + // Register the client's opt-in progress token so a cold SDK download + // triggered at materialization (first message) reports back to the + // `createSession` call that requested it. Cleared on materialize/dispose. + if (config?.progressToken) { + let bySession = this._downloadProgressTokens.get(provider.id); + if (!bySession) { + bySession = new Map(); + this._downloadProgressTokens.set(provider.id, bySession); + } + bySession.set(session.toString(), config.progressToken); + } this._logService.trace(`[AgentService] createSession returned: ${session.toString()}`); // Resolve config and seed the initial customization set in parallel so @@ -814,6 +836,9 @@ export class AgentService extends Disposable implements IAgentService { */ private _onDidMaterializeSession(e: IAgentMaterializeSessionEvent): void { const sessionKey = e.session.toString(); + // The session is now materialized — its SDK is resolved (any cold + // download already finished), so no further progress is expected for it. + this._clearDownloadProgressToken(sessionKey); const state = this._stateManager.getSessionState(sessionKey); if (!state) { this._logService.warn(`[AgentService] onDidMaterializeSession for unknown session: ${sessionKey}`); @@ -848,6 +873,49 @@ export class AgentService extends Disposable implements IAgentService { this._changesetCoordinator.onSessionMaterialized(sessionKey); } + /** Remove a session's pending download-progress token, if any. */ + private _clearDownloadProgressToken(sessionKey: string): void { + for (const [provider, bySession] of this._downloadProgressTokens) { + if (bySession.delete(sessionKey) && bySession.size === 0) { + this._downloadProgressTokens.delete(provider); + } + } + } + + /** + * Fan a host-level SDK download-progress sample out to the clients waiting + * on it. The downloader fires process-global frames keyed by package id + * (which equals the provider id); we emit one generic `progress` + * notification per session of that provider that supplied a + * {@link IAgentCreateSessionConfig.progressToken} on `createSession`, so the + * client can correlate it back to that call. A terminal frame reports + * `total === progress` (using `receivedBytes` when the size was never known) + * so clients dismiss the indicator, and clears the provider's tokens. + * + * `displayName` is the provider's brand noun (e.g. `Claude`). It is woven + * into the notification's localized, human-readable `message` (e.g. + * "Downloading Claude agent…") so a generic client can render the indicator + * verbatim without knowing the resource is an agent SDK. + */ + emitDownloadProgress(packageId: string, displayName: string, receivedBytes: number, totalBytes: number | undefined, terminal: boolean): void { + const bySession = this._downloadProgressTokens.get(packageId); + if (!bySession || bySession.size === 0) { + return; + } + // On a terminal frame force `progress === total` so clients treat the + // operation as complete (covers both the determinate case and the + // indeterminate one where `totalBytes` was never known, plus failures — + // the real error surfaces via the session-failure path). + const total = terminal ? receivedBytes : totalBytes; + const message = localize('agentHost.download.agentSdkTitle', "Downloading {0} agent…", displayName); + for (const progressToken of new Set(bySession.values())) { + this._stateManager.emitProgress({ progressToken, progress: receivedBytes, total, message }); + } + if (terminal) { + this._downloadProgressTokens.delete(packageId); + } + } + /** * Fire-and-forget probe that resolves the session's git state for its * working directory (if any) and merges it into `state._meta.git` via @@ -966,6 +1034,7 @@ export class AgentService extends Disposable implements IAgentService { if (provider) { await provider.disposeSession(session); this._sessionToProvider.delete(session.toString()); + this._clearDownloadProgressToken(session.toString()); } this._changesetCoordinator.onSessionDisposed(session.toString()); this._sideEffects.cancelSessionTitleGeneration(session.toString()); @@ -2211,6 +2280,7 @@ export class AgentService extends Disposable implements IAgentService { } await Promise.all(promises); this._sessionToProvider.clear(); + this._downloadProgressTokens.clear(); } // ---- helpers ------------------------------------------------------------ diff --git a/src/vs/platform/agentHost/node/claude/claudeAgent.ts b/src/vs/platform/agentHost/node/claude/claudeAgent.ts index 2efcd916ee72ed..cbbdb69889cb97 100644 --- a/src/vs/platform/agentHost/node/claude/claudeAgent.ts +++ b/src/vs/platform/agentHost/node/claude/claudeAgent.ts @@ -900,6 +900,15 @@ export class ClaudeAgent extends Disposable implements IAgent { // sibling Copilot provider gets nuked too. Catch and log instead. let sdkEntries: readonly SDKSessionInfo[]; try { + // Don't trigger a cold SDK download just to populate the session + // list at startup. When the SDK isn't local yet, surface an empty + // list; the download fires (with host-level progress) once the user + // starts a session, and the next `listSessions` — driven by the + // renderer's post-turn refresh — returns the full list. + if (!(await this._sdkService.canLoadWithoutDownload())) { + this._logService.info('[Claude] SDK not downloaded yet; deferring session list until a session triggers the download'); + return []; + } sdkEntries = await this._sdkService.listSessions(); } catch (err) { this._logService.warn('[Claude] SDK listSessions failed; surfacing empty list', err); diff --git a/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts b/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts index 5846daa8265d66..0955bf4e60e2f1 100644 --- a/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts +++ b/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts @@ -22,6 +22,7 @@ import { AgentHostClaudeSdkRootEnvVar } from '../../common/agentService.js'; */ export const ClaudeSdkPackage: IAgentSdkPackage = { id: 'claude', + displayName: 'Claude', devOverrideEnvVar: AgentHostClaudeSdkRootEnvVar, hasSeparateMuslLinuxPackage: true, }; @@ -54,6 +55,16 @@ export interface IClaudeAgentSdkService { getSessionMessages(sessionId: string, options?: GetSessionMessagesOptions): Promise; listSubagents(sessionId: string, options?: ListSubagentsOptions): Promise; getSubagentMessages(sessionId: string, agentId: string, options?: GetSubagentMessagesOptions): Promise; + + /** + * True iff the SDK can be loaded WITHOUT a network download — a dev + * override or dev bare-import is available, or a previously-downloaded SDK + * is cached on disk. Eager / background callers (e.g. `listSessions` at + * startup) gate on this so listing sessions never kicks off a multi-second + * cold download before the user has started a session. + */ + canLoadWithoutDownload(): Promise; + forkSession(sessionId: string, options?: ForkSessionOptions): Promise; createSdkMcpServer(options: { name: string; @@ -132,6 +143,17 @@ export class ClaudeAgentSdkService implements IClaudeAgentSdkService { return sdk.listSessions(undefined); } + async canLoadWithoutDownload(): Promise { + // A dev override (explicit SDK root) is always local. So is the dev + // bare-import path, which is taken when there is no product config — + // `isAvailable` is false exactly in that case. Otherwise the SDK comes + // from the downloader, which is only local once it has been cached. + if (process.env[AgentHostClaudeSdkRootEnvVar] || !this._downloader.isAvailable(ClaudeSdkPackage)) { + return true; + } + return this._downloader.isSdkResolvableWithoutDownload(ClaudeSdkPackage); + } + async getSessionInfo(sessionId: string): Promise { const sdk = await this._getSdk(); return sdk.getSessionInfo(sessionId); diff --git a/src/vs/platform/agentHost/node/codex/codexAgent.ts b/src/vs/platform/agentHost/node/codex/codexAgent.ts index 6b62c179afb36c..9833f8489b8b08 100644 --- a/src/vs/platform/agentHost/node/codex/codexAgent.ts +++ b/src/vs/platform/agentHost/node/codex/codexAgent.ts @@ -449,6 +449,7 @@ interface IConnectionReady { */ export const CodexSdkPackage: IAgentSdkPackage = { id: 'codex', + displayName: 'Codex', devOverrideEnvVar: AgentHostCodexAgentSdkRootEnvVar, hasSeparateMuslLinuxPackage: false, }; @@ -1774,7 +1775,16 @@ export class CodexAgent extends Disposable implements IAgent { if (!session.workingDirectory) { return; } - void this._materializeIfNeeded(session, false).then(() => { + void (async () => { + // Prewarm is a background latency optimization, not a user action, + // so it must NOT trigger a cold SDK download. When the SDK isn't + // local yet, skip prewarm; the first `sendMessage` materializes the + // thread and fires the (host-level progress-reported) download then. + if (!(await this._agentSdkDownloader.isSdkResolvableWithoutDownload(CodexSdkPackage))) { + this._logService.info(`[Codex] SDK not downloaded yet; skipping prewarm for session=${session.sessionUri.toString()} until a message triggers the download`); + return; + } + await this._materializeIfNeeded(session, false); if (session.prewarmClaimed || session.threadId === undefined) { return; } @@ -1783,7 +1793,7 @@ export class CodexAgent extends Disposable implements IAgent { void this._expirePrewarm(session); }, CodexPrewarmTtlMs); session.prewarmTimer = prewarmTimer; - }).catch(err => { + })().catch(err => { this._logService.warn(`[Codex] prewarm failed session=${session.sessionUri.toString()}: ${err instanceof Error ? err.message : String(err)}`); }); } @@ -2251,6 +2261,15 @@ export class CodexAgent extends Disposable implements IAgent { if (!this._githubToken) { return []; } + // Don't connect (and trigger a cold SDK download) just to list threads + // at startup. When the SDK isn't local yet, surface an empty list; the + // download fires (with host-level progress) once the user starts a + // session, and the next `listSessions` — driven by the renderer's + // post-turn refresh — returns the full list. + if (!(await this._agentSdkDownloader.isSdkResolvableWithoutDownload(CodexSdkPackage))) { + this._logService.info('[Codex] SDK not downloaded yet; deferring thread/list until a session triggers the download'); + return []; + } try { const conn = await this._ensureConnection(); const response = await conn.client.request<'thread/list', ThreadListResponse>('thread/list', { diff --git a/src/vs/platform/agentHost/node/protocolServerHandler.ts b/src/vs/platform/agentHost/node/protocolServerHandler.ts index 318a27b5a8a610..ab418cb98dd7b6 100644 --- a/src/vs/platform/agentHost/node/protocolServerHandler.ts +++ b/src/vs/platform/agentHost/node/protocolServerHandler.ts @@ -1140,6 +1140,7 @@ export class ProtocolServerHandler extends Disposable { fork, config: params.config, activeClient: params.activeClient, + progressToken: params.progressToken, }); } catch (err) { if (err instanceof ProtocolError) { diff --git a/src/vs/platform/agentHost/test/node/agentSdkDownloader.test.ts b/src/vs/platform/agentHost/test/node/agentSdkDownloader.test.ts index e41f93800ce09d..6daa264734debf 100644 --- a/src/vs/platform/agentHost/test/node/agentSdkDownloader.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSdkDownloader.test.ts @@ -19,7 +19,7 @@ import type { IFileService } from '../../../files/common/files.js'; import { DiskFileSystemProvider } from '../../../files/node/diskFileSystemProvider.js'; import { NullLogService } from '../../../log/common/log.js'; import { RequestService } from '../../../request/node/requestService.js'; -import { AgentSdkDownloader, resolveSdkTarget, type IAgentSdkPackage } from '../../node/agentSdkDownloader.js'; +import { AgentSdkDownloader, resolveSdkTarget, type IAgentSdkPackage, type IAgentSdkDownloadProgress } from '../../node/agentSdkDownloader.js'; import { ClaudeSdkPackage } from '../../node/claude/claudeAgentSdkService.js'; import { AgentHostClaudeSdkRootEnvVar } from '../../common/agentService.js'; import type { INativeEnvironmentService } from '../../../environment/common/environment.js'; @@ -128,7 +128,7 @@ suite('resolveSdkTarget', () => { ensureNoDisposablesAreLeakedInTestSuite(); function fakePkg(hasSeparateMuslLinuxPackage: boolean): IAgentSdkPackage { - return { id: 'test', devOverrideEnvVar: 'X', hasSeparateMuslLinuxPackage }; + return { id: 'test', displayName: 'Test', devOverrideEnvVar: 'X', hasSeparateMuslLinuxPackage }; } test('returns - for supported (platform, arch)', () => { @@ -239,13 +239,13 @@ suite('AgentSdkDownloader', () => { version: productConfig?.version ?? '1.0.0', urlTemplate: productConfig?.urlTemplate ?? `http://127.0.0.1:${server.port}/sdk-{sdkTarget}.tgz`, }; - return new AgentSdkDownloader( + return disposables.add(new AgentSdkDownloader( makeEnvService(userDataPath), makeProductService(config), makeRequestService(disposables), makeFileService(disposables), new NullLogService(), - ); + )); } test('isAvailable: false when no env override and no product config', () => { @@ -280,6 +280,32 @@ suite('AgentSdkDownloader', () => { assert.ok(fs.existsSync(path.join(root, '.complete'))); }); + test('loadSdkRoot: reports monotonic download progress ending at totalBytes', async () => { + const downloader = makeDownloader(); + const samples: IAgentSdkDownloadProgress[] = []; + disposables.add(downloader.onDidDownloadProgress(p => samples.push(p))); + + await downloader.loadSdkRoot(ClaudeSdkPackage, newToken()); + + const tarballSize = (await fsp.stat(fixture.tarballPath)).size; + // One `started`, ≥1 `progress`, one terminal `completed`, all sharing a + // single downloadId and carrying the brand display name. + assert.ok(samples.length >= 2, 'expected at least a started and a completed frame'); + assert.strictEqual(samples[0].phase, 'started'); + const completed = samples[samples.length - 1]; + assert.strictEqual(completed.phase, 'completed'); + assert.ok(samples.every(s => s.downloadId === samples[0].downloadId), 'all frames share one downloadId'); + assert.ok(samples.every(s => s.displayName === 'Claude'), 'all frames carry the brand display name'); + + // receivedBytes is monotonically non-decreasing and reaches the total + // reported via Content-Length. + for (let i = 1; i < samples.length; i++) { + assert.ok(samples[i].receivedBytes >= samples[i - 1].receivedBytes, 'receivedBytes must be monotonic'); + } + assert.strictEqual(completed.totalBytes, tarballSize); + assert.strictEqual(completed.receivedBytes, tarballSize); + }); + test('loadSdkRoot: cache hit returns immediately without re-downloading', async () => { const downloader = makeDownloader(); await downloader.loadSdkRoot(ClaudeSdkPackage, newToken()); diff --git a/src/vs/platform/agentHost/test/node/claudeAgent.integrationTest.ts b/src/vs/platform/agentHost/test/node/claudeAgent.integrationTest.ts index 46d9aec9fc1b2c..9cd445526eb338 100644 --- a/src/vs/platform/agentHost/test/node/claudeAgent.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/claudeAgent.integrationTest.ts @@ -356,6 +356,10 @@ class ProxyRoundTripSdkService implements IClaudeAgentSdkService { return []; } + async canLoadWithoutDownload(): Promise { + return true; + } + async getSessionInfo(_sessionId: string): Promise { return undefined; } diff --git a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts index 8c685bdec85429..e70dc30cfb3bec 100644 --- a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts @@ -228,6 +228,10 @@ class FakeClaudeAgentSdkService implements IClaudeAgentSdkService { return this.sessionList; } + async canLoadWithoutDownload(): Promise { + return true; + } + /** * Fake for {@link IClaudeAgentSdkService.getSessionInfo}. Tests stage * `sessionList` and the fake searches it by id; setting @@ -763,7 +767,9 @@ function forkSourceMessages(sourceId: string): SessionMessage[] { function stubAgentSdkDownloader(): IAgentSdkDownloader { return { _serviceBrand: undefined, + onDidDownloadProgress: Event.None, isAvailable: () => false, + isSdkResolvableWithoutDownload: async () => false, loadSdkRoot: () => { throw new Error('test stub: downloader.loadSdkRoot should not be called'); }, }; } diff --git a/src/vs/platform/agentHost/test/node/claudeSubagentResolver.test.ts b/src/vs/platform/agentHost/test/node/claudeSubagentResolver.test.ts index ee3986de42c5c1..ea9d3aa751dc16 100644 --- a/src/vs/platform/agentHost/test/node/claudeSubagentResolver.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeSubagentResolver.test.ts @@ -42,6 +42,7 @@ class FakeSdkService implements IClaudeAgentSdkService { getSubagentMessagesCalls: { sessionId: string; agentId: string }[] = []; async listSessions(): Promise { return []; } + async canLoadWithoutDownload(): Promise { return true; } async getSessionInfo(_id: string): Promise { return undefined; } async startup(_p: { options: Options; initializeTimeoutMs?: number }): Promise { throw new Error('not used'); } async query(_params: { prompt: string | AsyncIterable; options?: Options }): Promise { throw new Error('not used'); } diff --git a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts index f0d2f17a0eb214..571d3cea8d6e11 100644 --- a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts +++ b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts @@ -13,7 +13,7 @@ import { NullLogService } from '../../../log/common/log.js'; import { FileType } from '../../../files/common/files.js'; import { type IAgentCreateSessionConfig, type IAgentResolveSessionConfigParams, type IAgentService, type IAgentSessionConfigCompletionsParams, type IAgentSessionMetadata, type AuthenticateParams, type AuthenticateResult } from '../../common/agentService.js'; import { CompletionsParams, CompletionsResult, ContentEncoding, ListSessionsResult, ResourceReadResult, ResolveSessionConfigResult, SessionConfigCompletionsResult, ResourceMkdirParams, ResourceMkdirResult, ResourceResolveParams, ResourceResolveResult, ResourceCopyParams, ResourceCopyResult } from '../../common/state/protocol/commands.js'; -import { ActionType, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type ClientAnnotationsAction } from '../../common/state/sessionActions.js'; +import { ActionType, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type ClientAnnotationsAction, type ProgressParams } from '../../common/state/sessionActions.js'; import { PROTOCOL_VERSION } from '../../common/state/protocol/version/registry.js'; import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, JSON_RPC_INTERNAL_ERROR, ProtocolError, AhpErrorCodes, AHP_UNSUPPORTED_PROTOCOL_VERSION, AHP_SESSION_NOT_FOUND, type AhpNotification, type InitializeResult, type ProtocolMessage, type ReconnectResult, type ResourceListResult, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../../common/state/sessionProtocol.js'; import { MessageKind, ResponsePartKind, SessionStatus, ChangesetStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildChatUri, buildDefaultChatUri, type SessionSummary } from '../../common/state/sessionState.js'; @@ -1988,6 +1988,70 @@ suite('ProtocolServerHandler', () => { }); }); + suite('download progress channel', () => { + // Progress is emitted on the state manager (so it reaches both local + // IPC and remote WebSocket renderers through the same path as session + // notifications). This suite verifies the handler forwards each frame to + // connected clients as a `progress` notification on the root channel. + // Spun up per-test with a private state manager so the outer suite is + // unaffected. + let dlStateManager: AgentHostStateManager; + let dlServer: MockProtocolServer; + let dlAgentService: MockAgentService; + let localDisposables: DisposableStore; + + setup(() => { + localDisposables = new DisposableStore(); + dlStateManager = localDisposables.add(new AgentHostStateManager(new NullLogService())); + dlServer = localDisposables.add(new MockProtocolServer()); + dlAgentService = new MockAgentService(); + dlAgentService.setStateManager(dlStateManager); + localDisposables.add(dlAgentService); + localDisposables.add(new ProtocolServerHandler( + dlAgentService, + dlStateManager, + dlServer, + { defaultDirectory: URI.file('/home/testuser').toString() }, + localDisposables.add(new AgentHostFileSystemProvider()), + new NullLogService(), + )); + }); + + teardown(() => { + localDisposables.dispose(); + }); + + function connectDownloadClient(clientId: string): MockProtocolTransport { + const transport = new MockProtocolTransport(); + dlServer.simulateConnection(transport); + transport.simulateMessage(request(1, 'initialize', { + protocolVersions: [PROTOCOL_VERSION], + clientId, + })); + return transport; + } + + function findProgress(sent: ProtocolMessage[]): ProgressParams[] { + return sent + .filter(isJsonRpcNotification) + .filter((m): m is AhpNotification & { method: 'root/progress'; params: ProgressParams } => m.method === 'root/progress') + .map(m => m.params); + } + + test('forwards each progress frame to connected clients on the root channel', () => { + const transport = connectDownloadClient('client-dl-1'); + + dlStateManager.emitProgress({ progressToken: 't1', progress: 0, total: 1000, message: 'Claude' }); + dlStateManager.emitProgress({ progressToken: 't1', progress: 500, total: 1000, message: 'Claude' }); + dlStateManager.emitProgress({ progressToken: 't1', progress: 1000, total: 1000, message: 'Claude' }); + + const frames = findProgress(transport.sent); + assert.deepStrictEqual(frames.map(f => f.progress), [0, 500, 1000]); + assert.ok(frames.every(f => f.progressToken === 't1' && f.message === 'Claude' && f.total === 1000)); + assert.ok(frames.every(f => (f as ProgressParams & { channel: string }).channel === 'ahp-root://'), 'frames are broadcast on the root channel'); + }); + }); + suite('resource watches', () => { test('subscribe to a resource-watch channel returns the descriptor + bumps refcount; envelopes are routed', async () => { diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts index 2511e859011d71..d4bb83800d200d 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { disposableTimeout, raceTimeout } from '../../../../../base/common/async.js'; +import { disposableTimeout, DeferredPromise, raceTimeout } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { arrayEquals, structuralEquals } from '../../../../../base/common/equals.js'; @@ -27,13 +27,14 @@ import { migrateLegacyAutopilotConfig } from '../../../../../platform/agentHost/ import type { IAgentSubscription } from '../../../../../platform/agentHost/common/state/agentSubscription.js'; import { ResolveSessionConfigResult } from '../../../../../platform/agentHost/common/state/protocol/commands.js'; import { AgentCustomization, AgentSelection, ChangesSummary, type ChangesetFile, type ClientPluginCustomization, Customization, CustomizationType, ModelSelection, SessionStatus as ProtocolSessionStatus, RootConfigState, RootState, SessionActiveClient, SessionState, SessionSummary, type Changeset } from '../../../../../platform/agentHost/common/state/protocol/state.js'; -import { ActionType, isChatAction, isSessionAction, NotificationType } from '../../../../../platform/agentHost/common/state/sessionActions.js'; +import { ActionType, isChatAction, isSessionAction, NotificationType, type ProgressParams } from '../../../../../platform/agentHost/common/state/sessionActions.js'; import { AgentInfo, buildChatUri, buildDefaultChatUri, isDefaultChatUri, parseChatUri, readSessionGitHubState, readSessionGitState, ROOT_STATE_URI, SessionMeta, SessionSummaryMeta, StateComponents, type ChatSummary, type ISessionGitState } from '../../../../../platform/agentHost/common/state/sessionState.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IProgressService, IProgressStep, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js'; import { IAgentHostActiveClientService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.js'; import { IChatWidgetService } from '../../../../../workbench/contrib/chat/browser/chat.js'; @@ -1264,6 +1265,12 @@ class NewSession extends Disposable { session: backendUri, workingDirectory: this.workspaceUri, config: this._config?.values, + // MCP-style opt-in: offer to receive `progress` for any + // long-running bring-up (chiefly the lazy first-use SDK + // download, which fires later at first-message + // materialization). The host echoes this token on each + // `progress` frame so `_handleProgress` can correlate it. + progressToken: generateUuid(), ...(this._selectedAgent ? { agent: { uri: this._selectedAgent.uri } } : {}), ...(this._initialActiveClient ? { activeClient: this._initialActiveClient } : {}), }); @@ -1381,6 +1388,19 @@ class NewSession extends Disposable { * URI-scheme mapping for session metadata, the agent-provider lookup, and * the browse UI. */ +/** + * One in-flight download, tracked by + * {@link BaseAgentHostSessionsProvider._activeDownloads}. Owns the lifecycle + * of a single notification progress: `report` pushes a step, `complete` + * resolves the backing deferred so the notification is dismissed. + */ +interface IActiveDownload { + /** Last reported determinate percentage, used to compute progress increments. */ + lastPercent: number; + report(step: IProgressStep): void; + complete(): void; +} + export abstract class BaseAgentHostSessionsProvider extends Disposable implements IAgentHostSessionsProvider { abstract readonly id: string; @@ -1432,6 +1452,15 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement /** Cache of adapted sessions, keyed by raw session ID. */ protected readonly _sessionCache = new Map(); + /** + * Active progress indicators keyed by `progressToken`. Each entry owns one + * long-running notification progress (opened on the first frame) that is + * driven via {@link IActiveDownload.report} and dismissed via + * {@link IActiveDownload.complete} once `progress >= total`. See + * {@link _handleProgress}. + */ + private readonly _activeDownloads = new Map(); + /** * Temporary session that has been sent (first turn dispatched) but not yet * committed by the backend session list. Shown in the session list until the @@ -1535,6 +1564,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement @IStorageService protected readonly _storageService: IStorageService, @IDialogService protected readonly _dialogService: IDialogService, @IWorkspaceTrustManagementService protected readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IProgressService protected readonly _progressService: IProgressService, ) { super(); this._register(toDisposable(() => { @@ -1543,6 +1573,12 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement } this._sessionCache.clear(); })); + this._register(toDisposable(() => { + for (const download of this._activeDownloads.values()) { + download.complete(); + } + this._activeDownloads.clear(); + })); } // -- Subclass hooks ------------------------------------------------------- @@ -3213,6 +3249,8 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement this._handleSessionRemoved(n.session); } else if (n.type === NotificationType.SessionSummaryChanged) { this._handleSessionSummaryChanged(n.session, n.changes); + } else if (n.type === NotificationType.Progress) { + this._handleProgress(n); } })); @@ -3381,6 +3419,81 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement }); } + /** + * Render a generic `progress` notification as a notification progress bar. + * Progress is correlated to its originating `createSession` call by + * {@link ProgressParams.progressToken}; today the only producer is the + * agent host's lazy, first-use SDK download (shared across a provider's + * sessions). We surface one notification per `progressToken`: determinate + * when the host knows the `total` (`Content-Length`), or a byte-count spinner + * otherwise. The operation is complete — and the notification dismissed — + * once `progress >= total`. The human-readable brand noun rides on + * {@link ProgressParams.message}. + */ + private _handleProgress(progress: ProgressParams): void { + // New AI UI must stay hidden when the user has turned AI features off. + if (this._baseConfigurationService.getValue(ChatConfiguration.AIDisabled)) { + return; + } + + // Complete when we reach the (possibly server-synthesized) total. The + // host emits a terminal frame with `progress === total` for success, + // indeterminate completion, and failure alike; real errors surface via + // the session-failure path, not here. + const isComplete = progress.total !== undefined && progress.progress >= progress.total; + if (isComplete) { + this._activeDownloads.get(progress.progressToken)?.complete(); + this._activeDownloads.delete(progress.progressToken); + return; + } + + let entry = this._activeDownloads.get(progress.progressToken); + if (!entry) { + // First frame for this token (or one we joined late): open one + // long-running notification progress and drive it via `report` + // until a terminal frame resolves `deferred`. + const deferred = new DeferredPromise(); + let report: ((step: IProgressStep) => void) | undefined; + // `message` is the host-supplied, already-localized human-readable + // title (e.g. "Downloading Claude agent…"). Render it verbatim so + // this stays a generic progress indicator that makes no assumption + // about what is being downloaded; fall back only if a producer omits it. + const title = progress.message ?? localize('agentHost.download.titleFallback', "Downloading…"); + this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title, + }, + p => { + report = step => p.report(step); + return deferred.p; + }, + ); + entry = { + lastPercent: 0, + report: step => report?.(step), + complete: () => deferred.complete(), + }; + this._activeDownloads.set(progress.progressToken, entry); + } + + if (progress.total && progress.total > 0) { + const percent = Math.max(0, Math.min(100, Math.round((progress.progress / progress.total) * 100))); + const increment = percent - entry.lastPercent; + entry.lastPercent = percent; + entry.report({ + message: localize('agentHost.download.percent', "{0}%", percent), + increment: increment > 0 ? increment : 0, + total: 100, + }); + } else { + // No total: indeterminate. Show megabytes received so the user + // still sees the download making progress. + const megabytes = (progress.progress / (1024 * 1024)).toFixed(1); + entry.report({ message: localize('agentHost.download.megabytes', "{0} MB", megabytes) }); + } + } + private _handleConfigChanged(session: string, config: Record, replace: boolean): void { const rawId = AgentSession.id(session); const cached = this._sessionCache.get(rawId); diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts index 8e4ecd200a8dc2..273fff0f1d13c9 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts @@ -18,6 +18,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IStorageService } from '../../../../../platform/storage/common/storage.js'; +import { IProgressService } from '../../../../../platform/progress/common/progress.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js'; import { IAgentHostActiveClientService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.js'; @@ -87,8 +88,9 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide @IDialogService dialogService: IDialogService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IProgressService progressService: IProgressService, ) { - super(chatSessionsService, chatService, chatWidgetService, languageModelsService, _configurationService, logService, gitHubService, instantiationService, sessionsService, activeClientService, storageService, dialogService, workspaceTrustManagementService); + super(chatSessionsService, chatService, chatWidgetService, languageModelsService, _configurationService, logService, gitHubService, instantiationService, sessionsService, activeClientService, storageService, dialogService, workspaceTrustManagementService, progressService); this._isSessionsWindow = environmentService.isSessionsWindow; diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts index 327a1193904cde..10c24840015686 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts @@ -24,6 +24,7 @@ import { TestConfigurationService } from '../../../../../../platform/configurati import { IDialogService, IFileDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { InMemoryStorageService, IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { IProgressService } from '../../../../../../platform/progress/common/progress.js'; import { IWorkspaceTrustManagementService } from '../../../../../../platform/workspace/common/workspaceTrust.js'; import { IChatWidget, IChatWidgetService } from '../../../../../../workbench/contrib/chat/browser/chat.js'; import { IChatService, type ChatSendResult, type IChatSendRequestOptions } from '../../../../../../workbench/contrib/chat/common/chatService/chatService.js'; @@ -338,6 +339,7 @@ function createProvider(disposables: DisposableStore, agentHostService: MockAgen }); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IStorageService, options?.storageService ?? disposables.add(new InMemoryStorageService())); + instantiationService.stub(IProgressService, {}); instantiationService.stub(IGitHubService, options?.gitHubService ?? new class extends mock() { override findPullRequestNumberByHeadBranch = async () => undefined; }()); diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts index bbade91785aa25..ede47257d83412 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts @@ -27,6 +27,7 @@ import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IProgressService } from '../../../../../platform/progress/common/progress.js'; import { IAgentHostActiveClientService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.js'; import { IChatWidgetService } from '../../../../../workbench/contrib/chat/browser/chat.js'; import { IChatService } from '../../../../../workbench/contrib/chat/common/chatService/chatService.js'; @@ -219,8 +220,9 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid @IAgentHostActiveClientService activeClientService: IAgentHostActiveClientService, @IDialogService dialogService: IDialogService, @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IProgressService progressService: IProgressService, ) { - super(chatSessionsService, chatService, chatWidgetService, languageModelsService, _configurationService, logService, gitHubService, instantiationService, sessionsService, activeClientService, storageService, dialogService, workspaceTrustManagementService); + super(chatSessionsService, chatService, chatWidgetService, languageModelsService, _configurationService, logService, gitHubService, instantiationService, sessionsService, activeClientService, storageService, dialogService, workspaceTrustManagementService, progressService); this._connectionAuthority = agentHostAuthority(config.address); this._connectOnDemand = config.connectOnDemand; diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts index aac28637641d7e..ed52776908243f 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts @@ -23,6 +23,7 @@ import { IDialogService, IFileDialogService } from '../../../../../../platform/d import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { INotificationService } from '../../../../../../platform/notification/common/notification.js'; import { InMemoryStorageService, IStorageService } from '../../../../../../platform/storage/common/storage.js'; +import { IProgressService } from '../../../../../../platform/progress/common/progress.js'; import { IWorkspaceTrustManagementService } from '../../../../../../platform/workspace/common/workspaceTrust.js'; import { IChatWidget, IChatWidgetService } from '../../../../../../workbench/contrib/chat/browser/chat.js'; import { IChatService, type ChatSendResult, type IChatSendRequestOptions } from '../../../../../../workbench/contrib/chat/common/chatService/chatService.js'; @@ -217,6 +218,7 @@ function createProvider(disposables: DisposableStore, connection: MockAgentConne lookupLanguageModel: () => undefined, }); instantiationService.stub(IStorageService, overrides?.storageService ?? disposables.add(new InMemoryStorageService())); + instantiationService.stub(IProgressService, {}); instantiationService.stub(ILabelService, { getUriLabel: (uri: URI) => uri.path, }); From e6ba4aaee851bc492f03a5f7abded4a443f0a682 Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci Date: Fri, 26 Jun 2026 19:05:02 +0200 Subject: [PATCH 015/589] use the right edit tool --- .../prompts/node/agent/kimiPrompts.tsx | 19 +++++--- .../node/agent/test/kimiPrompts.spec.tsx | 17 ++++++- .../endpoint/common/chatModelCapabilities.ts | 19 ++++++-- .../test/node/chatModelCapabilities.spec.ts | 47 ++++++++++++++++++- 4 files changed, 90 insertions(+), 12 deletions(-) diff --git a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx index e97392f5f5cfae..0ec7f4d0f5265c 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { PromptElement, PromptSizing } from '@vscode/prompt-tsx'; +import { isKimiFamily } from '../../../../platform/endpoint/common/chatModelCapabilities'; import { IChatEndpoint } from '../../../../platform/networking/common/networking'; import { agenticBrowserTools, ToolName } from '../../../tools/common/toolNames'; import { InstructionMessage } from '../base/instructionMessage'; @@ -15,11 +16,6 @@ import { ApplyPatchInstructions, CodesearchModeInstructions, DefaultAgentPromptP import { FileLinkificationInstructions } from './fileLinkificationInstructions'; import { IAgentPrompt, PromptRegistry, ReminderInstructionsConstructor, SystemPrompt } from './promptRegistry'; -function isKimiModel(model: string): boolean { - const normalized = model.toLowerCase(); - return normalized === 'kimi-k2.6' || normalized === 'kimi-k2.7-code'; -} - class KimiAgentPrompt extends PromptElement { async render(state: void, sizing: PromptSizing) { const tools = detectToolCapabilities(this.props.availableTools); @@ -86,6 +82,17 @@ class KimiAgentPrompt extends PromptElement { {this.props.codesearchMode && } + {tools[ToolName.ReplaceString] && !tools[ToolName.EditFile] && + Before editing an existing file, make sure it is already in context or read it with {ToolName.ReadFile}.
+ {tools[ToolName.MultiReplaceString] + ? <>Use {ToolName.ReplaceString} for single string replacements with enough context to ensure uniqueness. Prefer {ToolName.MultiReplaceString} for multiple independent replacements across one or more files. Do not announce which tool you're using.
+ : <>Use {ToolName.ReplaceString} to edit files. Include sufficient surrounding context so the replacement is unique. You can use this tool multiple times per file.
} + Group changes by file.
+ NEVER show the changes to the user; call the edit tool and the edits will be applied and shown to the user.
+ NEVER print a codeblock that represents a change to a file. Use {ToolName.ReplaceString}{tools[ToolName.MultiReplaceString] ? ` or ${ToolName.MultiReplaceString}` : ''} instead.
+ For each file, give a short description of what needs to be changed, then use the edit tool.
+
} + {tools[ToolName.EditFile] && !tools[ToolName.ApplyPatch] && {tools[ToolName.ReplaceString] ? <> @@ -131,7 +138,7 @@ class KimiPromptResolver implements IAgentPrompt { static readonly familyPrefixes = ['kimi-k2.6', 'kimi-k2.7-code']; static matchesModel(endpoint: IChatEndpoint): boolean { - return isKimiModel(endpoint.family) || isKimiModel(endpoint.model); + return isKimiFamily(endpoint) || isKimiFamily(endpoint.model); } resolveSystemPrompt(endpoint: IChatEndpoint): SystemPrompt | undefined { diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx b/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx index 315281fffa7f4a..288c8b59001731 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/test/kimiPrompts.spec.tsx @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Raw } from '@vscode/prompt-tsx'; +import type { LanguageModelToolInformation } from 'vscode'; import { afterAll, beforeAll, expect, suite, test } from 'vitest'; import { IChatMLFetcher } from '../../../../../platform/chat/common/chatMLFetcher'; import { StaticChatMLFetcher } from '../../../../../platform/chat/test/common/staticChatMLFetcher'; @@ -13,6 +14,7 @@ import { IResponseDelta } from '../../../../../platform/networking/common/fetch' import { ITestingServicesAccessor } from '../../../../../platform/test/node/services'; import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation'; import { createExtensionUnitTestingServices } from '../../../../test/node/services'; +import { ToolName } from '../../../../tools/common/toolNames'; import { IToolsService } from '../../../../tools/common/toolsService'; import { PromptRenderer } from '../../base/promptRenderer'; import '../allAgentPrompts'; @@ -32,12 +34,12 @@ suite('KimiPrompts', () => { accessor.dispose(); }); - async function renderSystemPrompt(family: string): Promise { + async function renderSystemPrompt(family: string, availableTools?: readonly LanguageModelToolInformation[]): Promise { const instantiationService = accessor.get(IInstantiationService); const endpoint = instantiationService.createInstance(MockEndpoint, family); const customizations = await PromptRegistry.resolveAllCustomizations(instantiationService, endpoint); const renderer = PromptRenderer.create(instantiationService, endpoint, customizations.SystemPrompt, { - availableTools: accessor.get(IToolsService).tools, + availableTools: availableTools ?? accessor.get(IToolsService).tools, modelFamily: family, codesearchMode: false, }); @@ -61,4 +63,15 @@ suite('KimiPrompts', () => { expect(renderedPrompt).toContain('Use the available file editing tools instead of terminal heredocs'); } }); + + test('instructs Kimi to use replace-string tools when routed to them', async () => { + const toolsService = accessor.get(IToolsService); + const availableTools = toolsService.tools.filter(tool => tool.name !== ToolName.EditFile && tool.name !== ToolName.ApplyPatch); + const renderedPrompt = await renderSystemPrompt('kimi-k2.7-code', availableTools); + + expect(renderedPrompt).toContain(`Use ${ToolName.ReplaceString} for single string replacements`); + expect(renderedPrompt).toContain(`Prefer ${ToolName.MultiReplaceString} for multiple independent replacements`); + expect(renderedPrompt).not.toContain(`Use ${ToolName.EditFile}`); + expect(renderedPrompt).not.toContain(`Use ${ToolName.ApplyPatch}`); + }); }); diff --git a/extensions/copilot/src/platform/endpoint/common/chatModelCapabilities.ts b/extensions/copilot/src/platform/endpoint/common/chatModelCapabilities.ts index fb725ff704d3ca..5bbaa8b9b03920 100644 --- a/extensions/copilot/src/platform/endpoint/common/chatModelCapabilities.ts +++ b/extensions/copilot/src/platform/endpoint/common/chatModelCapabilities.ts @@ -166,6 +166,19 @@ export function isGpt53Codex(model: LanguageModelChat | IChatEndpoint | string) return family.startsWith('gpt-5.3-codex'); } +export function isKimiFamily(model: LanguageModelChat | IChatEndpoint | string): boolean { + const matches = (value: string): boolean => { + const normalized = value.toLowerCase(); + return normalized.startsWith('kimi-k2.6') || normalized.startsWith('kimi-k2.7-code'); + }; + + if (typeof model === 'string') { + return matches(model); + } + + return matches(model.family) || matches(getModelId(model)); +} + export function isVSCModelA(model: LanguageModelChat | IChatEndpoint) { const ID_hash = getCachedSha256Hash(getModelId(model)); @@ -261,14 +274,14 @@ export function modelPrefersJsonNotebookRepresentation(model: LanguageModelChat * Model supports replace_string_in_file as an edit tool. */ export function modelSupportsReplaceString(model: LanguageModelChat | IChatEndpoint): boolean { - return isGeminiFamily(model) || model.family.includes('grok-code') || modelSupportsMultiReplaceString(model) || isHiddenModelF(model) || isMinimaxFamily(model) || isHiddenFamilyH(model); + return isGeminiFamily(model) || model.family.includes('grok-code') || modelSupportsMultiReplaceString(model) || isHiddenModelF(model) || isMinimaxFamily(model) || isHiddenFamilyH(model) || isKimiFamily(model); } /** * Model supports multi_replace_string_in_file as an edit tool. */ export function modelSupportsMultiReplaceString(model: LanguageModelChat | IChatEndpoint): boolean { - return isAnthropicFamily(model) || isHiddenModelE(model) || isVSCModelReplaceStringSet(model) || isMinimaxFamily(model) || isHiddenFamilyH(model); + return isAnthropicFamily(model) || isHiddenModelE(model) || isVSCModelReplaceStringSet(model) || isMinimaxFamily(model) || isHiddenFamilyH(model) || isKimiFamily(model); } /** @@ -276,7 +289,7 @@ export function modelSupportsMultiReplaceString(model: LanguageModelChat | IChat * without needing insert_edit_into_file. */ export function modelCanUseReplaceStringExclusively(model: LanguageModelChat | IChatEndpoint): boolean { - return isAnthropicFamily(model) || model.family.includes('grok-code') || isHiddenModelE(model) || model.family.toLowerCase().includes('gemini-3') || isVSCModelReplaceStringSet(model) || isHiddenModelF(model) || isMinimaxFamily(model) || isHiddenFamilyH(model); + return isAnthropicFamily(model) || model.family.includes('grok-code') || isHiddenModelE(model) || model.family.toLowerCase().includes('gemini-3') || isVSCModelReplaceStringSet(model) || isHiddenModelF(model) || isMinimaxFamily(model) || isHiddenFamilyH(model) || isKimiFamily(model); } /** diff --git a/extensions/copilot/src/platform/endpoint/test/node/chatModelCapabilities.spec.ts b/extensions/copilot/src/platform/endpoint/test/node/chatModelCapabilities.spec.ts index 799a6c84ab7848..d8322ee0180b04 100644 --- a/extensions/copilot/src/platform/endpoint/test/node/chatModelCapabilities.spec.ts +++ b/extensions/copilot/src/platform/endpoint/test/node/chatModelCapabilities.spec.ts @@ -8,7 +8,7 @@ import { ConfigKey, IConfigurationService } from '../../../configuration/common/ import { DefaultsOnlyConfigurationService } from '../../../configuration/common/defaultsOnlyConfigurationService'; import { InMemoryConfigurationService } from '../../../configuration/test/common/inMemoryConfigurationService'; import type { IChatEndpoint } from '../../../networking/common/networking'; -import { getModelCapabilityOverride, modelSupportsContextEditing, modelSupportsPDFDocuments, modelSupportsToolSearch } from '../../common/chatModelCapabilities'; +import { getModelCapabilityOverride, isKimiFamily, modelCanUseApplyPatchExclusively, modelCanUseReplaceStringExclusively, modelSupportsApplyPatch, modelSupportsContextEditing, modelSupportsMultiReplaceString, modelSupportsPDFDocuments, modelSupportsReplaceString, modelSupportsToolSearch } from '../../common/chatModelCapabilities'; function fakeModel(family: string, model: string = family) { return { family, model } as unknown as IChatEndpoint; @@ -41,6 +41,51 @@ describe('modelSupportsPDFDocuments', () => { }); }); +describe('Kimi edit tool capabilities', () => { + test('uses replace-string tools without insert-edit or apply-patch', () => { + const models = { + 'kimi-k2.6': fakeModel('kimi-k2.6'), + 'kimi-k2.7-code': fakeModel('kimi-k2.7-code'), + 'unknown-family + kimi model id': fakeModel('unknown-family', 'kimi-k2.7-code-preview'), + }; + const actual = Object.fromEntries(Object.entries(models).map(([name, model]) => [name, { + isKimiFamily: isKimiFamily(model), + supportsReplaceString: modelSupportsReplaceString(model), + supportsMultiReplaceString: modelSupportsMultiReplaceString(model), + canUseReplaceStringExclusively: modelCanUseReplaceStringExclusively(model), + supportsApplyPatch: modelSupportsApplyPatch(model), + canUseApplyPatchExclusively: modelCanUseApplyPatchExclusively(model), + }])); + + expect(actual).toEqual({ + 'kimi-k2.6': { + isKimiFamily: true, + supportsReplaceString: true, + supportsMultiReplaceString: true, + canUseReplaceStringExclusively: true, + supportsApplyPatch: false, + canUseApplyPatchExclusively: false, + }, + 'kimi-k2.7-code': { + isKimiFamily: true, + supportsReplaceString: true, + supportsMultiReplaceString: true, + canUseReplaceStringExclusively: true, + supportsApplyPatch: false, + canUseApplyPatchExclusively: false, + }, + 'unknown-family + kimi model id': { + isKimiFamily: true, + supportsReplaceString: true, + supportsMultiReplaceString: true, + canUseReplaceStringExclusively: true, + supportsApplyPatch: false, + canUseApplyPatchExclusively: false, + }, + }); + }); +}); + describe('modelSupportsToolSearch', () => { test('supports Claude Sonnet/Opus 4.5 and up, including new and future families', () => { expect(modelSupportsToolSearch('claude-sonnet-4-5')).toBe(true); From 3cce7ab3dc2fa694bfd03971bfac3ed76abfee2d Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci Date: Fri, 26 Jun 2026 19:16:22 +0200 Subject: [PATCH 016/589] signing commit From 4ef42413b3fc6789e6ab2e91fd339d6a28b8bca5 Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci Date: Fri, 26 Jun 2026 19:19:26 +0200 Subject: [PATCH 017/589] address ccr --- .../copilot/src/extension/prompts/node/agent/kimiPrompts.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx index 0ec7f4d0f5265c..30725b77ccff3a 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx @@ -135,10 +135,10 @@ class KimiAgentPrompt extends PromptElement { } class KimiPromptResolver implements IAgentPrompt { - static readonly familyPrefixes = ['kimi-k2.6', 'kimi-k2.7-code']; + static readonly familyPrefixes: string[] = []; static matchesModel(endpoint: IChatEndpoint): boolean { - return isKimiFamily(endpoint) || isKimiFamily(endpoint.model); + return isKimiFamily(endpoint); } resolveSystemPrompt(endpoint: IChatEndpoint): SystemPrompt | undefined { From 6cd539ec0babecfb04bb58bb59e041c665d683d4 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 29 Jun 2026 11:20:37 +0200 Subject: [PATCH 018/589] readding code --- src/vs/editor/contrib/hover/browser/contentHoverRendered.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts index db03a84ca03f8c..85f4edb6ad3193 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts @@ -348,7 +348,7 @@ class RenderedContentHoverParts extends Disposable { this._focusedHoverPartIndex = -1; })); // Add copy button for marker hovers - if (renderedPart.type === 'hoverPart' && !renderedPart.participant.hideCopyButton) { + if (renderedPart.type === 'hoverPart' && !(renderedPart.hoverPart instanceof ColorHover) && !renderedPart.participant.hideCopyButton) { partDisposables.add(new HoverCopyButton( element, () => renderedPart.participant.getAccessibleContent(renderedPart.hoverPart), From 81d97ff0bd4ed9883ed5eda3f1e369b86692fc4f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 29 Jun 2026 19:23:48 +1000 Subject: [PATCH 019/589] chat: remember focused agent session's model as last-used (#323393) * chat: remember focused agent session's model as last-used New chat editors for agent-host session types (e.g. Copilot Agent Host) defaulted to "Auto" even when open editors used a non-Auto model. The per-agent last-used key was only written on explicit picks, so restored sessions that kept their model (the _syncFromModel `keep` path) never recorded it; a fresh editor then read the stale "auto" value. Record the focused session's resolved model into the same per-session-type key on focus, so new editors restore it. Guarded to skip local sessions, types without their own model pool, and unresolved/transient models during async load. Adds shouldRecordSessionTypeDefaultModel + unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../chat/browser/widget/chatWidgetService.ts | 2 ++ .../browser/widget/input/chatInputPart.ts | 17 +++++++++- .../widget/input/chatModelSelectionLogic.ts | 18 +++++++++++ .../input/chatModelSelectionLogic.test.ts | 31 +++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts index 998f6ef06c3edb..69f9f38f5b24ff 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts @@ -259,6 +259,8 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService return; } this.storageService.store(ChatLastUsedEditorSessionTypeStorageKey, sessionType, StorageScope.PROFILE, StorageTarget.USER); + // Also remember this session's current model as the last-used model for its type. + widget?.input.recordCurrentModelAsSessionTypeDefault(); } register(newWidget: IChatWidget): IDisposable { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 406702c3478ef2..3b2a69bef47903 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -90,7 +90,7 @@ import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../common/languageModels.js'; import { ChatModelConfigurationStore } from './chatModelConfigurationStore.js'; import { IChatModelInputState, IChatRequestModeInfo, IInputModel, logChangesToStateModel } from '../../../common/model/chatModel.js'; -import { filterModelsForSession, findBestMatchingModel, findDefaultModel, hasModelsTargetingSession, isModelValidForSession, mergeModelsWithCache, resolveConfiguredModel, resolveModelFromSyncState, shouldResetModelToDefault, shouldResetOnModelListChange, shouldRestoreLateArrivingModel, shouldRestorePersistedModel } from './chatModelSelectionLogic.js'; +import { filterModelsForSession, findBestMatchingModel, findDefaultModel, hasModelsTargetingSession, isModelValidForSession, mergeModelsWithCache, resolveConfiguredModel, resolveModelFromSyncState, shouldRecordSessionTypeDefaultModel, shouldResetModelToDefault, shouldResetOnModelListChange, shouldRestoreLateArrivingModel, shouldRestorePersistedModel } from './chatModelSelectionLogic.js'; import { chatSessionResourceToId, getChatSessionType, LocalChatSessionUri } from '../../../common/model/chatUri.js'; import { IChatResponseViewModel, isResponseVM } from '../../../common/model/chatViewModel.js'; import { IChatAgentService } from '../../../common/participants/chatAgents.js'; @@ -1593,6 +1593,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._syncInputStateToModel(); } + /** + * Record the focused session's resolved model as the last-used model for its session type. + * Skips local sessions, session types without their own pool, and unresolved/out-of-pool models. + */ + public recordCurrentModelAsSessionTypeDefault(): void { + const sessionType = this._currentSessionType; + const hasOwnPool = !!sessionType && this.sessionTypeHasOwnModelPool(sessionType); + const model = this._currentLanguageModel.get(); + if (!model || !shouldRecordSessionTypeDefaultModel(sessionType, hasOwnPool, model, this.getModels())) { + return; + } + this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER); + this.storageService.store(this.getSelectedModelIsDefaultStorageKey(), !!model.metadata.isDefaultForLocation[this.location], StorageScope.APPLICATION, StorageTarget.USER); + } + private checkModelSupported(): void { const lm = this._currentLanguageModel.get(); const allModels = this.getAllMergedModels(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts index c79f7ce11599c3..7ea0c268672402 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts @@ -83,6 +83,24 @@ export function hasModelsTargetingSession( return allModels.some(m => m.metadata.targetChatSessionType === sessionType); } +/** + * Decide whether a focused session's current model should be recorded as the last-used model for + * its session type. Only records non-local sessions that own a model pool and whose model is + * resolved and present in the pool, so a transient "Auto" during async model-list load cannot + * clobber the remembered selection. + */ +export function shouldRecordSessionTypeDefaultModel( + sessionType: string | undefined, + hasOwnPool: boolean, + model: ILanguageModelChatMetadataAndIdentifier | undefined, + models: ILanguageModelChatMetadataAndIdentifier[], +): boolean { + if (!sessionType || sessionType === 'local' || !hasOwnPool || !model) { + return false; + } + return models.some(m => m.identifier === model.identifier); +} + /** * Check if a model is valid for the current session's model pool. * If the session has targeted models, the model must target that session type. diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts index c6a572fb3edf83..4d08e51d0139c9 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts @@ -21,6 +21,7 @@ import { mergeModelsWithCache, resolveConfiguredModel, resolveModelFromSyncState, + shouldRecordSessionTypeDefaultModel, shouldResetModelToDefault, shouldResetOnModelListChange, shouldRestoreLateArrivingModel, @@ -1841,4 +1842,34 @@ suite('ChatModelSelectionLogic', () => { assert.strictEqual(reason({ trusted: true, requiresSetup: true, pickerModels: [gpt], liveModelIds: new Set([gpt.identifier]) }), undefined); }); }); + + suite('shouldRecordSessionTypeDefaultModel', () => { + const sessionType = 'agent-host-copilotcli'; + const opus = createSessionModel('claude-opus-4.8', 'Opus 4.8', sessionType); + const pool = [opus]; + + test('records a resolved non-local model present in the pool', () => { + assert.strictEqual(shouldRecordSessionTypeDefaultModel(sessionType, true, opus, pool), true); + }); + + test('skips local sessions', () => { + assert.strictEqual(shouldRecordSessionTypeDefaultModel('local', true, opus, pool), false); + }); + + test('skips when session type has no own pool', () => { + assert.strictEqual(shouldRecordSessionTypeDefaultModel(sessionType, false, opus, pool), false); + }); + + test('skips when no model is resolved (transient Auto during load)', () => { + assert.strictEqual(shouldRecordSessionTypeDefaultModel(sessionType, true, undefined, pool), false); + }); + + test('skips when the model is not in the pool', () => { + assert.strictEqual(shouldRecordSessionTypeDefaultModel(sessionType, true, opus, []), false); + }); + + test('skips when there is no session type', () => { + assert.strictEqual(shouldRecordSessionTypeDefaultModel(undefined, true, opus, pool), false); + }); + }); }); From 323f10a51b6adc8df95a0e67d2728b6e2032e82d Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Mon, 29 Jun 2026 10:25:21 +0100 Subject: [PATCH 020/589] Add codicon styling for auxiliary bar action items (#323432) style override: add codicon styling for auxiliary bar action items Co-authored-by: mrleemurray Co-authored-by: Copilot --- .../contrib/styleOverrides/browser/media/activityBar.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/activityBar.css b/src/vs/workbench/contrib/styleOverrides/browser/media/activityBar.css index 1d5152b4469a48..c96881211db390 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/media/activityBar.css +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/activityBar.css @@ -291,6 +291,7 @@ display: none; } +.style-override.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .codicon, .style-override.monaco-workbench .pane-composite-part.basepanel > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .codicon { font-weight: var(--vscode-agents-fontWeight-regular); } From 953d9d7ecf5e6bca2b6dac5d243467df33b7b95e Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Mon, 29 Jun 2026 10:25:33 +0100 Subject: [PATCH 021/589] Update floating panel spacing to use --vscode-spacing-size40 (#323425) Update floating panel spacing: change margin and padding to use --vscode-spacing-size40 Co-authored-by: mrleemurray Co-authored-by: Copilot --- .../browser/media/floatingPanels.css | 26 +++++++++---------- .../services/layout/browser/layoutService.ts | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/browser/media/floatingPanels.css b/src/vs/workbench/browser/media/floatingPanels.css index ab56c980c01d69..32a00d5b442f73 100644 --- a/src/vs/workbench/browser/media/floatingPanels.css +++ b/src/vs/workbench/browser/media/floatingPanels.css @@ -15,13 +15,13 @@ * card on a window edge gets a doubled outer gutter (`.floating-part-outer-*`). * Colors mirror the agents window (`agentsPanel.*` tokens). * - * Uses `--vscode-spacing-size60` (6px); keep this in sync with + * Uses `--vscode-spacing-size40` (4px); keep this in sync with * `FLOATING_PANEL_MARGIN` in code. */ .monaco-workbench.floating-panels .part.sidebar, .monaco-workbench.floating-panels .part.auxiliarybar { - margin: var(--vscode-spacing-size60); + margin: var(--vscode-spacing-size40); border: 1px solid var(--vscode-agentsPanel-border, var(--vscode-widget-border, transparent)) !important; border-radius: var(--vscode-cornerRadius-large); background-color: var(--vscode-agentsPanel-background) !important; @@ -30,7 +30,7 @@ /* Keep the panel surface aligned with the panel theme background in light theme. */ .monaco-workbench.floating-panels .part.panel { - margin: var(--vscode-spacing-size60); + margin: var(--vscode-spacing-size40); border: 1px solid var(--vscode-agentsPanel-border, var(--vscode-widget-border, transparent)) !important; border-radius: var(--vscode-cornerRadius-large); background-color: var(--vscode-panel-background) !important; @@ -55,16 +55,16 @@ * sync with the inset reservation in `AbstractPaneCompositePart`). */ .monaco-workbench.floating-panels .part.floating-part-outer-left { - margin-left: calc(var(--vscode-spacing-size60) * 2); + margin-left: calc(var(--vscode-spacing-size40) * 2); } .monaco-workbench.floating-panels .part.floating-part-outer-right { - margin-right: calc(var(--vscode-spacing-size60) * 2); + margin-right: calc(var(--vscode-spacing-size40) * 2); } /* Main editor: float like the other cards, but stay flush with the title bar. */ .monaco-workbench.floating-panels > .monaco-grid-view .part.editor { - margin: var(--vscode-spacing-size60); + margin: var(--vscode-spacing-size40); margin-top: 0; } @@ -74,11 +74,11 @@ * bars use. Kept in sync with the margin reservation in `EditorPart.layout`. */ .monaco-workbench.floating-panels > .monaco-grid-view .part.editor.floating-editor-outer-left { - margin-left: calc(var(--vscode-spacing-size60) * 2); + margin-left: calc(var(--vscode-spacing-size40) * 2); } .monaco-workbench.floating-panels > .monaco-grid-view .part.editor.floating-editor-outer-right { - margin-right: calc(var(--vscode-spacing-size60) * 2); + margin-right: calc(var(--vscode-spacing-size40) * 2); } /* The shell backdrop behind the floating cards matches the editor background. */ @@ -104,8 +104,8 @@ /* Activity bar (default position) gets a left and bottom gutter to match the cards. */ .monaco-workbench.floating-panels .part.activitybar { - margin-left: var(--vscode-spacing-size60); - margin-bottom: var(--vscode-spacing-size60); + margin-left: var(--vscode-spacing-size40); + margin-bottom: var(--vscode-spacing-size40); } /* The status bar draws its top border via a pseudo-element — hide it too. */ @@ -115,9 +115,9 @@ /* Inset the status bar items so they respect the same gutter as the cards. */ .monaco-workbench.floating-panels .part.statusbar { - padding-left: var(--vscode-spacing-size60); - padding-right: var(--vscode-spacing-size60); - padding-bottom: var(--vscode-spacing-size60); + padding-left: var(--vscode-spacing-size40); + padding-right: var(--vscode-spacing-size40); + padding-bottom: var(--vscode-spacing-size40); } .monaco-workbench.floating-panels .activitybar > .content > .composite-bar { diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index c8a921507134c5..7f2ed1f7f7ebc5 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -58,9 +58,9 @@ export const enum LayoutSettings { * experiment (`LayoutSettings.MODERN_UI`) is enabled. Parts grow or shrink their * content by this amount to leave room for the margin/border applied in CSS * (`src/vs/workbench/browser/media/floatingPanels.css`, `.floating-panels`). - * Keep in sync with the `--vscode-spacing-size60` (6px) token used there. + * Keep in sync with the `--vscode-spacing-size40` (4px) token used there. */ -export const FLOATING_PANEL_MARGIN = 6; +export const FLOATING_PANEL_MARGIN = 4; export const enum ActivityBarPosition { DEFAULT = 'default', From a5c1df2d3f603ec5979c5a2b8bb27b210b55e4bb Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Mon, 29 Jun 2026 10:26:22 +0100 Subject: [PATCH 022/589] Add title bar style override for macOS (#323435) style: add title bar style override for macOS Co-authored-by: mrleemurray --- .../styleOverrides/browser/media/titlebar.css | 19 +++++++++++++++++++ .../browser/styleOverrides.contribution.ts | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/vs/workbench/contrib/styleOverrides/browser/media/titlebar.css diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/titlebar.css b/src/vs/workbench/contrib/styleOverrides/browser/media/titlebar.css new file mode 100644 index 00000000000000..b3c14a1db149e2 --- /dev/null +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/titlebar.css @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Style-override module: "Title Bar". + * + * Adds a small amount of top padding to the title-bar left/center/right + * containers so their items read as vertically centered. Scoped to macOS for + * now. All rules are gated behind the `.style-override` class, which the + * StyleOverridesContribution toggles onto the workbench container(s) when the + * Modern UI Update setting is enabled. + */ +.style-override.monaco-workbench.mac .part.titlebar > .titlebar-container > .titlebar-left, +.style-override.monaco-workbench.mac .part.titlebar > .titlebar-container > .titlebar-center, +.style-override.monaco-workbench.mac .part.titlebar > .titlebar-container > .titlebar-right { + padding-top: 2px; +} diff --git a/src/vs/workbench/contrib/styleOverrides/browser/styleOverrides.contribution.ts b/src/vs/workbench/contrib/styleOverrides/browser/styleOverrides.contribution.ts index 381293ce3766bc..231ff7ea32c8a0 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/styleOverrides.contribution.ts +++ b/src/vs/workbench/contrib/styleOverrides/browser/styleOverrides.contribution.ts @@ -25,6 +25,7 @@ import './media/scrollShadows.css'; import './media/shadows.css'; import './media/statusBar.css'; import './media/tabs.css'; +import './media/titlebar.css'; interface IStyleOverrideModule { readonly id: string; @@ -64,7 +65,8 @@ const STYLE_OVERRIDE_MODULES: readonly IStyleOverrideModule[] = [ { id: 'scrollShadows' }, { id: 'shadows' }, { id: 'statusBar' }, - { id: 'tabs' } + { id: 'tabs' }, + { id: 'titlebar' } ]; /** From c87216ff49fd3a6a24485279c9637f57e8a9edde Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci Date: Mon, 29 Jun 2026 11:38:54 +0200 Subject: [PATCH 023/589] signing commit From 6387b615a07685fae2b2ae4e74ae13b5ef8112f6 Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:45:37 +0000 Subject: [PATCH 024/589] Bump version to 1.128.0 (#323406) * Bump version to 1.128.0 * signing commit --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Giuseppe Cianci --- extensions/copilot/package-lock.json | 6 +++--- extensions/copilot/package.json | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index 53f42cb984ce1b..135afbf30146b5 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -1,12 +1,12 @@ { "name": "copilot-chat", - "version": "0.55.0", + "version": "0.56.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "copilot-chat", - "version": "0.55.0", + "version": "0.56.0", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { @@ -144,7 +144,7 @@ "engines": { "node": ">=22.14.0", "npm": ">=9.0.0", - "vscode": "^1.127.0" + "vscode": "^1.128.0" } }, "node_modules/@anthropic-ai/claude-agent-sdk": { diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 26be50ec721f4a..bb871c74d4a73f 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -2,7 +2,7 @@ "name": "copilot-chat", "displayName": "GitHub Copilot", "description": "AI chat features powered by Copilot", - "version": "0.55.0", + "version": "0.56.0", "build": "1", "completionsCoreVersion": "1.378.1799", "internalLargeStorageAriaKey": "ec712b3202c5462fb6877acae7f1f9d7-c19ad55e-3e3c-4f99-984b-827f6d95bd9e-6917", @@ -22,7 +22,7 @@ "icon": "assets/copilot.png", "pricing": "Trial", "engines": { - "vscode": "^1.127.0", + "vscode": "^1.128.0", "npm": ">=9.0.0", "node": ">=22.14.0" }, diff --git a/package-lock.json b/package-lock.json index 5ae5fdb83e57cd..d488a237da27af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-oss-dev", - "version": "1.127.0", + "version": "1.128.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-oss-dev", - "version": "1.127.0", + "version": "1.128.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 110b3ff6ff2473..f0c22189c1f34e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-oss-dev", - "version": "1.127.0", + "version": "1.128.0", "distro": "7abf39b86c07d094722a4b3ec9f37e78fe3d5db3", "author": { "name": "Microsoft Corporation" From 25fc7a8eac5d5dbf0f42b0f84a7c3e4d4071c37b Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Mon, 29 Jun 2026 11:41:12 +0100 Subject: [PATCH 025/589] Update scrollbar styles for consistency (#323441) * style: update scrollbar styles for consistency across elements Co-authored-by: Copilot * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: adjust scrollbar width for improved corner visibility --------- Co-authored-by: mrleemurray Co-authored-by: Copilot Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../browser/media/roundedCorners.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/roundedCorners.css b/src/vs/workbench/contrib/styleOverrides/browser/media/roundedCorners.css index 03f9dfc1518ec8..2727b8fe050d43 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/media/roundedCorners.css +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/roundedCorners.css @@ -106,17 +106,17 @@ border-radius: 0 !important; } -/* Scrollbar sliders */ +/* Scrollbar sliders (panels, editor, lists — all consistent) */ .style-override .monaco-scrollable-element > .scrollbar > .slider { border-radius: var(--vscode-cornerRadius-small) !important; } -/* - * The editor's own scrollbars sit flush against the viewport edge (next to the - * minimap); rounding their sliders looks odd, so keep those square. - */ -.style-override .monaco-editor .monaco-scrollable-element > .scrollbar > .slider { - border-radius: 0 !important; +/* Terminal uses xterm.js' own scrollbar (.xterm-scrollable-element) */ +.style-override .xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-slider { + border-radius: var(--vscode-cornerRadius-small) !important; + /* Inset the slider so both left and right rounded corners are visible */ + left: 1px !important; + width: calc(100% - 1px) !important; } /* ============================================================================= From 0b121074ec9c2db704da40e24558b964c2168a9b Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 29 Jun 2026 11:51:07 +0100 Subject: [PATCH 026/589] Refactor padding styles for header and body in pane view for improved alignment --- .../contrib/styleOverrides/browser/media/padding.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css b/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css index d68435903fd850..1893f07012fdc0 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css @@ -33,14 +33,15 @@ /* Header: inset the section title / actions to line up with the rows below. */ .style-override .monaco-pane-view .pane:not(:has(> .pane-body.chat-viewpane)) > .pane-header { - padding-left: var(--vscode-spacing-size80, 8px); - padding-right: var(--vscode-spacing-size40, 4px); + padding-left: var(--vscode-spacing-size40, 4px); + padding-right: 0; + margin: 0 var(--vscode-spacing-size40, 4px); } /* Body: inset each row box, leaving the scrollbar pinned to the pane edge. */ .style-override .monaco-pane-view .pane-body:not(.chat-viewpane) .monaco-list-row { - left: var(--vscode-spacing-size80, 8px); - right: var(--vscode-spacing-size80, 8px); + left: var(--vscode-spacing-size40, 4px); + right: var(--vscode-spacing-size40, 4px); width: auto; } From d92477e276890ba0eda5a4bac9347818b7af12c6 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 29 Jun 2026 11:54:08 +0100 Subject: [PATCH 027/589] Add padding to agent sessions title container in chat viewpane --- .../contrib/styleOverrides/browser/media/padding.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css b/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css index 1893f07012fdc0..54671f5a615822 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css @@ -73,3 +73,9 @@ .style-override.monaco-workbench .monaco-pane-view .pane > .pane-header > .actions { margin-right: 0px; } + +.style-override.monaco-workbench .chat-viewpane.has-sessions-control .agent-sessions-container { + .agent-sessions-title-container { + padding-left: 12px; + } +} From a1e0f398510259c51eee14d74418483b0c448d41 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 29 Jun 2026 11:58:14 +0100 Subject: [PATCH 028/589] Adjust inset values for pane header separator for improved alignment --- .../contrib/styleOverrides/browser/media/paneHeaders.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/paneHeaders.css b/src/vs/workbench/contrib/styleOverrides/browser/media/paneHeaders.css index e9acb2e41c96ac..cedbafd2a03ecb 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/media/paneHeaders.css +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/paneHeaders.css @@ -40,8 +40,8 @@ content: ""; position: absolute; top: 0; - left: 8px; - right: 8px; + left: 4px; + right: 4px; height: 1px; pointer-events: none; background-color: var(--vscode-sideBarSectionHeader-border, var(--vscode-panelSectionHeader-border, var(--vscode-panel-border))); From d5ca958fb1e41918e2ef039beada3b0f3b21a792 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 29 Jun 2026 21:22:55 +1000 Subject: [PATCH 029/589] Enable attaching text attachments in copilot integration test (#322771) test: enable test for attaching a text blob and reading its function names --- .../test/node/protocol/copilotRealSdk.integrationTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/agentHost/test/node/protocol/copilotRealSdk.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/copilotRealSdk.integrationTest.ts index f0925c97e051f3..b7c494974a6395 100644 --- a/src/vs/platform/agentHost/test/node/protocol/copilotRealSdk.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/copilotRealSdk.integrationTest.ts @@ -144,7 +144,7 @@ defineSharedRealSdkTests(COPILOT_CONFIG); assert.match(result.responseText, /\badd\b/i, `expected the model to identify the attached file function; got: ${JSON.stringify(result.responseText)}`); }); - test.skip('attaches a text blob and reads its function names', async function () { + test('attaches a text blob and reads its function names', async function () { this.timeout(120_000); const sessionUri = await createRealSession(client, COPILOT_CONFIG, 'real-sdk-blob-attachment', createdSessions, URI.file(tmpdir()).toString()); From 85c31bc1203a2b191a906dde6a13c037e34dfaa7 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 29 Jun 2026 12:30:25 +0100 Subject: [PATCH 030/589] Adjust layout and styling for floating panels and activity bar for improved consistency Co-authored-by: Copilot --- .../workbench/browser/parts/views/viewPaneContainer.ts | 9 +++++++-- .../contrib/styleOverrides/browser/media/padding.css | 7 +++++++ .../contrib/styleOverrides/browser/media/paneHeaders.css | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 45e2b137b20765..409678580f9aac 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -38,7 +38,7 @@ import { IAddedViewDescriptorRef, ICustomViewDescriptor, IView, IViewContainerMo import { IViewsService } from '../../../services/views/common/viewsService.js'; import { FocusedViewContext } from '../../../common/contextkeys.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { isHorizontal, IWorkbenchLayoutService, LayoutSettings } from '../../../services/layout/browser/layoutService.js'; +import { isHorizontal, IWorkbenchLayoutService, LayoutSettings, FLOATING_PANEL_MARGIN } from '../../../services/layout/browser/layoutService.js'; import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ViewContainerMenuActions } from './viewMenuActions.js'; @@ -628,7 +628,12 @@ export class ViewPaneContainer extends Comp this.paneview.flipOrientation(dimension.height, dimension.width); } - this.paneview.layout(dimension.height, dimension.width); + // In Modern UI (floating panels) reserve a small bottom gap so the last + // pane does not sit flush against the part edge, matching the 4px + // horizontal margins on the pane headers. Add 1px for the part's bottom + // border so the visible gap lines up with the horizontal margins. + const bottomGap = this.layoutService.isFloatingPanelsEnabled() ? FLOATING_PANEL_MARGIN + 1 : 0; + this.paneview.layout(Math.max(0, dimension.height - bottomGap), dimension.width); } this.dimension = dimension; diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css b/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css index 54671f5a615822..245ce90fa6a848 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/padding.css @@ -79,3 +79,10 @@ padding-left: 12px; } } + +/* Give the settings / profile (global) actions at the bottom of the default + * vertical activity bar a 4px bottom margin so they sit consistently above the + * window edge, matching the pane header margins. */ +.style-override.monaco-workbench .activitybar:not(.top):not(.bottom) > .content > :not(.composite-bar):last-child { + margin-bottom: calc(var(--vscode-spacing-size20, 2px) + var(--vscode-strokeThickness, 1px)); +} diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/paneHeaders.css b/src/vs/workbench/contrib/styleOverrides/browser/media/paneHeaders.css index cedbafd2a03ecb..61f86ebcb73149 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/media/paneHeaders.css +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/paneHeaders.css @@ -63,7 +63,7 @@ * inline header background from the PaneView is overridden here; the hover tint * below still wins via its higher specificity. */ .style-override .monaco-pane-view .pane > .pane-header { - border-radius: var(--vscode-cornerRadius-medium); + border-radius: var(--vscode-cornerRadius-small); background-color: var(--vscode-sideBar-background, var(--vscode-panel-background)) !important; } From 04682785f8f5faa10d6a4fc0e8f2208b9e5ee197 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Mon, 29 Jun 2026 12:40:58 +0100 Subject: [PATCH 031/589] Style overrides: Add sash handles style override for improved UI visibility (#323464) * Add sash handles style override for improved UI visibility * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Adjust box-shadow values for vertical and horizontal sash handles for improved visibility Co-authored-by: Copilot --------- Co-authored-by: mrleemurray Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot --- .../browser/media/sashHandles.css | 89 +++++++++++++++++++ .../browser/styleOverrides.contribution.ts | 2 + 2 files changed, 91 insertions(+) create mode 100644 src/vs/workbench/contrib/styleOverrides/browser/media/sashHandles.css diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/sashHandles.css b/src/vs/workbench/contrib/styleOverrides/browser/media/sashHandles.css new file mode 100644 index 00000000000000..8e9cc36c13c2d8 --- /dev/null +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/sashHandles.css @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Style-override module: "Sash Handles". + * + * With the Modern UI floating-panel treatment the workbench parts read as + * separate cards with a gap between them, which hides where one part ends and + * the next begins. Sashes are invisible until hovered, so the resize boundary + * is hard to find. This module gives every inter-panel sash a persistent, + * low-contrast grip — three small dots — so users can see where parts meet and + * where to grab to resize. The grip is suppressed for sashes within a part + * (editor splits, tree/view resizers) so only the boundaries between top-level + * parts are marked. On hover/active the grip fades out in favour of the + * existing full-length highlight (see base sash.css), so behaviour while + * dragging is unchanged. + * + * All rules are gated behind the `.style-override` class, which the + * StyleOverridesContribution toggles onto the workbench container(s) when the + * `workbench.experimental.modernUI` (Modern UI Update) setting is enabled. + * Without that class these rules never apply. + */ + +/* Persistent grip handle, centered on the sash, drawn via the spare pseudo. */ +.style-override .monaco-sash:not(.disabled)::after { + content: ''; + position: absolute; + pointer-events: none; + width: 4px; + height: 4px; + border-radius: 50%; + background: color-mix(in srgb, var(--vscode-foreground) 30%, transparent); + opacity: 0.5; +} + +.monaco-enable-motion .style-override .monaco-sash:not(.disabled)::after { + transition: opacity 0.1s ease-out; +} + +/* Vertical sash: three dots stacked vertically, centered. */ +.style-override .monaco-sash.vertical:not(.disabled)::after { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-shadow: 0 -7px currentColor, 0 7px currentColor; + color: color-mix(in srgb, var(--vscode-foreground) 30%, transparent); +} + +/* Horizontal sash: three dots in a row, centered. */ +.style-override .monaco-sash.horizontal:not(.disabled)::after { + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + box-shadow: -7px 0 currentColor, 7px 0 currentColor; + color: color-mix(in srgb, var(--vscode-foreground) 30%, transparent); +} + +/* While hovering/dragging, defer to the base full-length highlight. */ +.style-override .monaco-sash.hover:not(.disabled)::after, +.style-override .monaco-sash.active:not(.disabled)::after { + opacity: 0; +} + +/* Only mark boundaries between top-level parts. Sashes inside a part (editor + * splits, tree/view resizers) live within `.part`, so suppress their grip. */ +.style-override .part .monaco-sash::after { + content: none; +} + +/* Hide grips for collapsed parts so a flush boundary isn't marked (and the + * panel grip can't be clipped by the status bar). The workbench container + * carries `nopanel`/`nosidebar` when those parts are hidden: the panel sash is + * horizontal, the primary side bar sash is vertical. */ +.style-override.nopanel .monaco-sash.horizontal::after, +.style-override.nosidebar .monaco-sash.vertical::after { + content: none; +} + +@media (forced-colors: active) { + .style-override .monaco-sash:not(.disabled)::after, + .style-override .monaco-sash.vertical:not(.disabled)::after, + .style-override .monaco-sash.horizontal:not(.disabled)::after { + background: CanvasText; + color: CanvasText; + opacity: 1; + } +} diff --git a/src/vs/workbench/contrib/styleOverrides/browser/styleOverrides.contribution.ts b/src/vs/workbench/contrib/styleOverrides/browser/styleOverrides.contribution.ts index 231ff7ea32c8a0..5d6711df14b695 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/styleOverrides.contribution.ts +++ b/src/vs/workbench/contrib/styleOverrides/browser/styleOverrides.contribution.ts @@ -21,6 +21,7 @@ import './media/keyboardFocusOnly.css'; import './media/padding.css'; import './media/paneHeaders.css'; import './media/roundedCorners.css'; +import './media/sashHandles.css'; import './media/scrollShadows.css'; import './media/shadows.css'; import './media/statusBar.css'; @@ -62,6 +63,7 @@ const STYLE_OVERRIDE_MODULES: readonly IStyleOverrideModule[] = [ { id: 'padding' }, { id: 'paneHeaders', layoutAffecting: true }, { id: 'roundedCorners' }, + { id: 'sashHandles' }, { id: 'scrollShadows' }, { id: 'shadows' }, { id: 'statusBar' }, From 569e47217723f5ff79d412429c005af79484e2ae Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:56:32 +0000 Subject: [PATCH 032/589] AgentHost - fix commit changes operation visibility (#323459) * AgentHost - fix commit changes operation visibility * Fix tests --- .../agentHost/node/agentHostChangesetOperationService.ts | 5 +---- .../agentHost/node/agentHostCommitOperationProvider.ts | 6 +++++- .../test/node/agentHostCommitOperationProvider.test.ts | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentHostChangesetOperationService.ts b/src/vs/platform/agentHost/node/agentHostChangesetOperationService.ts index 164a4205327736..995490df8adbf8 100644 --- a/src/vs/platform/agentHost/node/agentHostChangesetOperationService.ts +++ b/src/vs/platform/agentHost/node/agentHostChangesetOperationService.ts @@ -76,7 +76,7 @@ export class AgentHostChangesetOperationService extends Disposable implements IA }); } - private _getOperations(context: IChangesetOperationContext): readonly ChangesetOperation[] | undefined { + private _getOperations(context: IChangesetOperationContext): readonly ChangesetOperation[] { const operations: ChangesetOperation[] = []; for (const contribution of this._handlerRegistrations.keys()) { const contributed = contribution.getOperations(context); @@ -84,9 +84,6 @@ export class AgentHostChangesetOperationService extends Disposable implements IA operations.push(...contributed); } } - if (operations.length === 0) { - return undefined; - } // Operations are disabled while a turn is active so the working tree / // branch state can't be mutated mid-request. diff --git a/src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts b/src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts index 5b6557848e14b1..51b5a1cd93d43d 100644 --- a/src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts +++ b/src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts @@ -32,11 +32,15 @@ export class AgentHostCommitOperationContribution extends Disposable implements return store; } - getOperations({ gitState }: IChangesetOperationContext): ChangesetOperation[] | undefined { + getOperations({ changesetKind, gitHubState, gitState }: IChangesetOperationContext): ChangesetOperation[] | undefined { if ((gitState?.uncommittedChanges ?? 0) <= 0) { return undefined; } + if (!gitHubState?.pullRequestUrl && changesetKind !== 'uncommitted') { + return undefined; + } + return [{ id: AgentHostCommitOperationHandler.OPERATION_COMMIT, label: localize('agentHost.changeset.commit', "Commit Changes"), diff --git a/src/vs/platform/agentHost/test/node/agentHostCommitOperationProvider.test.ts b/src/vs/platform/agentHost/test/node/agentHostCommitOperationProvider.test.ts index 3a699875a059f0..9503efc85ddbb0 100644 --- a/src/vs/platform/agentHost/test/node/agentHostCommitOperationProvider.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostCommitOperationProvider.test.ts @@ -51,6 +51,6 @@ suite('AgentHostCommitOperationContribution', () => { const operations = provider.getOperations({ sessionKey, changesetUri: buildSessionChangesetUri(sessionKey), changesetKind: ChangesetKind.Session, gitState: gitStateWithUncommittedChanges }); - assert.deepStrictEqual(operations?.map(op => op.id), ['commit']); + assert.deepStrictEqual(operations?.map(op => op.id), undefined); }); }); From 57200838c88462f387a4e0104c27bb3a2cb3ccfa Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:04:37 +0200 Subject: [PATCH 033/589] Change Skip button to End Tour with Esc hint (#323473) * Change Skip button to End Tour with Esc hint Fixes #323446 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use new localization ids and update skip wording Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../onboarding/browser/spotlight/spotlightOverlay.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightOverlay.ts b/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightOverlay.ts index 6ced326c81a701..06012632e191fa 100644 --- a/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightOverlay.ts +++ b/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightOverlay.ts @@ -137,7 +137,8 @@ export class SpotlightOverlay extends Disposable { const actions = append(footer, $('.spotlight-callout-actions')); this._skipButton = this._register(new Button(actions, { ...defaultButtonStyles, secondary: true })); - this._skipButton.label = localize('spotlight.skip', "Skip"); + this._skipButton.label = localize('spotlight.endTour', "End Tour"); + this._skipButton.setTitle(localize('spotlight.endTour.tooltip', "End Tour (Esc)")); this._register(this._skipButton.onDidClick(() => this._onDidSkip.fire(OnboardingDismissReason.SkipButton))); this._backButton = this._register(new Button(actions, { ...defaultButtonStyles, secondary: true })); @@ -148,7 +149,12 @@ export class SpotlightOverlay extends Disposable { this._nextButton.label = localize('spotlight.next', "Next"); this._register(this._nextButton.onDidClick(() => this._onDidClickNext.fire('button'))); - // Keyboard handling on the callout: Esc skips, focus is trapped within. + // Buttons swallow Escape internally, so route their escape events to end the tour too. + for (const button of [this._skipButton, this._backButton, this._nextButton]) { + this._register(button.onDidEscape(() => this._onDidSkip.fire(OnboardingDismissReason.EscapeKey))); + } + + // Keyboard handling on the callout: Esc ends the tour, focus is trapped within. this._register(addDisposableListener(this._callout, EventType.KEY_DOWN, e => this._onKeyDown(e))); this._register({ dispose: () => this._restoreFocus() }); From 34fa7dbcc3839f9c3b9238a702e239ddaa08feff Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 29 Jun 2026 14:05:50 +0100 Subject: [PATCH 034/589] Remove IStorageSourceFilter (#323480) --- .../aiCustomizationWorkspaceService.ts | 10 +- .../customizationsDebugLog.contribution.ts | 36 ++--- .../remoteAgentHostCustomizationHarness.ts | 9 +- ...emoteAgentHostCustomizationHarness.test.ts | 3 +- .../aiCustomizationShortcutsWidget.fixture.ts | 3 +- .../api/browser/mainThreadChatAgents2.ts | 7 +- .../agentHost/agentHostChatContribution.ts | 3 +- .../aiCustomizationDebugPanel.ts | 29 +--- .../aiCustomizationItemsModel.ts | 2 +- .../aiCustomizationManagementEditor.ts | 2 +- .../customizationCreatorService.ts | 2 +- ...promptsServiceCustomizationItemProvider.ts | 15 +- .../common/aiCustomizationWorkspaceService.ts | 31 +--- .../common/customizationHarnessService.ts | 19 +-- .../aiCustomizationItemsModel.test.ts | 5 +- .../aiCustomizationListWidget.test.ts | 5 +- .../applyStorageSourceFilter.test.ts | 151 ------------------ .../customizationHarnessService.test.ts | 20 --- 18 files changed, 41 insertions(+), 311 deletions(-) delete mode 100644 src/vs/workbench/contrib/chat/test/browser/aiCustomization/applyStorageSourceFilter.test.ts diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index 713297bde54756..5d7ff351aad3d5 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -7,9 +7,8 @@ import { derived, IObservable, observableValue, ISettableObservable } from '../. import { relativePath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IAICustomizationWorkspaceService, AICustomizationManagementSection, applyStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IAICustomizationWorkspaceService, AICustomizationManagementSection } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IChatPromptSlashCommand, IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; -import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js'; import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { CustomizationCreatorService } from '../../../../workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.js'; @@ -47,7 +46,6 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization @ISessionsService private readonly sessionsService: ISessionsService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IPromptsService private readonly promptsService: IPromptsService, - @ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService, @ICommandService private readonly commandService: ICommandService, @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, @@ -264,11 +262,7 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization } async getFilteredPromptSlashCommands(token: CancellationToken): Promise { - const allCommands = await this.promptsService.getPromptSlashCommands(token); - return allCommands.filter(cmd => { - const filter = this.harnessService.getActiveDescriptor().getStorageSourceFilter(cmd.type); - return applyStorageSourceFilter([cmd], filter).length > 0; - }); + return await this.promptsService.getPromptSlashCommands(token); } private static readonly _skillUIIntegrations: ReadonlyMap = new Map([ diff --git a/src/vs/sessions/contrib/chat/browser/customizationsDebugLog.contribution.ts b/src/vs/sessions/contrib/chat/browser/customizationsDebugLog.contribution.ts index 7a897b98f8bd64..b7bcb645d6dc5f 100644 --- a/src/vs/sessions/contrib/chat/browser/customizationsDebugLog.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/customizationsDebugLog.contribution.ts @@ -9,12 +9,11 @@ import { autorun } from '../../../../base/common/observable.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { IAICustomizationWorkspaceService, IStorageSourceFilter, applyStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IPromptsService, PromptsStorage, IPromptPath } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; -import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js'; const PROMPT_SECTIONS: { section: AICustomizationManagementSection; type: PromptsType }[] = [ { section: AICustomizationManagementSection.Agents, type: PromptsType.agent }, @@ -34,8 +33,7 @@ class CustomizationsDebugLogContribution extends Disposable implements IWorkbenc @IPromptsService private readonly _promptsService: IPromptsService, @IAICustomizationWorkspaceService private readonly _workspaceService: IAICustomizationWorkspaceService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, - @IMcpService private readonly _mcpService: IMcpService, - @ICustomizationHarnessService private readonly _harnessService: ICustomizationHarnessService + @IMcpService private readonly _mcpService: IMcpService ) { super(); this._logger = this._register(loggerService.createLogger('customizationsDebug', { name: 'Customizations Debug' })); @@ -72,7 +70,6 @@ class CustomizationsDebugLogContribution extends Disposable implements IWorkbenc private async _doLogSnapshot(): Promise { const root = this._workspaceService.getActiveProjectRoot()?.fsPath ?? '(none)'; - const descriptor = this._harnessService.getActiveDescriptor(); this._logger.info(''); this._logger.info('=== Customizations Snapshot ==='); @@ -85,16 +82,14 @@ class CustomizationsDebugLogContribution extends Disposable implements IWorkbenc this._logger.info(` ${'--------'.padEnd(16)} ${'-----'.padStart(6)} ${'----'.padStart(6)} ${'---'.padStart(6)} ${'-----'.padStart(7)}`); for (const { section, type } of PROMPT_SECTIONS) { - const filter = descriptor.getStorageSourceFilter(type); - await this._logSectionRow(section, type, filter); + await this._logSectionRow(section, type); } this._logger.info(''); // Details per section for (const { section, type } of PROMPT_SECTIONS) { - const filter = descriptor.getStorageSourceFilter(type); - await this._logSectionDetails(section, type, filter); + await this._logSectionDetails(section, type); } // MCP Servers @@ -115,7 +110,7 @@ class CustomizationsDebugLogContribution extends Disposable implements IWorkbenc this._logger.info(''); } - private async _logSectionRow(section: AICustomizationManagementSection, type: PromptsType, filter: IStorageSourceFilter): Promise { + private async _logSectionRow(section: AICustomizationManagementSection, type: PromptsType): Promise { try { const [localFiles, userFiles, extensionFiles] = await Promise.all([ this._promptsService.listPromptFilesForStorage(type, PromptsStorage.local, CancellationToken.None), @@ -123,18 +118,17 @@ class CustomizationsDebugLogContribution extends Disposable implements IWorkbenc this._promptsService.listPromptFilesForStorage(type, PromptsStorage.extension, CancellationToken.None), ]); const all: IPromptPath[] = [...localFiles, ...userFiles, ...extensionFiles]; - const filtered = applyStorageSourceFilter(all, filter); - const local = filtered.filter(f => f.storage === PromptsStorage.local).length; - const user = filtered.filter(f => f.storage === PromptsStorage.user).length; - const ext = filtered.filter(f => f.storage === PromptsStorage.extension).length; + const local = all.filter(f => f.storage === PromptsStorage.local).length; + const user = all.filter(f => f.storage === PromptsStorage.user).length; + const ext = all.filter(f => f.storage === PromptsStorage.extension).length; - this._logger.info(` ${section.padEnd(16)} ${String(local).padStart(6)} ${String(user).padStart(6)} ${String(ext).padStart(6)} ${String(filtered.length).padStart(7)}`); + this._logger.info(` ${section.padEnd(16)} ${String(local).padStart(6)} ${String(user).padStart(6)} ${String(ext).padStart(6)} ${String(all.length).padStart(7)}`); } catch { this._logger.info(` ${section.padEnd(16)} (error)`); } } - private async _logSectionDetails(section: AICustomizationManagementSection, type: PromptsType, filter: IStorageSourceFilter): Promise { + private async _logSectionDetails(section: AICustomizationManagementSection, type: PromptsType): Promise { try { // Source folders - where we look for files const sourceFolders = await this._promptsService.getSourceFolders(type); @@ -152,20 +146,18 @@ class CustomizationsDebugLogContribution extends Disposable implements IWorkbenc this._promptsService.listPromptFilesForStorage(type, PromptsStorage.extension, CancellationToken.None), ]); const all: IPromptPath[] = [...localFiles, ...userFiles, ...extensionFiles]; - const filtered = applyStorageSourceFilter(all, filter); - if (filtered.length > 0) { + if (all.length > 0) { if (sourceFolders.length === 0) { this._logger.info(` -- ${section} --`); } - this._logger.info(` Filter: sources=[${filter.sources.join(', ')}]`); - this._logger.info(` Found ${filtered.length} item(s):`); - for (const f of filtered) { + this._logger.info(` Found ${all.length} item(s):`); + for (const f of all) { this._logger.info(` [${f.storage}] ${f.uri.fsPath}`); } } - if (sourceFolders.length > 0 || filtered.length > 0) { + if (sourceFolders.length > 0 || all.length > 0) { this._logger.info(''); } } catch { diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts index 661e19af02a8ed..5c4c08d860909d 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts @@ -18,9 +18,8 @@ import { ActionType } from '../../../../../platform/agentHost/common/state/sessi import { ROOT_STATE_URI, customizationId, type Customization } from '../../../../../platform/agentHost/common/state/sessionState.js'; import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { INotificationService } from '../../../../../platform/notification/common/notification.js'; -import { AICustomizationManagementSection, AICustomizationSources, IAICustomizationWorkspaceService, type IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { AICustomizationManagementSection, IAICustomizationWorkspaceService } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { ICustomizationSyncProvider, type IHarnessDescriptor, type ICustomizationItemAction } from '../../../../../workbench/contrib/chat/common/customizationHarnessService.js'; -import { PromptsType } from '../../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { AgentCustomizationItemProvider } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.js'; import { CustomizationType } from '../../../../../platform/agentHost/common/state/protocol/state.js'; @@ -141,9 +140,6 @@ export function createRemoteAgentHarnessDescriptor( itemProvider: AgentCustomizationItemProvider, syncProvider: ICustomizationSyncProvider, ): IHarnessDescriptor { - const allSources = [AICustomizationSources.local, AICustomizationSources.user, AICustomizationSources.plugin, AICustomizationSources.extension, AICustomizationSources.builtin]; - const filter: IStorageSourceFilter = { sources: allSources }; - return { id: harnessId, label: displayName, @@ -153,9 +149,6 @@ export function createRemoteAgentHarnessDescriptor( AICustomizationManagementSection.McpServers, ], hideGenerateButton: true, - getStorageSourceFilter(_type: PromptsType): IStorageSourceFilter { - return filter; - }, itemProvider, syncProvider, pluginActions: controller.pluginActions, diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts index dc96a33e523866..6345e316c5f2db 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts @@ -21,7 +21,7 @@ import { PromptsType } from '../../../../../../workbench/contrib/chat/common/pro import { NullLogService } from '../../../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../../../platform/notification/common/notification.js'; import { URI } from '../../../../../../base/common/uri.js'; -import { AICustomizationSources, IAICustomizationWorkspaceService } from '../../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IAICustomizationWorkspaceService } from '../../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { SYNCED_CUSTOMIZATION_SCHEME } from '../../../../../../workbench/services/agentHost/common/agentHostFileSystemService.js'; import { RemoteAgentPluginController } from '../../browser/remoteAgentHostCustomizationHarness.js'; import { CustomizationHarnessServiceBase, IHarnessDescriptor } from '../../../../../../workbench/contrib/chat/common/customizationHarnessService.js'; @@ -722,7 +722,6 @@ suite('RemoteAgentHostCustomizationHarness', () => { id: harnessId, label: 'Remote Agent Host (test)', icon: ThemeIcon.fromId(Codicon.remote.id), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.plugin] }), itemProvider: provider, }; const harnessService = disposables.add(new CustomizationHarnessServiceBase([descriptor], harnessId, new MockPromptsService())); diff --git a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts index 743925521eb2ea..3e320b67ef4c64 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts @@ -19,7 +19,7 @@ import { IAgentHostToolSetEnablementService, IToolEnablementState } from '../../ import { IAICustomizationItemsModel, ItemsModelSection } from '../../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemsModel.js'; import { ICustomizationHarnessService, IHarnessDescriptor } from '../../../../../workbench/contrib/chat/common/customizationHarnessService.js'; import { getChatSessionType } from '../../../../../workbench/contrib/chat/common/model/chatUri.js'; -import { AICustomizationManagementSection, AICustomizationSources } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { AICustomizationManagementSection } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IAICustomizationListItem } from '../../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.js'; import { AICustomizationShortcutsWidget } from '../../browser/aiCustomizationShortcutsWidget.js'; import { CUSTOMIZATION_ITEMS, CustomizationLinkViewItem, ICustomizationItemConfig } from '../../browser/customizationsToolbar.contribution.js'; @@ -162,7 +162,6 @@ function createMockHarnessService(hiddenSections: readonly string[] = []): ICust label: 'Fixture', icon: ThemeIcon.fromId('vm'), hiddenSections, - getStorageSourceFilter: () => ({ sources: AICustomizationSources.all }), }; return new class extends mock() { override readonly activeSessionResource = observableValue('mockActiveSessionResource', URI.parse(`${descriptor.id}:///session`)); diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index a42e2afdce8deb..2ea8dcb67d92d3 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -47,7 +47,7 @@ import { ExtHostChatAgentsShape2, ExtHostContext, IChatAgentInvokeResult, IChatS import { NotebookDto } from './mainThreadNotebookDto.js'; import { getChatSessionType, isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.js'; import { ICustomizationHarnessService, ICustomizationItem, ICustomizationItemProvider, IHarnessDescriptor } from '../../contrib/chat/common/customizationHarnessService.js'; -import { AICustomizationManagementSection, AICustomizationSources } from '../../contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { AICustomizationManagementSection } from '../../contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IAgentPlugin, IAgentPluginService } from '../../contrib/chat/common/plugins/agentPluginService.js'; import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js'; @@ -841,11 +841,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA label: metadata.label, icon: metadata.iconId ? ThemeIcon.fromId(metadata.iconId) : ThemeIcon.fromId(Codicon.extensions.id), hiddenSections, - getStorageSourceFilter: () => ({ - // Extension-provided harnesses manage their own items via the provider, - // so we show all sources for storage-filter-based flows. - sources: AICustomizationSources.all - }), itemProvider, }; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts index fd7b78f5617332..810fbfd5d28f43 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts @@ -29,7 +29,7 @@ import { authenticateProtectedResources, AgentHostAuthTokenCache, resolveAuthent import { AgentHostLanguageModelProvider, agentHostProviderSupportsAutoModel } from './agentHostLanguageModelProvider.js'; import { AgentHostSessionHandler } from './agentHostSessionHandler.js'; import { IAgentHostActiveClientService } from './agentHostActiveClientService.js'; -import { AICustomizationManagementSection, AICustomizationSources } from '../../../common/aiCustomizationWorkspaceService.js'; +import { AICustomizationManagementSection } from '../../../common/aiCustomizationWorkspaceService.js'; const LOCAL_AGENT_HOST_SESSION_TYPE_PREFIX = 'agent-host-'; @@ -251,7 +251,6 @@ export class AgentHostContribution extends Disposable implements IWorkbenchContr // The Tools section is surfaced for the Copilot CLI agent host only. hiddenSections: agent.provider === 'copilotcli' ? [] : [AICustomizationManagementSection.Tools], hideGenerateButton: true, - getStorageSourceFilter: () => ({ sources: AICustomizationSources.all }), syncProvider, itemProvider, })); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationDebugPanel.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationDebugPanel.ts index 95711f6434ee80..630e0d837ba880 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationDebugPanel.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationDebugPanel.ts @@ -7,7 +7,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { URI } from '../../../../../base/common/uri.js'; import { IPromptsService, PromptsStorage, IPromptPath } from '../../common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; -import { IAICustomizationWorkspaceService, IStorageSourceFilter, AICustomizationSources, applyStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js'; +import { IAICustomizationWorkspaceService, AICustomizationSources } from '../../common/aiCustomizationWorkspaceService.js'; import { type AICustomizationSource, AICustomizationManagementSection, sectionToPromptType } from './aiCustomizationManagement.js'; import { ICustomizationHarnessService, type ICustomizationItem } from '../../common/customizationHarnessService.js'; import { IAgentPluginService } from '../../common/plugins/agentPluginService.js'; @@ -41,14 +41,12 @@ export async function generateCustomizationDebugReport( ): Promise { const promptType = sectionToPromptType(section); const activeDescriptor = harnessService.getActiveDescriptor(); - const filter = activeDescriptor.getStorageSourceFilter(promptType); const lines: string[] = []; lines.push(`== Customization Debug: ${section} (${promptType}) ==`); lines.push(`Window: ${workspaceService.isSessionsWindow ? 'Sessions' : 'Core VS Code'}`); lines.push(`Active root: ${workspaceService.getActiveProjectRoot()?.fsPath ?? '(none)'}`); lines.push(`Sections: [${workspaceService.managementSections.join(', ')}]`); - lines.push(`Filter sources: [${filter.sources.join(', ')}]`); // Active harness descriptor if (activeDescriptor) { @@ -76,7 +74,7 @@ export async function generateCustomizationDebugReport( lines.push('--- Stage 1: No provider available ---'); lines.push(''); await appendRawServiceData(lines, promptsService, promptType); - await appendFilteredData(lines, promptsService, promptType, filter); + await appendUnfilteredData(lines, promptsService, promptType); } @@ -221,26 +219,15 @@ async function appendRawServiceData(lines: string[], promptsService: IPromptsSer lines.push(''); } -async function appendFilteredData(lines: string[], promptsService: IPromptsService, promptType: PromptsType, filter: IStorageSourceFilter): Promise { - lines.push('--- Stage 2b: After applyStorageSourceFilter ---'); +async function appendUnfilteredData(lines: string[], promptsService: IPromptsService, promptType: PromptsType): Promise { + lines.push('--- Stage 2b: All files (no filtering applied) ---'); const { localFiles, userFiles, extensionFiles } = await getPromptFilesByStorage(promptsService, promptType); const all: IPromptPath[] = [...localFiles, ...userFiles, ...extensionFiles]; - const filtered = applyStorageSourceFilter(all, filter); - lines.push(` Input: ${all.length} → Filtered: ${filtered.length}`); - lines.push(` local: ${filtered.filter(f => f.storage === PromptsStorage.local).length}`); - lines.push(` user: ${filtered.filter(f => f.storage === PromptsStorage.user).length}`); - lines.push(` extension: ${filtered.filter(f => f.storage === PromptsStorage.extension).length}`); - - const removedCount = all.length - filtered.length; - if (removedCount > 0) { - const filteredUris = new Set(filtered.map(f => f.uri.toString())); - const removed = all.filter(f => !filteredUris.has(f.uri.toString())); - lines.push(` Removed (${removedCount}):`); - for (const f of removed) { - lines.push(` [${f.storage}] ${f.uri.fsPath}`); - } - } + lines.push(` Count: ${all.length} total`); + lines.push(` local: ${all.filter(f => f.storage === PromptsStorage.local).length}`); + lines.push(` user: ${all.filter(f => f.storage === PromptsStorage.user).length}`); + lines.push(` extension: ${all.filter(f => f.storage === PromptsStorage.extension).length}`); lines.push(''); } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemsModel.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemsModel.ts index df56233b723907..18e7a2ed0f7904 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemsModel.ts @@ -249,7 +249,7 @@ export class AICustomizationItemsModel extends Disposable implements IAICustomiz } return new PureItemProviderItemSource(sessionResource, descriptor.itemProvider, this.itemNormalizer); } else { - const itemProvider = descriptor.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider, () => descriptor); + const itemProvider = descriptor.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider); return new ItemProviderItemSource( sessionResource, itemProvider, diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 3b1e3cca8210a4..95251decf6c5eb 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -1230,7 +1230,7 @@ export class AICustomizationManagementEditor extends EditorPane { private async resolveTargetDirectoryWithPicker(type: PromptsType, target: 'workspace' | 'user'): Promise { const sessionResource = this.harnessService.activeSessionResource.get(); const activeDescriptor = this.harnessService.getActiveDescriptor(); - const provider = activeDescriptor.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider, () => activeDescriptor); + const provider = activeDescriptor.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider); if (!provider.provideSourceFolders) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts index 04d78eaf6296b0..c9159a2a6b742f 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts @@ -123,7 +123,7 @@ export class CustomizationCreatorService { private async resolveTargetDirectoryWithPicker(type: PromptsType): Promise { const sessionResource = this.harnessService.activeSessionResource.get(); const activeDescriptor = this.harnessService.getActiveDescriptor(); - const provider = activeDescriptor.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider, () => activeDescriptor); + const provider = activeDescriptor.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider); if (!provider.provideSourceFolders) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts index cf3c955414482b..f4fa4d86c56a49 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts @@ -12,12 +12,12 @@ import { basename, dirname } from '../../../../../base/common/resources.js'; import { localize } from '../../../../../nls.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { IAICustomizationWorkspaceService, applySourceFilter, AICustomizationSources } from '../../common/aiCustomizationWorkspaceService.js'; +import { IAICustomizationWorkspaceService, AICustomizationSources } from '../../common/aiCustomizationWorkspaceService.js'; import { HookType, HOOK_METADATA } from '../../common/promptSyntax/hookTypes.js'; import { formatHookCommandLabel } from '../../common/promptSyntax/hookSchema.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { ICustomAgent, IPromptsService, matchesSessionType, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; -import { ICustomizationItem, ICustomizationItemProvider, ICustomizationSourceFolder, IHarnessDescriptor } from '../../common/customizationHarnessService.js'; +import { ICustomizationItem, ICustomizationItemProvider, ICustomizationSourceFolder } from '../../common/customizationHarnessService.js'; import { BUILTIN_STORAGE } from './aiCustomizationManagement.js'; import { getFriendlyName, isChatExtensionItem } from './aiCustomizationItemSource.js'; import { getChatSessionType } from '../../common/model/chatUri.js'; @@ -31,7 +31,6 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt readonly onDidChange: Event; constructor( - private readonly getActiveDescriptor: () => IHarnessDescriptor, @IPromptsService private readonly promptsService: IPromptsService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @IProductService private readonly productService: IProductService, @@ -180,7 +179,7 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt await this.fetchPromptServiceInstructions(items, extensionInfoByUri, disabledUris, promptType); } - return this.applyLocalFilters(this.applyBuiltinGroupKeys(items, extensionInfoByUri), promptType); + return this.applyBuiltinGroupKeys(items, extensionInfoByUri); } private async fetchPromptServiceHooks(items: ICustomizationItem[], disabledUris: ResourceSet, promptType: PromptsType): Promise { @@ -329,12 +328,4 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt }); } - private applyLocalFilters(groupedItems: ICustomizationItem[], promptType: PromptsType): readonly ICustomizationItem[] { - const descriptor = this.getActiveDescriptor(); - const filter = descriptor.getStorageSourceFilter(promptType); - const items = applySourceFilter(groupedItems, filter); - - return items; - } - } diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index 1f1acfa27ab14f..e8de3da96f522f 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -8,7 +8,7 @@ import { IObservable } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { PromptsType } from './promptSyntax/promptTypes.js'; -import { IChatPromptSlashCommand, PromptsStorage } from './promptSyntax/service/promptsService.js'; +import { IChatPromptSlashCommand } from './promptSyntax/service/promptsService.js'; export const IAICustomizationWorkspaceService = createDecorator('aiCustomizationWorkspaceService'); @@ -53,40 +53,11 @@ export type AICustomizationManagementSection = typeof AICustomizationManagementS * Per-type filter policy controlling which storage sources are visible * for a given customization type. */ -export interface IStorageSourceFilter { - /** - * Which storage groups to display (e.g. workspace, user, extension, builtin). - */ - readonly sources: readonly AICustomizationSource[]; -} - -/** - * Controls which features are shown on the welcome page of the - * AI Customization Management Editor. - */ export interface IWelcomePageFeatures { /** Show the "Configure Your AI" getting-started banner. */ readonly showGettingStartedBanner: boolean; } -/** - * Applies a source filter to an array of items that have uri and source. - * Removes items whose source is not in the filter's source list. - */ -export function applySourceFilter(items: readonly T[], filter: IStorageSourceFilter): readonly T[] { - const sourceSet = new Set(filter.sources); - return items.filter(item => sourceSet.has(item.source)); -} - -/** - * Applies a storage filter to an array of items that have uri and storage. - * Removes items whose storage is not in the filter's source list. - */ -export function applyStorageSourceFilter(items: readonly T[], filter: IStorageSourceFilter): readonly T[] { - const sourceSet = new Set(filter.sources); - return items.filter(item => sourceSet.has(item.storage)); -} - /** * Provides workspace context for AI Customization views. */ diff --git a/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts b/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts index c29b6a2ed2fc6a..c29ef8ad2df53b 100644 --- a/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts +++ b/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts @@ -11,7 +11,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { AICustomizationManagementSection, AICustomizationSource, AICustomizationSources, BUILTIN_STORAGE, IStorageSourceFilter } from './aiCustomizationWorkspaceService.js'; +import { AICustomizationManagementSection, AICustomizationSource, BUILTIN_STORAGE } from './aiCustomizationWorkspaceService.js'; import { PromptsType } from './promptSyntax/promptTypes.js'; import { AGENT_MD_FILENAME } from './promptSyntax/config/promptFileLocations.js'; import { IAgentSource, IChatPromptSlashCommand, ICustomAgent, IPromptsService, IResolvedChatPromptSlashCommand, matchesSessionType, PromptsStorage } from './promptSyntax/service/promptsService.js'; @@ -107,11 +107,6 @@ export interface IHarnessDescriptor { * When `undefined`, the harness is always available (e.g. Local). */ readonly requiredAgentId?: string; - /** - * Returns the storage source filter that should be applied to customization - * items of the given type when this harness is active. - */ - getStorageSourceFilter(type: PromptsType): IStorageSourceFilter; /** * When set, this harness is backed by an extension-contributed provider * that can supply customization items directly (bypassing promptsService @@ -368,14 +363,7 @@ export interface ICustomizationSlashCommand { readonly sessionTypes?: readonly string[]; } -// #region Shared filter constants - -/** - * Empty filter returned when no harness is registered yet. - */ -const EMPTY_FILTER: IStorageSourceFilter = { - sources: [], -}; +// #region Shared descriptor constants /** * Empty descriptor returned when no harness is registered yet. @@ -384,7 +372,6 @@ const EMPTY_DESCRIPTOR: IHarnessDescriptor = { id: '', label: '', icon: Codicon.sparkle, - getStorageSourceFilter: () => EMPTY_FILTER, }; @@ -405,7 +392,6 @@ const EMPTY_DESCRIPTOR: IHarnessDescriptor = { * with no user-root restrictions. */ export function createVSCodeHarnessDescriptor(): IHarnessDescriptor { - const filter: IStorageSourceFilter = { sources: AICustomizationSources.all }; return { id: SessionType.Local, label: localize('harness.local', "Local"), @@ -417,7 +403,6 @@ export function createVSCodeHarnessDescriptor(): IHarnessDescriptor { rootFileShortcuts: [AGENT_MD_FILENAME], }], ]), - getStorageSourceFilter: () => filter, }; } diff --git a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationItemsModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationItemsModel.test.ts index de3dc7d0342c51..b137ff64f77095 100644 --- a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationItemsModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationItemsModel.test.ts @@ -16,7 +16,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/ import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; import { AICustomizationItemsModel } from '../../../browser/aiCustomization/aiCustomizationItemsModel.js'; -import { AICustomizationManagementSection, AICustomizationSources, BUILTIN_STORAGE, IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../common/aiCustomizationWorkspaceService.js'; +import { AICustomizationManagementSection, AICustomizationSources, BUILTIN_STORAGE, IAICustomizationWorkspaceService } from '../../../common/aiCustomizationWorkspaceService.js'; import { ICustomizationHarnessService, ICustomizationItem, ICustomizationItemProvider, ICustomizationSyncProvider, IHarnessDescriptor } from '../../../common/customizationHarnessService.js'; import { ContributionEnablementState } from '../../../common/enablement.js'; import { IAgentPluginService, type IAgentPlugin } from '../../../common/plugins/agentPluginService.js'; @@ -49,7 +49,6 @@ suite('AICustomizationItemsModel', () => { id, label: id, icon: Codicon.settingsGear, - getStorageSourceFilter: (): IStorageSourceFilter => ({ sources: [PromptsStorage.local, PromptsStorage.user] }), itemProvider: provider, syncProvider, }; @@ -538,7 +537,6 @@ suite('AICustomizationItemsModel', () => { id: 'A', label: 'A', icon: Codicon.settingsGear, - getStorageSourceFilter: (): IStorageSourceFilter => ({ sources: [PromptsStorage.local, PromptsStorage.user] }), itemProvider: provider, }; const sessionResource = URI.parse('A:///active-session'); @@ -778,7 +776,6 @@ suite('AICustomizationItemsModel', () => { id: sessionType, label: 'Agent Host Test', icon: Codicon.settingsGear, - getStorageSourceFilter: (): IStorageSourceFilter => ({ sources: [] }), itemProvider: provider, }; const sessionResource = URI.parse(`${sessionType}:///active-session`); diff --git a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationListWidget.test.ts b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationListWidget.test.ts index a863b1026ef193..4f04bb2c641b0d 100644 --- a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationListWidget.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationListWidget.test.ts @@ -16,12 +16,12 @@ import { workbenchInstantiationService } from '../../../../../test/browser/workb import { AICustomizationListWidget } from '../../../browser/aiCustomization/aiCustomizationListWidget.js'; import { IAICustomizationItemsModel } from '../../../browser/aiCustomization/aiCustomizationItemsModel.js'; import { extractExtensionIdFromPath, getCustomizationSecondaryText, truncateToFirstLine } from '../../../browser/aiCustomization/aiCustomizationListWidgetUtils.js'; -import { AICustomizationManagementSection, IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../common/aiCustomizationWorkspaceService.js'; +import { AICustomizationManagementSection, IAICustomizationWorkspaceService } from '../../../common/aiCustomizationWorkspaceService.js'; import { ICustomizationHarnessService, IHarnessDescriptor } from '../../../common/customizationHarnessService.js'; import { ContributionEnablementState } from '../../../common/enablement.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; import { IAgentPluginService } from '../../../common/plugins/agentPluginService.js'; -import { IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; +import { IPromptsService } from '../../../common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { ResourceSet } from '../../../../../../base/common/map.js'; @@ -164,7 +164,6 @@ suite('aiCustomizationListWidget', () => { id: 'test', label: 'Test', icon: Codicon.settingsGear, - getStorageSourceFilter: (): IStorageSourceFilter => ({ sources: [PromptsStorage.local, PromptsStorage.user] }), itemProvider: { onDidChange: Event.None, provideChatSessionCustomizations: (sessionResource: URI, token: CancellationToken) => Promise.resolve(undefined), diff --git a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/applyStorageSourceFilter.test.ts b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/applyStorageSourceFilter.test.ts deleted file mode 100644 index 038ac28eaf88b8..00000000000000 --- a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/applyStorageSourceFilter.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { URI } from '../../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { AICustomizationSource, AICustomizationSources, applySourceFilter, IStorageSourceFilter } from '../../../common/aiCustomizationWorkspaceService.js'; - -function item(path: string, source: AICustomizationSource): { uri: URI; source: AICustomizationSource } { - return { uri: URI.file(path), source }; -} - -suite('applyStorageSourceFilter', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('source filtering', () => { - test('keeps items matching sources', () => { - const items = [ - item('/w/a.md', AICustomizationSources.local), - item('/u/b.md', AICustomizationSources.user), - item('/e/c.md', AICustomizationSources.extension), - ]; - const filter: IStorageSourceFilter = { - sources: [AICustomizationSources.local, AICustomizationSources.user, AICustomizationSources.extension], - }; - assert.strictEqual(applySourceFilter(items, filter).length, 3); - }); - - test('removes items not in sources', () => { - const items = [ - item('/w/a.md', AICustomizationSources.local), - item('/u/b.md', AICustomizationSources.user), - item('/e/c.md', AICustomizationSources.extension), - item('/p/d.md', AICustomizationSources.plugin), - ]; - const filter: IStorageSourceFilter = { - sources: [AICustomizationSources.local], - }; - const result = applySourceFilter(items, filter); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].uri.toString(), URI.file('/w/a.md').toString()); - }); - - test('empty sources removes everything', () => { - const items = [ - item('/w/a.md', AICustomizationSources.local), - item('/u/b.md', AICustomizationSources.user), - ]; - const filter: IStorageSourceFilter = { sources: [] }; - assert.strictEqual(applySourceFilter(items, filter).length, 0); - }); - - test('empty items returns empty', () => { - const filter: IStorageSourceFilter = { - sources: [AICustomizationSources.local, AICustomizationSources.user], - }; - assert.strictEqual(applySourceFilter([], filter).length, 0); - }); - }); - - suite('combined filtering', () => { - test('sessions-like filter: hooks show only local', () => { - const items = [ - item('/w/.github/hooks/pre.json', AICustomizationSources.local), - item('/home/.claude/settings.json', AICustomizationSources.user), - ]; - const filter: IStorageSourceFilter = { - sources: [AICustomizationSources.local], - }; - const result = applySourceFilter(items, filter); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].source, AICustomizationSources.local); - }); - - test('show multiple sources together', () => { - const items = [ - item('/w/a.md', AICustomizationSources.local), - item('/home/.copilot/b.md', AICustomizationSources.user), - item('/home/.vscode/c.md', AICustomizationSources.user), - item('/e/d.md', AICustomizationSources.extension), - item('/p/e.md', AICustomizationSources.plugin), - ]; - const filter: IStorageSourceFilter = { - sources: [AICustomizationSources.local, AICustomizationSources.user], - }; - const result = applySourceFilter(items, filter); - // local + all users kept, extension + plugin excluded - assert.strictEqual(result.length, 3); - }); - - test('core-like filter: show everything', () => { - const items = [ - item('/w/a.md', AICustomizationSources.local), - item('/u/b.md', AICustomizationSources.user), - item('/e/c.md', AICustomizationSources.extension), - item('/p/d.md', AICustomizationSources.plugin), - ]; - const filter: IStorageSourceFilter = { - sources: [AICustomizationSources.local, AICustomizationSources.user, AICustomizationSources.extension, AICustomizationSources.plugin], - }; - assert.strictEqual(applySourceFilter(items, filter).length, 4); - }); - - test('core-like filter with builtin: extension items pass when both extension and builtin are in sources', () => { - // Items from the chat extension have storage=extension but groupKey=builtin. - // The filter operates on storage, so extension items pass through regardless of groupKey. - const items = [ - item('/w/a.md', AICustomizationSources.local), - item('/e/builtin-agent.md', AICustomizationSources.extension), - item('/e/third-party.md', AICustomizationSources.extension), - item('/b/sessions-builtin.md', AICustomizationSources.builtin), - ]; - const filter: IStorageSourceFilter = { - sources: [AICustomizationSources.local, AICustomizationSources.extension, AICustomizationSources.builtin], - }; - const result = applySourceFilter(items, filter); - assert.strictEqual(result.length, 4); - }); - - test('builtin source is respected independently', () => { - const items = [ - item('/e/from-extension.md', AICustomizationSources.extension), - item('/b/from-sessions.md', AICustomizationSources.builtin), - ]; - // Only builtin in sources — extension items excluded - const filter: IStorageSourceFilter = { - sources: [AICustomizationSources.builtin], - }; - const result = applySourceFilter(items, filter); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].source, AICustomizationSources.builtin); - }); - }); - - suite('type safety', () => { - test('works with objects that have extra properties', () => { - const items = [ - { uri: URI.file('/w/a.md'), source: AICustomizationSources.local, name: 'A', extra: true }, - { uri: URI.file('/u/b.md'), source: AICustomizationSources.user, name: 'B', extra: false }, - ]; - const filter: IStorageSourceFilter = { - sources: [AICustomizationSources.local], - }; - const result = applySourceFilter(items, filter); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].name, 'A'); - }); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts b/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts index 600215bcfc63cb..59b59d2928c008 100644 --- a/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts @@ -14,7 +14,6 @@ import { ICustomAgent, IPromptsService, PromptsStorage } from '../../common/prom import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { SessionType } from '../../common/chatSessionsService.js'; import { MockPromptsService } from './promptSyntax/service/mockPromptsService.js'; -import { AICustomizationSources } from '../../common/aiCustomizationWorkspaceService.js'; suite('CustomizationHarnessService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -47,7 +46,6 @@ suite('CustomizationHarnessService', () => { id: harnessId, label: 'Test Harness', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -73,7 +71,6 @@ suite('CustomizationHarnessService', () => { id: harnessId, label: 'Test Harness', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -100,7 +97,6 @@ suite('CustomizationHarnessService', () => { id: 'test-ext', label: 'Test Extension', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -122,7 +118,6 @@ suite('CustomizationHarnessService', () => { id: 'test-ext', label: 'Test Extension', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -144,7 +139,6 @@ suite('CustomizationHarnessService', () => { id: 'test-ext', label: 'Test Extension', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -168,7 +162,6 @@ suite('CustomizationHarnessService', () => { id: 'test-ext', label: 'Test Extension', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -190,12 +183,10 @@ suite('CustomizationHarnessService', () => { const service = createService(); const emitter = new Emitter(); store.add(emitter); - const customFilter = { sources: [PromptsStorage.local, PromptsStorage.user] }; const externalDescriptor: IHarnessDescriptor = { id: 'test-ext', label: 'Test Extension', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => customFilter, itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -205,7 +196,6 @@ suite('CustomizationHarnessService', () => { store.add(service.registerExternalHarness(externalDescriptor)); service.setActiveSession(activeSessionResource); - assert.deepStrictEqual(service.getActiveDescriptor().getStorageSourceFilter(PromptsType.agent), customFilter); }); test('external harness item provider returns items', async () => { @@ -225,7 +215,6 @@ suite('CustomizationHarnessService', () => { id: 'test-ext', label: 'Test Extension', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider, }; const activeSessionResource = URI.parse('test-ext://session'); @@ -246,7 +235,6 @@ suite('CustomizationHarnessService', () => { id: 'cli', label: 'Copilot CLI (static)', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), }; const service = createService( createVSCodeHarnessDescriptor(), @@ -260,7 +248,6 @@ suite('CustomizationHarnessService', () => { id: 'cli', label: 'Copilot CLI (from API)', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -281,7 +268,6 @@ suite('CustomizationHarnessService', () => { id: 'cli', label: 'Copilot CLI (static)', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), }; const service = createService( createVSCodeHarnessDescriptor(), @@ -294,7 +280,6 @@ suite('CustomizationHarnessService', () => { id: 'cli', label: 'Copilot CLI (from API)', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -315,7 +300,6 @@ suite('CustomizationHarnessService', () => { id: 'cli', label: 'Copilot CLI (static)', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), }; const service = createService( createVSCodeHarnessDescriptor(), @@ -328,7 +312,6 @@ suite('CustomizationHarnessService', () => { id: 'cli', label: 'Copilot CLI (from API)', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [], @@ -360,7 +343,6 @@ suite('CustomizationHarnessService', () => { id: testSessionType, label: 'Test Extension', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [ @@ -390,7 +372,6 @@ suite('CustomizationHarnessService', () => { id: testSessionType, label: 'Test Extension', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.plugin] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [ @@ -481,7 +462,6 @@ suite('CustomizationHarnessService', () => { id: testSessionType1, label: 'Test Extension', icon: ThemeIcon.fromId('extensions'), - getStorageSourceFilter: () => ({ sources: [AICustomizationSources.local] }), itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async (_sessionResource: URI, _token: CancellationToken) => [ From f3eeafdc5f0381f76b75b6531d925cc5bec2683d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:05:55 +0200 Subject: [PATCH 035/589] Hide new-session tours while a modal editor is visible (#323479) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/onboardingTours/browser/tours/newSessionTour.ts | 5 ++++- .../onboardingTours/browser/tours/newSessionViewTour.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/sessions/contrib/onboardingTours/browser/tours/newSessionTour.ts b/src/vs/sessions/contrib/onboardingTours/browser/tours/newSessionTour.ts index ae1946dbfa6c36..03861fa644f2e9 100644 --- a/src/vs/sessions/contrib/onboardingTours/browser/tours/newSessionTour.ts +++ b/src/vs/sessions/contrib/onboardingTours/browser/tours/newSessionTour.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IObservable } from '../../../../../base/common/observable.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { EditorPartModalContext } from '../../../../../workbench/common/contextkeys.js'; import { ChatContextKeys } from '../../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { IOnboardingScenario } from '../../../../../workbench/contrib/onboarding/common/onboardingScenario.js'; import { ISpotlightPayload, SPOTLIGHT_PRESENTATION_KIND } from '../../../../../workbench/contrib/onboarding/browser/spotlight/spotlightTypes.js'; @@ -59,12 +61,13 @@ const newSessionPayload: ISpotlightPayload = { * {@link NewSessionTourContribution}, which flips it after the eligible user * presses the pulsing New Session button. * `ChatContextKeys.enabled` keeps the tour hidden when AI features are disabled. + * The modal-editor gate keeps the tour hidden while a modal editor is showing. */ export function createNewSessionTour(signal: IObservable): IOnboardingScenario { return { id: NEW_SESSION_TOUR_ID, seenKey: NEW_SESSION_ONBOARDING_SEEN_KEY, - when: ChatContextKeys.enabled, + when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorPartModalContext.toNegated()), trigger: { kind: 'observable', signal }, priority: 100, presentation: { diff --git a/src/vs/sessions/contrib/onboardingTours/browser/tours/newSessionViewTour.ts b/src/vs/sessions/contrib/onboardingTours/browser/tours/newSessionViewTour.ts index e05c0a7f9035e3..3a64e77fd6722c 100644 --- a/src/vs/sessions/contrib/onboardingTours/browser/tours/newSessionViewTour.ts +++ b/src/vs/sessions/contrib/onboardingTours/browser/tours/newSessionViewTour.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IObservable } from '../../../../../base/common/observable.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { EditorPartModalContext } from '../../../../../workbench/common/contextkeys.js'; import { ChatContextKeys } from '../../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { IOnboardingScenario } from '../../../../../workbench/contrib/onboarding/common/onboardingScenario.js'; import { ISpotlightPayload, SPOTLIGHT_PRESENTATION_KIND } from '../../../../../workbench/contrib/onboarding/browser/spotlight/spotlightTypes.js'; @@ -71,6 +73,7 @@ const newSessionViewPayload: ISpotlightPayload = { * {@link NewSessionViewTourContribution}, which flips it once an eligible * (brand-new) user has the new-session view open and rendered. * `ChatContextKeys.enabled` keeps the tour hidden when AI features are disabled. + * The modal-editor gate keeps the tour hidden while a modal editor is showing. * * Shares {@link NEW_SESSION_ONBOARDING_SEEN_KEY} with the pulsing-button * {@link createNewSessionTour} variant, so a user who has seen either tour is @@ -80,7 +83,7 @@ export function createNewSessionViewTour(signal: IObservable): IOnboard return { id: NEW_SESSION_VIEW_TOUR_ID, seenKey: NEW_SESSION_ONBOARDING_SEEN_KEY, - when: ChatContextKeys.enabled, + when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorPartModalContext.toNegated()), trigger: { kind: 'observable', signal }, priority: 100, presentation: { From b802a62be50dba26931409d45e7094884639179a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 29 Jun 2026 23:29:49 +1000 Subject: [PATCH 036/589] Remove last-used session type and agent logic (#323484) * remove last-used session type logic and related tests * refactor: remove last-used session type logic from chat editor input * refactor: remove unused storage service dependency from ChatEditorInput --- .../chat/browser/actions/chatActions.ts | 7 +-- .../chat/browser/widget/chatWidgetService.ts | 29 +--------- .../browser/widget/input/chatInputPart.ts | 39 +------------ .../widget/input/chatModelSelectionLogic.ts | 18 ------ .../widgetHosts/editor/chatEditorInput.ts | 7 +-- .../contrib/chat/common/constants.ts | 48 ---------------- .../input/chatModelSelectionLogic.test.ts | 31 ----------- .../chat/test/common/constants.test.ts | 55 +------------------ 8 files changed, 7 insertions(+), 227 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index c40ce7948d584b..c400fe1dbf7205 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -26,7 +26,6 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js'; -import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -57,7 +56,7 @@ import { ElicitationState, IChatService, IChatToolInvocation } from '../../commo import { ISCMHistoryItemChangeRangeVariableEntry, ISCMHistoryItemChangeVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/model/chatViewModel.js'; import { IChatWidgetHistoryService } from '../../common/widget/chatWidgetHistoryService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatLastUsedEditorSessionTypeStorageKey, ChatModeKind, getDefaultNewChatSessionType, getNewChatEditorSessionResource } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, getDefaultNewChatSessionResource, getDefaultNewChatSessionType } from '../../common/constants.js'; import { AICustomizationManagementCommands } from '../aiCustomization/aiCustomizationManagement.js'; import { ILanguageModelChatSelector, ILanguageModelsService } from '../../common/languageModels.js'; import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js'; @@ -579,9 +578,7 @@ export function registerChatActions() { * `local` or the chosen provider is unavailable. */ function getNewChatEditorSessionUri(accessor: ServicesAccessor): URI { - const storageService = accessor.get(IStorageService); - const lastUsedSessionType = storageService.get(ChatLastUsedEditorSessionTypeStorageKey, StorageScope.PROFILE); - return getNewChatEditorSessionResource(accessor.get(IConfigurationService), accessor.get(IChatSessionsService), lastUsedSessionType); + return getDefaultNewChatSessionResource(accessor.get(IConfigurationService), accessor.get(IChatSessionsService)); } registerAction2(PrimaryOpenChatGlobalAction); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts index 69f9f38f5b24ff..fc680aa9a89e75 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts @@ -11,14 +11,11 @@ import { isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ACTIVE_GROUP, IEditorService, type PreferredGroup } from '../../../../services/editor/common/editorService.js'; import { IEditorGroup, IEditorGroupsService, isEditorGroup } from '../../../../services/editor/common/editorGroupsService.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { IChatService } from '../../common/chatService/chatService.js'; -import { localChatSessionType } from '../../common/chatSessionsService.js'; -import { ChatAgentLocation, ChatLastUsedEditorSessionTypeStorageKey } from '../../common/constants.js'; -import { getChatSessionType } from '../../common/model/chatUri.js'; +import { ChatAgentLocation } from '../../common/constants.js'; import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService, IQuickChatService, isIChatViewViewContext } from '../chat.js'; import { ChatEditor, IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; @@ -51,7 +48,6 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService @IEditorService private readonly editorService: IEditorService, @IChatService private readonly chatService: IChatService, @ILogService private readonly logService: ILogService, - @IStorageService private readonly storageService: IStorageService, ) { super(); } @@ -237,32 +233,10 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService } this._lastFocusedWidget = widget; - this.recordLastUsedSessionType(widget); this._onDidChangeFocusedWidget.fire(widget); this._onDidChangeFocusedSession.fire(); } - /** - * Stores the session type (agent) of the last-focused user-facing chat so new chat editors can reuse it (excluding Quick Chat). - */ - private recordLastUsedSessionType(widget: IChatWidget | undefined): void { - const sessionResource = widget?.viewModel?.sessionResource; - if (!sessionResource) { - return; - } - if (this.quickChatService.sessionResource && isEqual(sessionResource, this.quickChatService.sessionResource)) { - return; - } - const sessionType = getChatSessionType(sessionResource); - // Only remember non-local agents to avoid clobbering the user's last-used agent. - if (sessionType === localChatSessionType) { - return; - } - this.storageService.store(ChatLastUsedEditorSessionTypeStorageKey, sessionType, StorageScope.PROFILE, StorageTarget.USER); - // Also remember this session's current model as the last-used model for its type. - widget?.input.recordCurrentModelAsSessionTypeDefault(); - } - register(newWidget: IChatWidget): IDisposable { if (this._widgets.some(widget => widget === newWidget)) { throw new Error('Cannot register the same widget multiple times'); @@ -279,7 +253,6 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)), newWidget.onDidChangeViewModel(({ previousSessionResource, currentSessionResource }) => { if (this._lastFocusedWidget === newWidget && !isEqual(previousSessionResource, currentSessionResource)) { - this.recordLastUsedSessionType(newWidget); this._onDidChangeFocusedSession.fire(); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 3b2a69bef47903..f8723f16a3952e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -90,7 +90,7 @@ import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../common/languageModels.js'; import { ChatModelConfigurationStore } from './chatModelConfigurationStore.js'; import { IChatModelInputState, IChatRequestModeInfo, IInputModel, logChangesToStateModel } from '../../../common/model/chatModel.js'; -import { filterModelsForSession, findBestMatchingModel, findDefaultModel, hasModelsTargetingSession, isModelValidForSession, mergeModelsWithCache, resolveConfiguredModel, resolveModelFromSyncState, shouldRecordSessionTypeDefaultModel, shouldResetModelToDefault, shouldResetOnModelListChange, shouldRestoreLateArrivingModel, shouldRestorePersistedModel } from './chatModelSelectionLogic.js'; +import { filterModelsForSession, findBestMatchingModel, findDefaultModel, hasModelsTargetingSession, isModelValidForSession, mergeModelsWithCache, resolveConfiguredModel, resolveModelFromSyncState, shouldResetModelToDefault, shouldResetOnModelListChange, shouldRestoreLateArrivingModel, shouldRestorePersistedModel } from './chatModelSelectionLogic.js'; import { chatSessionResourceToId, getChatSessionType, LocalChatSessionUri } from '../../../common/model/chatUri.js'; import { IChatResponseViewModel, isResponseVM } from '../../../common/model/chatViewModel.js'; import { IChatAgentService } from '../../../common/participants/chatAgents.js'; @@ -907,21 +907,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge || hasModelsTargetingSession(this.getAllMergedModels(), sessionType); } - /** - * Returns the persisted model for the current session type, if it exists in the current model pool. - */ - private _getRememberedSessionTypeModel(): ILanguageModelChatMetadataAndIdentifier | undefined { - const sessionType = this._currentSessionType; - if (!sessionType || !this.sessionTypeHasOwnModelPool(sessionType)) { - return undefined; - } - const persisted = this.storageService.get(this.getSelectedModelStorageKey(), StorageScope.APPLICATION); - if (!persisted) { - return undefined; - } - return this.getModels().find(m => m.identifier === persisted); - } - private initSelectedModel() { // initSelectedModel is scoped to the current storage key/session type. // Do not let a delayed restore from a previous session type apply later. @@ -1343,13 +1328,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (!state && this._chatSessionIsEmpty) { state = this._emptyInputState.read(undefined); message = `syncing from empty input state for ${forSessionResource.toString()}`; - // Seed model/config from the last-used selection for this session type (configured default still wins). - const rememberedSessionTypeModel = this._getRememberedSessionTypeModel(); - if (rememberedSessionTypeModel) { - const base = state ?? this.getCurrentInputState(); - const rememberedModelConfiguration = this._modelConfigStore.getModelConfiguration(rememberedSessionTypeModel.identifier); - state = { ...base, selectedModel: rememberedSessionTypeModel, modelConfiguration: rememberedModelConfiguration }; - } // A configured default model (e.g. set by enterprise policy via // `chat.defaultModel`) starts every NEW conversation and // must win over the remembered empty-input draft model. `initSelectedModel` @@ -1593,21 +1571,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._syncInputStateToModel(); } - /** - * Record the focused session's resolved model as the last-used model for its session type. - * Skips local sessions, session types without their own pool, and unresolved/out-of-pool models. - */ - public recordCurrentModelAsSessionTypeDefault(): void { - const sessionType = this._currentSessionType; - const hasOwnPool = !!sessionType && this.sessionTypeHasOwnModelPool(sessionType); - const model = this._currentLanguageModel.get(); - if (!model || !shouldRecordSessionTypeDefaultModel(sessionType, hasOwnPool, model, this.getModels())) { - return; - } - this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER); - this.storageService.store(this.getSelectedModelIsDefaultStorageKey(), !!model.metadata.isDefaultForLocation[this.location], StorageScope.APPLICATION, StorageTarget.USER); - } - private checkModelSupported(): void { const lm = this._currentLanguageModel.get(); const allModels = this.getAllMergedModels(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts index 7ea0c268672402..c79f7ce11599c3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts @@ -83,24 +83,6 @@ export function hasModelsTargetingSession( return allModels.some(m => m.metadata.targetChatSessionType === sessionType); } -/** - * Decide whether a focused session's current model should be recorded as the last-used model for - * its session type. Only records non-local sessions that own a model pool and whose model is - * resolved and present in the pool, so a transient "Auto" during async model-list load cannot - * clobber the remembered selection. - */ -export function shouldRecordSessionTypeDefaultModel( - sessionType: string | undefined, - hasOwnPool: boolean, - model: ILanguageModelChatMetadataAndIdentifier | undefined, - models: ILanguageModelChatMetadataAndIdentifier[], -): boolean { - if (!sessionType || sessionType === 'local' || !hasOwnPool || !model) { - return false; - } - return models.some(m => m.identifier === model.identifier); -} - /** * Check if a model is valid for the current session's model pool. * If the session has targeted models, the model must target that session type. diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts index f4fa2c80e2cb3c..d3b1afa932ddfc 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts @@ -16,13 +16,12 @@ import * as nls from '../../../../../../nls.js'; import { ConfirmResult, IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IStorageService, StorageScope } from '../../../../../../platform/storage/common/storage.js'; import { registerIcon } from '../../../../../../platform/theme/common/iconRegistry.js'; import { EditorInputCapabilities, IEditorIdentifier, IEditorSerializer, IUntypedEditorInput, Verbosity } from '../../../../../common/editor.js'; import { EditorInput, IEditorCloseHandler } from '../../../../../common/editor/editorInput.js'; import { IChatModelReference, IChatService } from '../../../common/chatService/chatService.js'; import { IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; -import { ChatAgentLocation, ChatEditorTitleMaxLength, ChatLastUsedEditorSessionTypeStorageKey, getDefaultNewChatSessionResource, getDefaultNewChatSessionType, getNewChatEditorSessionResource } from '../../../common/constants.js'; +import { ChatAgentLocation, ChatEditorTitleMaxLength, getDefaultNewChatSessionResource, getDefaultNewChatSessionType } from '../../../common/constants.js'; import { IChatEditingSession, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { IChatModel } from '../../../common/model/chatModel.js'; import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/chatUri.js'; @@ -66,7 +65,6 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler @IConfigurationService private readonly configurationService: IConfigurationService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IStorageService private readonly storageService: IStorageService, ) { super(); @@ -233,8 +231,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: true, debugOwner: 'ChatEditorInput#resolveNewLocalSession' }); } } else if (!this.options.target) { - const lastUsedSessionType = this.storageService.get(ChatLastUsedEditorSessionTypeStorageKey, StorageScope.PROFILE); - const defaultResource = getNewChatEditorSessionResource(this.configurationService, this.chatSessionsService, lastUsedSessionType); + const defaultResource = getDefaultNewChatSessionResource(this.configurationService, this.chatSessionsService); if (getChatSessionType(defaultResource) === localChatSessionType) { this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: !inputType, debugOwner: 'ChatEditorInput#resolveUntitled' }); } else { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 751d39980801d5..231e21efe41ee7 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -284,54 +284,6 @@ export function getDefaultNewChatSessionResource( : URI.from({ scheme: defaultType, path: `/untitled-${generateUuid()}` }); } -/** - * Storage key for the last-used non-local editor chat session type (agent), persisted at profile scope. - */ -export const ChatLastUsedEditorSessionTypeStorageKey = 'chat.lastUsedEditorSessionType'; - -/** - * Resolves the session type (agent) for a new chat editor, preferring the last-used visible non-local agent when `chat.editor.defaultProvider` isn't explicitly configured. - */ -export function getNewChatEditorSessionType( - configurationService: IConfigurationService, - chatSessionsService: Pick, - lastUsedSessionType: string | undefined, -): string { - const inspected = configurationService.inspect(ChatConfiguration.EditorDefaultProvider); - const explicitlyConfigured = inspected.applicationValue !== undefined - || inspected.userValue !== undefined - || inspected.userLocalValue !== undefined - || inspected.userRemoteValue !== undefined - || inspected.workspaceValue !== undefined - || inspected.workspaceFolderValue !== undefined - || inspected.memoryValue !== undefined - || inspected.policyValue !== undefined; - - if (!explicitlyConfigured - && lastUsedSessionType - && lastUsedSessionType !== localChatSessionType - && isVisibleEditorChatSessionType(lastUsedSessionType, configurationService, chatSessionsService)) { - return lastUsedSessionType; - } - - return getDefaultNewChatSessionType(configurationService, chatSessionsService); -} - -/** - * Like {@link getDefaultNewChatSessionResource}, but prefers the user's - * last-used session type via {@link getNewChatEditorSessionType}. - */ -export function getNewChatEditorSessionResource( - configurationService: IConfigurationService, - chatSessionsService: Pick, - lastUsedSessionType: string | undefined, -): URI { - const sessionType = getNewChatEditorSessionType(configurationService, chatSessionsService, lastUsedSessionType); - return sessionType === localChatSessionType - ? LocalChatSessionUri.getNewSessionUri() - : URI.from({ scheme: sessionType, path: `/untitled-${generateUuid()}` }); -} - export function isEditorLocalAgentEnabled(configurationService: IConfigurationService): boolean { return configurationService.getValue(ChatConfiguration.EditorLocalAgentEnabled) ?? true; } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts index 4d08e51d0139c9..c6a572fb3edf83 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts @@ -21,7 +21,6 @@ import { mergeModelsWithCache, resolveConfiguredModel, resolveModelFromSyncState, - shouldRecordSessionTypeDefaultModel, shouldResetModelToDefault, shouldResetOnModelListChange, shouldRestoreLateArrivingModel, @@ -1842,34 +1841,4 @@ suite('ChatModelSelectionLogic', () => { assert.strictEqual(reason({ trusted: true, requiresSetup: true, pickerModels: [gpt], liveModelIds: new Set([gpt.identifier]) }), undefined); }); }); - - suite('shouldRecordSessionTypeDefaultModel', () => { - const sessionType = 'agent-host-copilotcli'; - const opus = createSessionModel('claude-opus-4.8', 'Opus 4.8', sessionType); - const pool = [opus]; - - test('records a resolved non-local model present in the pool', () => { - assert.strictEqual(shouldRecordSessionTypeDefaultModel(sessionType, true, opus, pool), true); - }); - - test('skips local sessions', () => { - assert.strictEqual(shouldRecordSessionTypeDefaultModel('local', true, opus, pool), false); - }); - - test('skips when session type has no own pool', () => { - assert.strictEqual(shouldRecordSessionTypeDefaultModel(sessionType, false, opus, pool), false); - }); - - test('skips when no model is resolved (transient Auto during load)', () => { - assert.strictEqual(shouldRecordSessionTypeDefaultModel(sessionType, true, undefined, pool), false); - }); - - test('skips when the model is not in the pool', () => { - assert.strictEqual(shouldRecordSessionTypeDefaultModel(sessionType, true, opus, []), false); - }); - - test('skips when there is no session type', () => { - assert.strictEqual(shouldRecordSessionTypeDefaultModel(undefined, true, opus, pool), false); - }); - }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/constants.test.ts b/src/vs/workbench/contrib/chat/test/common/constants.test.ts index d2f6a701f2c887..2bc66b545212e2 100644 --- a/src/vs/workbench/contrib/chat/test/common/constants.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/constants.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { ChatConfiguration, getDefaultNewChatSessionType, getNewChatEditorSessionType, isVisibleEditorChatSessionType } from '../../common/constants.js'; +import { ChatConfiguration, getDefaultNewChatSessionType, isVisibleEditorChatSessionType } from '../../common/constants.js'; import { localChatSessionType, SessionType, IChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; import { MockChatSessionsService } from './mockChatSessionsService.js'; @@ -77,56 +77,3 @@ suite('ChatConfiguration defaults', () => { assert.strictEqual(isVisibleEditorChatSessionType(localChatSessionType, configurationService, chatSessionsService), true); }); }); - -suite('getNewChatEditorSessionType (last-used agent)', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - function createChatSessionsService(...types: string[]): MockChatSessionsService { - const service = new MockChatSessionsService(); - service.setContributions(types.map(type => ({ - type, - name: type, - displayName: type, - description: type, - } satisfies IChatSessionsExtensionPoint))); - return service; - } - - test('prefers a visible non-local last-used agent when the provider is not explicitly configured', () => { - const configurationService = new TestConfigurationService(); - const chatSessionsService = createChatSessionsService(SessionType.AgentHostCopilot); - - assert.strictEqual(getNewChatEditorSessionType(configurationService, chatSessionsService, SessionType.AgentHostCopilot), SessionType.AgentHostCopilot); - }); - - test('ignores a last-used agent whose provider is not visible/registered', () => { - const configurationService = new TestConfigurationService(); - const chatSessionsService = createChatSessionsService(); // agent host not registered - - assert.strictEqual(getNewChatEditorSessionType(configurationService, chatSessionsService, SessionType.AgentHostCopilot), localChatSessionType); - }); - - test('an explicitly configured local provider wins over a last-used agent', () => { - const configurationService = new TestConfigurationService({ - [ChatConfiguration.EditorDefaultProvider]: 'local', - }); - const chatSessionsService = createChatSessionsService(SessionType.AgentHostCopilot); - - assert.strictEqual(getNewChatEditorSessionType(configurationService, chatSessionsService, SessionType.AgentHostCopilot), localChatSessionType); - }); - - test('a last-used local agent does not override the default', () => { - const configurationService = new TestConfigurationService(); - const chatSessionsService = createChatSessionsService(SessionType.AgentHostCopilot); - - assert.strictEqual(getNewChatEditorSessionType(configurationService, chatSessionsService, localChatSessionType), localChatSessionType); - }); - - test('falls back to the default when there is no last-used agent', () => { - const configurationService = new TestConfigurationService(); - const chatSessionsService = createChatSessionsService(SessionType.AgentHostCopilot); - - assert.strictEqual(getNewChatEditorSessionType(configurationService, chatSessionsService, undefined), localChatSessionType); - }); -}); From 4b6f5e55bb8918a09130b4eb60e82e5926d7e498 Mon Sep 17 00:00:00 2001 From: Robo Date: Mon, 29 Jun 2026 22:53:48 +0900 Subject: [PATCH 037/589] chore: bump electron@42.5.0 (#321629) * chore: bump electron@42.4.0 * chore: apply temp dir workaround for short paths * chore: use 24.15.x for CI node * chore: update nodejs build * chore: bump electron@42.5.0 * fix: unblock playwright install on node 24.17 Node 24.16+ made Readable pause()/resume() a no-op on destroyed streams which makes yauzl 2.x / extract-zip 2.x and older playwright extraction hang forever. - extensions/copilot: add "yauzl": "^3.3.1" override (was missed by #318682) so electron and @vscode/vsce no longer resolve the broken yauzl 2.10, fixing the hung `npm ci` in the Copilot and Extract chat-lib pipelines. - extensions/copilot: bump electron ^39.8.5 -> ^42.5.0 so its install script uses the native @electron-internal/extract-zip instead of extract-zip. - bump @playwright/test ^1.56.1 -> ^1.61.1 so `playwright install` uses the fixed extractor, unblocking the "Download Electron and Playwright" step in all electron test pipelines. * chore: update build * agentHost: fix macOS sandbox smoke sentinel parsing On macOS CI, the AgentHost sandbox smoke test resolves the shell to /bin/sh, which uses the sentinel-based completion path. In that path, the parser could consume the echoed sentinel command text (`<<>>`) before the real numeric marker arrived, causing a false `Exit code: -1` failure even though the command later completed successfully. Harden the sentinel parser to ignore echoed/non-numeric sentinel text and use the latest complete numeric marker instead. Also force the macOS AgentHost sandbox smoke test to use /bin/sh and assert that in the suite log so local runs exercise the same path as CI. Adds a regression test for echoed sentinel command text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: update screenshot baseline after playwright bump * chore: bump distro * chore: fix typecheck * chore: bump distro --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .npmrc | 4 +- .nvmrc | 2 +- build/azure-pipelines/linux/setup-env.sh | 8 +- build/checksums/electron.txt | 150 ++-- build/checksums/nodejs.txt | 12 +- build/lib/electron.ts | 3 +- build/lib/preLaunch.ts | 3 +- build/linux/debian/dep-lists.ts | 1 + build/linux/dependencies-generator.ts | 2 +- cgmanifest.json | 16 +- extensions/copilot/.nvmrc | 2 +- extensions/copilot/package-lock.json | 824 ++++++------------ extensions/copilot/package.json | 7 +- package-lock.json | 91 +- package.json | 6 +- remote/.npmrc | 4 +- scripts/test-remote-integration.bat | 4 + scripts/test-web-integration.bat | 4 + .../node/copilot/copilotShellTools.ts | 32 +- .../test/node/copilotShellTools.test.ts | 45 +- test/automation/src/playwrightDriver.ts | 4 +- .../blocks-ci-screenshots.md | 12 +- .../playwright/package-lock.json | 24 +- .../componentFixtures/playwright/package.json | 2 +- test/mcp/src/automationTools/windows.ts | 2 +- .../areas/agentsWindow/agentsWindow.test.ts | 16 + test/smoke/src/main.ts | 14 +- 27 files changed, 506 insertions(+), 788 deletions(-) diff --git a/.npmrc b/.npmrc index 8c21e58ef14d5f..ef2f0e8c263233 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="42.3.0" -ms_build_id="14159160" +target="42.5.0" +ms_build_id="14500164" runtime="electron" ignore-scripts=false build_from_source="true" diff --git a/.nvmrc b/.nvmrc index 5bf4400f22922e..1dd37d53743d1a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.15.0 +24.17.0 diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index 2f275d1597581b..b9dc1d80b995e1 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -39,7 +39,7 @@ EOF if [ "$npm_config_arch" == "x64" ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/148.0.7778.97/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/148.0.7778.271/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux # Download libcxx headers and objects from upstream electron releases DEBUG=libcxx-fetcher \ @@ -51,9 +51,9 @@ if [ "$npm_config_arch" == "x64" ]; then # Set compiler toolchain # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.97:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.97:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.97:build/config/c++/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.271:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.271:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.271:build/config/c++/BUILD.gn export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu" export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu" export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -DSPDLOG_USE_STD_FORMAT -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 7db96c8acfc3ba..37aeada5f98f60 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -6c25b7496a0f3f4e325965ac3d934f9460e6b39fc2aa05b2aefecb1e212e7535 *chromedriver-v42.2.0-darwin-arm64.zip -7ea0a5378a615b816c6652c1e8f62b25fee751f34c669eb3eff122dcde98dffc *chromedriver-v42.2.0-darwin-x64.zip -8f6c2462e05491ab7a6ea6492fe7f62c8de1b01900978dd3dcbc878fde6f5e3e *chromedriver-v42.2.0-linux-arm64.zip -72cdb458b48306ec4939021198696926651a6a6dd47b8acf4486d77689ffe88f *chromedriver-v42.2.0-linux-armv7l.zip -ebe0fb1e5eb8a83a20612d660191d8292826d46d93701fe6390a9a5ab69aede0 *chromedriver-v42.2.0-linux-x64.zip -f19d5491a6c5d2970b05a42e1e2a5bcab75206423cea9dfe8ef5c2b4a0ceb38b *chromedriver-v42.2.0-mas-arm64.zip -30c88509d9640b4bf0066da7b6df7c4bb61d894df20941daaeeead598da40c28 *chromedriver-v42.2.0-mas-x64.zip -a6a21d19b84938d32ea75ad19013370c8937536c049d4d5c44175fbcf3703c5b *chromedriver-v42.2.0-win32-arm64.zip -da076702e72317843754e72cc1895c4d223e423f177061342ae5aec78a7281a9 *chromedriver-v42.2.0-win32-ia32.zip -97074022e6b5048b0bd9932c011e8377148c04b0e5455ea2e24bfc048d888297 *chromedriver-v42.2.0-win32-x64.zip -4012c6a738d83799544cabd04e41c8eba2467e4de573664326eaea174f2f8b47 *electron-api.json -bffe18be718c641a0fdfa8bf2ab82dbce462b8809be97898a9c4dfd136be071d *electron-v42.2.0-darwin-arm64-dsym-snapshot.zip -5a341581905b84ae62b4f7664c0fd950ec78fef172042dd8b55dc2a8d9aea66a *electron-v42.2.0-darwin-arm64-dsym.tar.xz -110b25eadfd680ad44ba5b43bf59dcf31297976ee128735e3ef9bbc2a35758cf *electron-v42.2.0-darwin-arm64-symbols.zip -f45f80da0a2d005530b70f6f6b00756dbf875947a21e533041a05b3c4d629f79 *electron-v42.2.0-darwin-arm64.zip -aefc2008c08cd3795cf997a5c5d88772497f460c5cc6f5008b01c412d7534417 *electron-v42.2.0-darwin-x64-dsym-snapshot.zip -a6b70b51de1f05541e42138d02768ef72940c125379b0d7a929fc3e5f2e1eb75 *electron-v42.2.0-darwin-x64-dsym.tar.xz -2d64a5bf8af6b9dccecde9ac09f20cbae36a76be537c4b3e5c3a238add5a4a01 *electron-v42.2.0-darwin-x64-symbols.zip -cd6a2d4feca84b7e7b2c4a5a13a443fc3c1173e77b05f798deaab2f0f41002a1 *electron-v42.2.0-darwin-x64.zip -2c0e81bc0e82c9e1966d4042b565dbf4924bd6365c08e6cc715001b3842ff9cd *electron-v42.2.0-linux-arm64-debug.zip -30983431149fc5a813c2145843744292e2553577f19d75aa09cbf8f7100d9319 *electron-v42.2.0-linux-arm64-symbols.zip -1f2037dbdcb8b1327b855ec15fbe3fb8a7f27786b331d17866e88377a0606ad8 *electron-v42.2.0-linux-arm64.zip -948553edfe3fa5272327d867b68aceb6e272cdeb68df4a40a6ffa045976531d2 *electron-v42.2.0-linux-armv7l-debug.zip -fed24d4f0c4fa40c3dfe38fdea423d6f36dc09630a614fb9060373fe30f310c4 *electron-v42.2.0-linux-armv7l-symbols.zip -00de1cc51859a4e064a2178cb29c899feadfabf24d926d8f684c30e6ab9b72a7 *electron-v42.2.0-linux-armv7l.zip -e0a0c2f97e97321ca194bf2ed7275ad6b31c0c0f4f7e4989838c4fd74728f306 *electron-v42.2.0-linux-x64-debug.zip -cf65567a4c5ef25c24cbdc8a6059a419773756f79313a1e682272c42f5716f2a *electron-v42.2.0-linux-x64-symbols.zip -9caeeb15dada37cb3a2d80bf0f5899d175db026a4def11560890bd2f19684909 *electron-v42.2.0-linux-x64.zip -69ad3951065eb33bb6533f872267828e4ea728ca7d539c649cebc966c0ea1bab *electron-v42.2.0-mas-arm64-dsym-snapshot.zip -38c4beeb71171ab97f55e9a213aa44a45e20e62cc73207532f29ff70a2698539 *electron-v42.2.0-mas-arm64-dsym.tar.xz -359be04c4da1b1979f0e1275b3abd5bd2cbab43c2290d71bfdeb174c1f42beba *electron-v42.2.0-mas-arm64-symbols.zip -535aca88dbe2977d22dc63e47887d09c7e41e8ccf4c0f23d67b7ce593e2341dd *electron-v42.2.0-mas-arm64.zip -e621151d901eeab917e225c97e102fa33ba5225c5cd5b8c152948d8e4a60d104 *electron-v42.2.0-mas-x64-dsym-snapshot.zip -0d33ddacaa69b923fe0117f6ed7d92c8918de6e98daa5f0e9462acf1ea758c5e *electron-v42.2.0-mas-x64-dsym.tar.xz -1527430d26a58505c0e3e52f27afc5aa5feffa5dd17d83d1b89ebcb16dfb4c0c *electron-v42.2.0-mas-x64-symbols.zip -576abe887a65587910ffc481fcaac39bb42e2791c54ae3a2c000431a44299cd2 *electron-v42.2.0-mas-x64.zip -d4e06302625806acf6a39b15a22835de6d4c38ce0cedfe1716d4c0df8416fc2b *electron-v42.2.0-win32-arm64-pdb.zip -09792a89e357258afae9d3abe51f42592062bafb1b98e80d77daa1cc554e26f2 *electron-v42.2.0-win32-arm64-symbols.zip -615b0145f304e0eea0ffd20c0fcf0e8e7e5255e1c3f3ae0577725a16ebb91994 *electron-v42.2.0-win32-arm64-toolchain-profile.zip -1e6b5639cbbb0134f41e5cb62a283ebc34bc580a8fb21349d711f275d60f9705 *electron-v42.2.0-win32-arm64.zip -5b5803e32a2ddfb51bbf4a0097a6fb48f964b2844150cec9c6fb48d3eef16865 *electron-v42.2.0-win32-ia32-pdb.zip -218ce2ebacc35de9bc9ac83a1b65551fd53c9622b71516a484bcfed0aec2c4a6 *electron-v42.2.0-win32-ia32-symbols.zip -615b0145f304e0eea0ffd20c0fcf0e8e7e5255e1c3f3ae0577725a16ebb91994 *electron-v42.2.0-win32-ia32-toolchain-profile.zip -1a866e0634ff95f83a043c7c6738e148f38362ed0afa60aa5eb8dcab16def7b5 *electron-v42.2.0-win32-ia32.zip -82329a1c558392b1d2a80cfc24d1463cf88fd0bb7b542eee24305ae099935bf6 *electron-v42.2.0-win32-x64-pdb.zip -00b8c0437bb75b0db1dfa2bf23e6a6d2436c0665e2a627bd2732ccab0fd1e648 *electron-v42.2.0-win32-x64-symbols.zip -615b0145f304e0eea0ffd20c0fcf0e8e7e5255e1c3f3ae0577725a16ebb91994 *electron-v42.2.0-win32-x64-toolchain-profile.zip -6e034b748ad5ed9445bd3da4b7d0792ed49556774a541217b507c156b00dd69a *electron-v42.2.0-win32-x64.zip -acb2fdd6e99a2056ff88fe26d94b141922e30c5a6a2b11868aa5e708ba30266d *electron.d.ts -3099226c4eb0c13134bbdd970c0fac8a372547b401c54552a89fa1689470dbd7 *ffmpeg-v42.2.0-darwin-arm64.zip -1f4fac2e8f4f8b136bc29ce6be50598f217300e10c1a203b6b893f623ba516ba *ffmpeg-v42.2.0-darwin-x64.zip -1fd892c2195d89a7d4f926167c122c34ab7ce15a61b75c8e5ba0cbfc47ebdb4f *ffmpeg-v42.2.0-linux-arm64.zip -6b6a200705ded7211b02d7ee56396d99c2003aa6b0b83bdd8c89f95702ea459d *ffmpeg-v42.2.0-linux-armv7l.zip -ffb78ccf0b2cbf1a1c0da2a69d8021337ae5352642a2d84adae951f95c771696 *ffmpeg-v42.2.0-linux-x64.zip -3099226c4eb0c13134bbdd970c0fac8a372547b401c54552a89fa1689470dbd7 *ffmpeg-v42.2.0-mas-arm64.zip -1f4fac2e8f4f8b136bc29ce6be50598f217300e10c1a203b6b893f623ba516ba *ffmpeg-v42.2.0-mas-x64.zip -87ad1b3868751b2af34cfebd62f9874ced837fc4389adad3e7659a437afcffb1 *ffmpeg-v42.2.0-win32-arm64.zip -57af529a0cc217bb5f20c67554eb526926b853a1f1594a06e8423662cead9c09 *ffmpeg-v42.2.0-win32-ia32.zip -62dc51e9290b444c6640048a715d8d7b60fe3e546a98f0faff945e4a522250f8 *ffmpeg-v42.2.0-win32-x64.zip -72ea4fdeabf3a49dd86bdeb136c916f7b54952b439edd527321963a5dbb0460d *hunspell_dictionaries.zip -98e8f208b8fd697e2a035d03e83de5731ba92b23f13f826abc60c0b7eab0d952 *libcxx-objects-v42.2.0-linux-arm64.zip -7187c3f0406a04b8e19af6ca374e6d7ca7833e47ea2382f7fba1ed7061f7d8be *libcxx-objects-v42.2.0-linux-armv7l.zip -7254b02f7220aa47210c382f45668016710d07cd7c7c61da5c4c556cc2334446 *libcxx-objects-v42.2.0-linux-x64.zip -37ade3096c7362b9df8945311f55abb8d039f93aabacec086e449cee4a31dcab *libcxx_headers.zip -c623bfe37d755eb9020fc080d4ef0df799acf3439b7b588f9a62d9b6e3908b75 *libcxxabi_headers.zip -44b16dd0e2bade1253e6f36235a43f005ed6472c1017195ee1ea1e629b8ab504 *mksnapshot-v42.2.0-darwin-arm64.zip -9d59364581e6001d655cd6c659e76ab80799d794e22b0c990b181268faa0492c *mksnapshot-v42.2.0-darwin-x64.zip -6035f7f148470352c04f0591d360685724a7784780f6d74c4fdaa99c71b8be6f *mksnapshot-v42.2.0-linux-arm64-x64.zip -27d4c39086c376744bd606f55c7b6c1420d0267f94cd8974031ce4b1f88320e6 *mksnapshot-v42.2.0-linux-armv7l-x64.zip -ca549469050d6a360e6b95ea202cb61ea704db51f7d67aa67c457e5dfa98b459 *mksnapshot-v42.2.0-linux-x64.zip -8d2271bd6cb9a16d34e02dfc92efb1fd1a2908038165efc9aa161cf5e62dc7ae *mksnapshot-v42.2.0-mas-arm64.zip -ae8453efb615242dd27de97cf1f9c6b8f4c6ad9cfab5fc476671d8751c5ac319 *mksnapshot-v42.2.0-mas-x64.zip -f21cdde8044078ce14acd0c65af1e7cf999d78e6d40d467cc73f854c9e9ef481 *mksnapshot-v42.2.0-win32-arm64-x64.zip -4b5b7d68067f55a0fc4dd4c295ca115070f288684e7942e0e80badc2b64754a6 *mksnapshot-v42.2.0-win32-ia32.zip -25d850c5674e0a821bb5897197b6e38c0d55a5cf0505d4d79a39b608b54c4b5c *mksnapshot-v42.2.0-win32-x64.zip +f46a52e829e4e8d294887346e2cd65a43cda3cf0c4988f9e8c40bff083b21ca0 *chromedriver-v42.5.0-darwin-arm64.zip +95ea8ac2949531f92a69e935345fb5f2c49dabb4d03244e90431b54e4177d66c *chromedriver-v42.5.0-darwin-x64.zip +217eff942b1fb63767a2fccdf309b8af67bc85dcbe5b6a626159706f8b0f6549 *chromedriver-v42.5.0-linux-arm64.zip +af76c74bebd454a43e6ed463eba64b2d635661203c31bca8efbe829169f1df2a *chromedriver-v42.5.0-linux-armv7l.zip +ada1322977b76a42c97baa12a9388fb3d33e6043a9d4380ded3ccd003c9f3481 *chromedriver-v42.5.0-linux-x64.zip +9a1fc333f04178abc4985596ff5e6843e857d63bc05be8ae9b8d39adc184890c *chromedriver-v42.5.0-mas-arm64.zip +7c1357049a4f4d87e86f80f002b948dbd77f2987002c05d09f67495ae38d6235 *chromedriver-v42.5.0-mas-x64.zip +619bb88746f6708d9f291847ec8f9a360b9dd799cdda96c2125e6e64f6ba5dc6 *chromedriver-v42.5.0-win32-arm64.zip +d3cf1b135f8fd9c27d5736f64b4afd1044b851ba3122c7a0985188aba94f2df0 *chromedriver-v42.5.0-win32-ia32.zip +a22ffa5f552adf98609ce15511133f8d3d2a0dee3f7be21cbf252bac98364ef6 *chromedriver-v42.5.0-win32-x64.zip +9600be1460e73162fa0bc04b0d27f6af9f87af9c9e8c1979feeb81777a984fce *electron-api.json +d32ee862313327486bfe33c2d91d404d4ee20146773c8a5f87bd418f8a8e9a91 *electron-v42.5.0-darwin-arm64-dsym-snapshot.zip +606a47375b0e48343d3a96d3bada5add9e26768036edecbdb6e6f1aea754e55e *electron-v42.5.0-darwin-arm64-dsym.tar.xz +155e135264dfe1b4109140acc23e321e00495516089937ee0909ff3e70a9447f *electron-v42.5.0-darwin-arm64-symbols.zip +5e392f66b3f6d13caa6d50bbe10fce3d848ca16c5680529357aa26c5da58a1e6 *electron-v42.5.0-darwin-arm64.zip +7a4691f6648a10dfb3315a45e1ce4c297a606d1c475b65fbb5c9d80688cbb0bb *electron-v42.5.0-darwin-x64-dsym-snapshot.zip +fc620d7787958dbe74dc9acaf42fe35fcdbba7f0c727b376e979b1b86e0d99c9 *electron-v42.5.0-darwin-x64-dsym.tar.xz +9fca997d9f6187b398730b69819e3643dc182b869e1ad0b80a7b5e1cc55a9ad1 *electron-v42.5.0-darwin-x64-symbols.zip +ca4792d47e74a6afce1d39a019f201d1a47fa81fcf4e8bcaac6fe5756f172940 *electron-v42.5.0-darwin-x64.zip +f144b1025a7b8c5bf34cfbc6f17f47e594db186de4b5647286d40618c8efd5c4 *electron-v42.5.0-linux-arm64-debug.zip +54878afd06169b12e1e2908319883d2e3ecd4a3fc39187152a4110f13a20db46 *electron-v42.5.0-linux-arm64-symbols.zip +0188f1d86efe785e5721cfaa1ae51d6ef68260705c30037d0d271aaaf78af315 *electron-v42.5.0-linux-arm64.zip +d58cf9b92c9e74885d6fd0ea0740602fa368d87967ed35b8286aaf4170e053e1 *electron-v42.5.0-linux-armv7l-debug.zip +45cfe194be6c3af8c8f4f04e83d26cea2f0d01d822d6d0d39cf24ef11298b6f3 *electron-v42.5.0-linux-armv7l-symbols.zip +360ff6d128be77be83fb3eaf458380fdca226be06f247551049fe3b6ef8ac382 *electron-v42.5.0-linux-armv7l.zip +a0fad46643b7bf6bfce7b2d0a3bd26166079e81e0581d9f3420484984e135c4b *electron-v42.5.0-linux-x64-debug.zip +3c1a4dc6c79bad3d9bc7d3af17b22bc3c97543c27a49b18d852b9eeb3ced2c0a *electron-v42.5.0-linux-x64-symbols.zip +6705a9d0cc5c8f225d705d6e1c2607b2b5be8667d2befb18cdafe8b7b29b8008 *electron-v42.5.0-linux-x64.zip +11b3c08d04f8e89983bff6ee1fc872f8d94f0a92c07311c81da6ac493cbedcc9 *electron-v42.5.0-mas-arm64-dsym-snapshot.zip +1ed2d9702069c699e143366d86e24398349072209459d23883c8bda74f62a161 *electron-v42.5.0-mas-arm64-dsym.tar.xz +7a474a3498973aeb9344c193a383e3590fde4ce083b0f0d51a0eced4f6d31ac6 *electron-v42.5.0-mas-arm64-symbols.zip +33166a8b868cd6624cb0d70847b42067219a2f532050d1cdd128d8d7eb9f2b74 *electron-v42.5.0-mas-arm64.zip +b6eaf6f40e20f196974f675c936a2d25c97a3f660faecba8978fd59b838a7b40 *electron-v42.5.0-mas-x64-dsym-snapshot.zip +30ca0601ba38e804152d16805eb01b05a60452359ce3d6b9cebb1e21d024f4dc *electron-v42.5.0-mas-x64-dsym.tar.xz +5da55d91f5b4159f4bc6fa4831b995b621dcab6ef60d20d64aaf074dcba3943c *electron-v42.5.0-mas-x64-symbols.zip +b2c23925f3d6561dadc8f563576b7a222ea20606a8002986e5937bd066710870 *electron-v42.5.0-mas-x64.zip +d7cea2c82487a4af809c181c99cec529d0a00c86fcfc70684d9a2c5c0d295212 *electron-v42.5.0-win32-arm64-pdb.zip +2799cffe27425316d27336fc03d8c77581aa8e65e27964104da68d7780b1a040 *electron-v42.5.0-win32-arm64-symbols.zip +8e245a0179165d88b82f382dda3f68d0851a2f4b21b14b833b9b7c4374d7da3a *electron-v42.5.0-win32-arm64-toolchain-profile.zip +765370134fdd2dd851bb019d7b7d9379ed7087f288eb8bee4043b42cfd9c33d8 *electron-v42.5.0-win32-arm64.zip +d4b34eb9e4e1ac77f11a0be51d39e94548362d1df63f6b7675016e6cb7b9b904 *electron-v42.5.0-win32-ia32-pdb.zip +eaa1fa1bc6bb14f8877e8ec930b90d9cdbc9dc41765cf530f2c5e6aca1da1fe8 *electron-v42.5.0-win32-ia32-symbols.zip +8e245a0179165d88b82f382dda3f68d0851a2f4b21b14b833b9b7c4374d7da3a *electron-v42.5.0-win32-ia32-toolchain-profile.zip +aa040c36074046a723fca9173aac1c8bf70c19d8a156215e8dbeb17ea37f588b *electron-v42.5.0-win32-ia32.zip +615726602fd5d697c12fc9b66de28343919631b24ab1179d83da46508e8a87d4 *electron-v42.5.0-win32-x64-pdb.zip +a913f632a979b7b7fb964b4d43ad45976638a5673cec1b6a69fd6572f57adc0d *electron-v42.5.0-win32-x64-symbols.zip +8e245a0179165d88b82f382dda3f68d0851a2f4b21b14b833b9b7c4374d7da3a *electron-v42.5.0-win32-x64-toolchain-profile.zip +127bbf7a755b438612c076b22baee258a87cd3d07168cc82ea46ffc015936114 *electron-v42.5.0-win32-x64.zip +21ead4c9c384849887ca8cac0a2a7a683d0cf90a3b62a2a2ba8519224e16e249 *electron.d.ts +45d8022f3bd67995d90242c5176c832fdf3abbdaac575b2469ca0537d0eb58db *ffmpeg-v42.5.0-darwin-arm64.zip +3a5e8f19fd4aa38b3d31eeb7da40f0434e87703050912fc0ca9dbca2de06f0e2 *ffmpeg-v42.5.0-darwin-x64.zip +9efdc919ffd357f00f080a8b1e1baeea19dcfc3164ea02d5cd12b93f85e67b73 *ffmpeg-v42.5.0-linux-arm64.zip +2e24581796500f7d6ed0c16076d74b7fe0b7cf625844623535077df03886bc26 *ffmpeg-v42.5.0-linux-armv7l.zip +78e8d6fd7432e6cbf4ef024728b46aa0b7fa292a598fbbadcc42474f87380d42 *ffmpeg-v42.5.0-linux-x64.zip +28e44748d48b6168aefde8601225c5f3dfb9404d616de2e6035c634354e0a6ac *ffmpeg-v42.5.0-mas-arm64.zip +0bdb9e4050c4159935e8640d0facd6e868583d921386e9c2a5bf3c550c5baf3c *ffmpeg-v42.5.0-mas-x64.zip +81a1c0273c172f140fbf261c194d7ae7369a1e7abe27cae413c8590e2bca4cad *ffmpeg-v42.5.0-win32-arm64.zip +87cffd235b85029c21a1e962132b608209e9929d2a22611985fda317cfe7e95f *ffmpeg-v42.5.0-win32-ia32.zip +497790a714fffc090d217b66332dfe6896609aa24293dc3fb4effce57fe29c82 *ffmpeg-v42.5.0-win32-x64.zip +6db69261ec30c95e30cc3bcd526e8fcec59cd1ad785a7985e520f77bc974a03c *hunspell_dictionaries.zip +441e5c5d7ac9407438d1f2a9974bbc0ef1c3a28e9408051f577e790cd6c6d171 *libcxx-objects-v42.5.0-linux-arm64.zip +99a3a1f895debddf023f01631c2c2b81911992bbbc514718d2ad24599040e4da *libcxx-objects-v42.5.0-linux-armv7l.zip +c7e4c5c5ac5505f97a555cc21bd09014a2e740e5d25e999f2bdc7542a86256e0 *libcxx-objects-v42.5.0-linux-x64.zip +92cdadcc04957db6e7b65421790f77f56bc0c8e6dc1407ad1adbfbdf83dbbc06 *libcxx_headers.zip +a169c9558951055efc054d31c3b4c8a04aa588ef1533b4456b6b27950d121994 *libcxxabi_headers.zip +9611bdee31d4464a8b06e9d44e99adf290a898969a502cfc9ac5616ae1026a0c *mksnapshot-v42.5.0-darwin-arm64.zip +89f7e234802958438a115c96b2f6207153f776a13b07247947fd2e5ca9b1ce3d *mksnapshot-v42.5.0-darwin-x64.zip +2a6d92eb9eb89419dc3b2ab1287329acc16955a62833d32bda2bd90ded45a5b0 *mksnapshot-v42.5.0-linux-arm64-x64.zip +391263da6782b2536067113ce7dab8ef19d9b3d59a74430fef59a4d8a7364502 *mksnapshot-v42.5.0-linux-armv7l-x64.zip +f7752a92ad45041e0c00a74bdc10a0d13574849ad944ba2b3424dc255460465f *mksnapshot-v42.5.0-linux-x64.zip +b9425403b15e273be565d7a937c7d28fc09f421df9fa4d376d6932bfecc91ff5 *mksnapshot-v42.5.0-mas-arm64.zip +a7b7230be427b849f31467298c2c4a934c7273e731ea17513c5790c6d66c8205 *mksnapshot-v42.5.0-mas-x64.zip +4a32b6558fb9aaae703cdc2e6a87ce05e95184e6d0e49e350a956c0908724ef7 *mksnapshot-v42.5.0-win32-arm64-x64.zip +ca73fb8ef0d6e4cf92f569a0dee9770f13166e67be68b12688a7ecaa630a2163 *mksnapshot-v42.5.0-win32-ia32.zip +d8a484f4f3347d7fd73bc4af59ce743e90e14fe7743a1be5ea09452e9c3bbd5b *mksnapshot-v42.5.0-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index 16e83546f49a19..a787f4a19a9f94 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,6 +1,6 @@ -372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4 node-v24.15.0-darwin-arm64.tar.gz -ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b node-v24.15.0-darwin-x64.tar.gz -73afc234d558c24919875f51c2d1ea002a2ada4ea6f83601a383869fefa64eed node-v24.15.0-linux-arm64.tar.gz -44836872d9aec49f1e6b52a9a922872db9a2b02d235a616a5681b6a85fec8d89 node-v24.15.0-linux-x64.tar.gz -49a54c103f4919ce64199a043ef5cd309507de491d718085edee089cd8e87543 win-arm64/node.exe -3331e1ffe19874215472217c5e94f5a0c6d8e18c4ac7111d3937aa0ad5e9b4a5 win-x64/node.exe +4fc3266a3702eebc39cc37661cf4eeceeade307e242ab64e4d7ce7949197e11f node-v24.17.0-darwin-arm64.tar.gz +80da552fe037290cb130e9dea590f5eeeb7aa450636f0c89ab41415511c1ec27 node-v24.17.0-darwin-x64.tar.gz +faa0d59ba7fe7045c950ed09b190578fb8eee73e4358686d38fcc99ca58c1480 node-v24.17.0-linux-arm64.tar.gz +e0472427aa791ad80bdc426ff7cc73cdd28ed0f616d1ff9689a23a7f47f1265f node-v24.17.0-linux-x64.tar.gz +44999f9ec6486d01202d8961f343eac8c9f2847b234a8637c3fd0f1e2bb3288a win-arm64/node.exe +c6335d08331c23d68b9f2b18adb102002d76ef150b47248e954c507e0d033664 win-x64/node.exe diff --git a/build/lib/electron.ts b/build/lib/electron.ts index e229b875f552c7..f9236eaf47235e 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -102,8 +102,7 @@ function darwinBundleDocumentTypes(types: { [name: string]: string | string[] }, }); } -const { msBuildId } = util.getElectronVersion(); -export const electronVersion = '42.2.0'; +const { electronVersion, msBuildId } = util.getElectronVersion(); // In product builds, `@vscode/gulp-electron` is given an asset resolver (via the // `repo` option) that fetches the prebuilt Electron archives on demand from the diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts index df9ef7738c6e84..0aa037e6969065 100644 --- a/build/lib/preLaunch.ts +++ b/build/lib/preLaunch.ts @@ -48,7 +48,8 @@ async function getElectron() { async function isExpectedElectronInstalled(): Promise { try { - const { electronVersion } = await import('./electron.ts'); + const { getElectronVersion } = await import('./util.ts'); + const { electronVersion } = getElectronVersion(); const installedVersion = (await fs.readFile(path.join(rootDir, '.build', 'electron', 'version'), 'utf8')).trim().replace(/^v/, ''); return installedVersion === electronVersion; } catch { diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 96e394f86bb207..d7bd81ccd104d9 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -40,6 +40,7 @@ export const referenceGeneratedDepsByArch = { 'libdbus-1-3 (>= 1.9.14)', 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', + 'libglib2.0-0 (>= 2.12.0)', 'libglib2.0-0 (>= 2.39.4)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index eb1d73d011ac9a..2fcb15e0271928 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -22,7 +22,7 @@ import product from '../../product.json' with { type: 'json' }; // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.97:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/148.0.7778.271:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/cgmanifest.json b/cgmanifest.json index b90a69f0349a5e..afc549e769e0b6 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "6b3fa66a923a9442c8ab0bc71b4b41ff24528d3b" + "commitHash": "21c21077f4937668031f80089a90618b7e1fe779" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "148.0.7778.97" + "version": "148.0.7778.271" }, { "component": { @@ -516,12 +516,12 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "848430679556aed0bd073f2bc263331ad84fa119", - "tag": "24.15.0" + "commitHash": "413e874773eef1e27a8397ddc949dbbe19cadb31", + "tag": "24.17.0" } }, "isOnlyProductionDependency": true, - "version": "24.15.0" + "version": "24.17.0" }, { "component": { @@ -529,13 +529,13 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "87740a867bddf434afec16e1f8b4f02235d3e7f7", - "tag": "42.2.0" + "commitHash": "d7bccf5b2f27969b4e7f34cf877f55cf0813be7a", + "tag": "42.5.0" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "42.2.0" + "version": "42.5.0" }, { "component": { diff --git a/extensions/copilot/.nvmrc b/extensions/copilot/.nvmrc index 5bf4400f22922e..1dd37d53743d1a 100644 --- a/extensions/copilot/.nvmrc +++ b/extensions/copilot/.nvmrc @@ -1 +1 @@ -24.15.0 +24.17.0 diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index 135afbf30146b5..2da3e52e7073e4 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -97,12 +97,12 @@ "@vscode/lsif-language-service": "^0.1.0-pre.4", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", - "@vscode/test-web": "^0.0.80", + "@vscode/test-web": "^0.0.81", "@vscode/vsce": "3.6.0", "copyfiles": "^2.4.1", "csv-parse": "^6.0.0", "dotenv": "^17.2.0", - "electron": "^39.8.5", + "electron": "^42.5.0", "esbuild": "0.28.1", "fastq": "^1.19.1", "glob": "^11.1.0", @@ -119,7 +119,7 @@ "openai": "^6.7.0", "outdent": "^0.8.0", "picomatch": "^4.0.4", - "playwright": "^1.58.2", + "playwright": "^1.61.1", "prettier": "^3.6.2", "react": "^17.0.2", "react-dom": "17.0.2", @@ -751,34 +751,35 @@ "node": ">=10" } }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz", + "integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", "progress": "^2.0.3", - "semver": "^6.2.0", + "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "engines": { - "node": ">=12" + "node": ">=22.12.0" }, "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "undici": "^7.24.4" } }, "node_modules/@emnapi/core": { @@ -3683,16 +3684,16 @@ } }, "node_modules/@koa/router": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-15.4.0.tgz", - "integrity": "sha512-vKYlXtoCfcAN8z4dHiveYX55rTYOgHEYJNumK1WM9ZAwaArhreGVkyC1LTMGfUQUJyIO/SbwRFBOHeOCY8/MaQ==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-15.6.0.tgz", + "integrity": "sha512-iEOXlvGIBqSNkGXrg0XtMARAOm5zA24oedXxiTGEkrD4JgwVjfRDddCQvW1s4WEcwDYvyecRbf8BikXsuEEj8w==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", "http-errors": "^2.0.1", "koa-compose": "^4.1.0", - "path-to-regexp": "^8.3.0" + "path-to-regexp": "^8.4.2" }, "engines": { "node": ">= 20" @@ -5098,14 +5099,14 @@ } }, "node_modules/@playwright/browser-chromium": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.59.1.tgz", - "integrity": "sha512-XDwr0qOrzLXAuBAzg4WO/xctVMb+ldJ54yz9KCpFu8G8MVJzUVFO6BvK1tBtBl4DIoFcoFRKHgUGZT+8wOC8BQ==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.61.1.tgz", + "integrity": "sha512-t3/zE0i9gik5R/NpRs7G2Xo/6NPeABW6ReplGdtkeWeAkaV764CgFgoKjJo21D2xgjnvDvRYubqBUu4xl0VCqA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.59.1" + "playwright-core": "1.61.1" }, "engines": { "node": ">=18" @@ -5616,18 +5617,6 @@ "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -5731,18 +5720,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@textlint/ast-node-types": { "version": "14.8.4", "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-14.8.4.tgz", @@ -5879,18 +5856,6 @@ "@types/node": "*" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -5985,12 +5950,6 @@ "@types/unist": "*" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", - "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==", - "dev": true - }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -6018,15 +5977,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -6128,15 +6078,6 @@ "@types/react": "*" } }, - "node_modules/@types/responselike": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz", - "integrity": "sha512-TiGnitEDxj2X0j+98Eqk5lv/Cij8oHd32bU4D/Yw6AOq7vvTk0gSD2GPj0G/HkvhMoVsdlhYF4yqqlyPBTM6Sg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/sarif": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", @@ -6270,16 +6211,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.1.tgz", - "integrity": "sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.36.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", @@ -7100,26 +7031,26 @@ } }, "node_modules/@vscode/test-web": { - "version": "0.0.80", - "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.80.tgz", - "integrity": "sha512-QgsRPp8IuPcdbUdVtqQkFN/sGd+6cr6g0ENEVFOHwJbQqZtJSiZD5w0e6tgmoeq9nBjNqZCq87OO1GkwFHkPyw==", + "version": "0.0.81", + "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.81.tgz", + "integrity": "sha512-qAYNX1mf4hE0L3T/186J8AH+Z7Inm81OHMACkkyKE2J6HJZlXou0OgABkSvd8gt0BiPjI+V+xkduAaQ8Kcjexg==", "dev": true, "license": "MIT", "dependencies": { "@koa/cors": "^5.0.0", - "@koa/router": "^15.3.0", - "@playwright/browser-chromium": "^1.58.2", + "@koa/router": "^15.6.0", + "@playwright/browser-chromium": "^1.61.0", "gunzip-maybe": "^1.4.2", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "koa": "^3.1.2", + "http-proxy-agent": "^9.1.0", + "https-proxy-agent": "^9.1.0", + "koa": "^3.2.1", "koa-morgan": "^1.0.1", "koa-mount": "^4.2.0", "koa-static": "^5.0.0", "minimist": "^1.2.8", - "playwright": "^1.58.2", - "tar-fs": "^3.1.1", - "tinyglobby": "^0.2.15", + "playwright": "^1.61.0", + "tar-fs": "^3.1.2", + "tinyglobby": "^0.2.17", "vscode-uri": "^3.1.0" }, "bin": { @@ -7129,10 +7060,50 @@ "node": ">=20" } }, + "node_modules/@vscode/test-web/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vscode/test-web/node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vscode/test-web/node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@vscode/test-web/node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7145,13 +7116,14 @@ } }, "node_modules/@vscode/test-web/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } @@ -7727,11 +7699,19 @@ } }, "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } }, "node_modules/balanced-match": { "version": "1.0.2", @@ -7740,20 +7720,26 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", - "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "dev": true, "license": "Apache-2.0", - "optional": true + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } }, "node_modules/bare-fs": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", - "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", + "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", @@ -7774,42 +7760,45 @@ } }, "node_modules/bare-os": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", - "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.2.tgz", + "integrity": "sha512-h530JsrkYi8518ZfR57GHaLoI5YzXkGGEV0Y+mf4KYPBn4OnNajiznwkDq7FgE+Vnmyss9Utnzi44y7sowiAXA==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "bare": ">=1.14.0" } }, "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", + "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { - "streamx": "^2.21.0" + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" }, "peerDependencies": { + "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, "bare-buffer": { "optional": true }, @@ -7819,12 +7808,11 @@ } }, "node_modules/bare-url": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", - "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-path": "^3.0.0" } @@ -7978,13 +7966,6 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "dev": true, - "optional": true - }, "node_modules/boundary": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", @@ -8180,43 +8161,6 @@ "node": ">=8" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -8458,18 +8402,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cls-hooked": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", @@ -8927,6 +8859,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "optional": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -8942,6 +8875,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, + "optional": true, "engines": { "node": ">=10" }, @@ -8994,15 +8928,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -9096,13 +9021,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "optional": true - }, "node_modules/diagnostic-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", @@ -9273,24 +9191,41 @@ "license": "MIT" }, "node_modules/electron": { - "version": "39.8.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.8.5.tgz", - "integrity": "sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA==", + "version": "42.5.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-42.5.0.tgz", + "integrity": "sha512-cYEKS9XFz+c9fAB4jI0x49yz1FFzB55r3q96wu9YkwwJMv7t9202IE/ltlgy6yitl/J4M7C8JQcmUqdzDvPl/w==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" }, "bin": { - "electron": "cli.js" + "electron": "cli.js", + "install-electron": "install.js" }, "engines": { - "node": ">= 12.20.55" + "node": ">= 22.12.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" } }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/embla-carousel": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", @@ -9376,12 +9311,16 @@ } }, "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/error-ex": { @@ -9533,13 +9472,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "optional": true - }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -9694,6 +9626,16 @@ "node": ">= 0.6" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -9864,26 +9806,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9951,15 +9873,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -10112,20 +10025,6 @@ "dev": true, "optional": true }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -10286,21 +10185,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -10360,24 +10244,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -10454,31 +10320,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -10732,12 +10573,6 @@ "node": ">= 0.6" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -10770,19 +10605,6 @@ "node": ">= 14" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -11580,13 +11402,6 @@ "bignumber.js": "^9.0.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -11628,28 +11443,12 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "optional": true - }, "node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -11772,9 +11571,9 @@ } }, "node_modules/koa": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/koa/-/koa-3.1.2.tgz", - "integrity": "sha512-2LOQnFKu3m0VxpE+5sb5+BRTSKrXmNxGgxVRiKwD9s5KQB1zID/FRXhtzeV7RT1L2GVpdEEAfVuclFOMGl1ikA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.2.1.tgz", + "integrity": "sha512-e7IpWJrnanNUroVK2taAgMxoEZvHLXdQiNjeExSu/DEIWm83jaKGBgb7tLmu2rMYpA027qFB3iLR/k3AVpFRnA==", "dev": true, "license": "MIT", "dependencies": { @@ -11919,16 +11718,20 @@ } }, "node_modules/koa/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/leven": { @@ -12374,15 +12177,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/lru-cache": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", @@ -12447,19 +12241,6 @@ "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12599,15 +12380,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -13236,18 +13008,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -13757,15 +13517,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -14011,7 +13762,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -14052,13 +13804,13 @@ } }, "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.2" + "playwright-core": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -14071,9 +13823,9 @@ } }, "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -14098,19 +13850,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/playwright/node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -14242,6 +13981,7 @@ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -14316,6 +14056,24 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -14392,18 +14150,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -14726,12 +14472,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, "node_modules/resolve-path": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", @@ -14796,18 +14536,6 @@ "node": ">= 0.6" } }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -14855,24 +14583,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/rolldown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", @@ -15175,13 +14885,6 @@ "node": ">=10" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "optional": true - }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -15242,35 +14945,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -15646,13 +15320,6 @@ "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "optional": true - }, "node_modules/stack-chain": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", @@ -15716,17 +15383,15 @@ "license": "MIT" }, "node_modules/streamx": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", - "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", "dev": true, "license": "MIT", "dependencies": { + "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -15953,6 +15618,7 @@ "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" }, @@ -16215,6 +15881,16 @@ "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.2.33.tgz", "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -16262,9 +15938,9 @@ } }, "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -16653,9 +16329,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", - "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -16680,15 +16356,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -17468,13 +17135,16 @@ } }, "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "dev": true, + "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/yazl": { diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index bb871c74d4a73f..2901b721d81d57 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -7122,12 +7122,12 @@ "@vscode/lsif-language-service": "^0.1.0-pre.4", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", - "@vscode/test-web": "^0.0.80", + "@vscode/test-web": "^0.0.81", "@vscode/vsce": "3.6.0", "copyfiles": "^2.4.1", "csv-parse": "^6.0.0", "dotenv": "^17.2.0", - "electron": "^39.8.5", + "electron": "^42.5.0", "esbuild": "0.28.1", "fastq": "^1.19.1", "glob": "^11.1.0", @@ -7144,7 +7144,7 @@ "openai": "^6.7.0", "outdent": "^0.8.0", "picomatch": "^4.0.4", - "playwright": "^1.58.2", + "playwright": "^1.61.1", "prettier": "^3.6.2", "react": "^17.0.2", "react-dom": "17.0.2", @@ -7216,6 +7216,7 @@ }, "overrides": { "string_decoder": "npm:string_decoder@1.2.0", + "yauzl": "^3.3.1", "zod": "3.25.76" }, "vscodeCommit": "94c8e2adc50e26ef70af85a0de3a9efed757acaa", diff --git a/package-lock.json b/package-lock.json index d488a237da27af..338c7eff02e978 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,7 +80,7 @@ "@anthropic-ai/claude-agent-sdk": "0.3.187", "@openai/codex": "0.142.0", "@playwright/cli": "^0.1.9", - "@playwright/test": "^1.56.1", + "@playwright/test": "^1.61.1", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/chrome-remote-interface": "^0.33.0", "@types/cookie": "^0.3.3", @@ -123,7 +123,7 @@ "cookie": "^0.7.2", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "42.2.0", + "electron": "42.5.0", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -923,6 +923,16 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.3.tgz", + "integrity": "sha512-OjKpjB7gohtEjZiq6nDx1egqjZJhGPN1iFOIED+NFhB/MMkXw/XRcHjh1DGXKT5z2W9eW7Jy2UKU3gpjvusFTQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@electron/get": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", @@ -2752,13 +2762,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", - "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.1" + "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -7111,15 +7121,15 @@ "dev": true }, "node_modules/electron": { - "version": "42.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-42.2.0.tgz", - "integrity": "sha512-b2Tc7sIKiZEl0tBVwFM5GJ+FT5KYhmy9QJHjx8BGVZPVW2SctXWEvrE959ElB56qw7H05dBkhlikDA1DmpaAMw==", + "version": "42.5.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-42.5.0.tgz", + "integrity": "sha512-cYEKS9XFz+c9fAB4jI0x49yz1FFzB55r3q96wu9YkwwJMv7t9202IE/ltlgy6yitl/J4M7C8JQcmUqdzDvPl/w==", "dev": true, "license": "MIT", "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", - "@types/node": "^24.9.0", - "extract-zip": "^2.0.1" + "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", @@ -8261,26 +8271,6 @@ "node": ">=0.10.0" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -9100,31 +9090,6 @@ "node": ">=8" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -15292,13 +15257,13 @@ } }, "node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.1" + "playwright-core": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -15323,9 +15288,9 @@ } }, "node_modules/playwright/node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index f0c22189c1f34e..91f0bee534b7e5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.128.0", - "distro": "7abf39b86c07d094722a4b3ec9f37e78fe3d5db3", + "distro": "9edf315ef11f6002bb6c8784bf27c81bdea7c8c1", "author": { "name": "Microsoft Corporation" }, @@ -164,7 +164,7 @@ "@anthropic-ai/claude-agent-sdk": "0.3.187", "@openai/codex": "0.142.0", "@playwright/cli": "^0.1.9", - "@playwright/test": "^1.56.1", + "@playwright/test": "^1.61.1", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/chrome-remote-interface": "^0.33.0", "@types/cookie": "^0.3.3", @@ -207,7 +207,7 @@ "cookie": "^0.7.2", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "42.2.0", + "electron": "42.5.0", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", diff --git a/remote/.npmrc b/remote/.npmrc index 6f2d4e8df7b2cf..813ce0f480e192 100644 --- a/remote/.npmrc +++ b/remote/.npmrc @@ -1,6 +1,6 @@ disturl="https://nodejs.org/dist" -target="24.15.0" -ms_build_id="438265" +target="24.17.0" +ms_build_id="451432" runtime="node" build_from_source="true" legacy-peer-deps="true" diff --git a/scripts/test-remote-integration.bat b/scripts/test-remote-integration.bat index 96288d35886d80..5e2c8864c2c215 100644 --- a/scripts/test-remote-integration.bat +++ b/scripts/test-remote-integration.bat @@ -3,6 +3,10 @@ setlocal pushd %~dp0\.. +:: TODO(deepak1556): Remove this once bumped > 24.16.0, refs https://github.com/nodejs/node/issues/63638 +for /f "delims=" %%i in ('node -p "require('fs').realpathSync.native(require('os').tmpdir())"') do set "TMP=%%i" +set "TEMP=%TMP%" + IF "%~1" == "" ( set AUTHORITY=vscode-remote://test+test/ :: backward to forward slashed diff --git a/scripts/test-web-integration.bat b/scripts/test-web-integration.bat index bc33dfc2a35c03..2f6c320e5878a0 100644 --- a/scripts/test-web-integration.bat +++ b/scripts/test-web-integration.bat @@ -3,6 +3,10 @@ setlocal pushd %~dp0\.. +:: TODO(deepak1556): Remove this once bumped > 24.16.0, refs https://github.com/nodejs/node/issues/63638 +for /f "delims=" %%i in ('node -p "require('fs').realpathSync.native(require('os').tmpdir())"') do set "TMP=%%i" +set "TEMP=%TMP%" + IF "%~1" == "" ( set AUTHORITY=vscode-remote://test+test/ :: backward to forward slashed diff --git a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts b/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts index d0db4dea996a79..94e40f56b4650f 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts @@ -360,21 +360,27 @@ function shouldUseBracketedPasteMode(command: string): boolean { function parseSentinel(content: string, sentinelId: string): { found: boolean; exitCode: number; outputBeforeSentinel: string } { const marker = `${SENTINEL_PREFIX}${sentinelId}_EXIT_`; - const idx = content.indexOf(marker); - if (idx === -1) { - return { found: false, exitCode: -1, outputBeforeSentinel: content }; + let markerIndex = content.lastIndexOf(marker); + while (markerIndex !== -1) { + const outputBeforeSentinel = content.substring(0, markerIndex); + const afterMarker = content.substring(markerIndex + marker.length); + const endIdx = afterMarker.indexOf('>>>'); + if (endIdx !== -1) { + const exitCodeStr = afterMarker.substring(0, endIdx).trim(); + if (/^-?\d+$/.test(exitCodeStr)) { + return { + found: true, + exitCode: parseInt(exitCodeStr, 10), + outputBeforeSentinel, + }; + } + } + // Ignore echoed sentinel command text (for example `$?`) and continue + // scanning for the latest complete numeric sentinel marker. + markerIndex = content.lastIndexOf(marker, markerIndex - 1); } - const outputBeforeSentinel = content.substring(0, idx); - const afterMarker = content.substring(idx + marker.length); - const endIdx = afterMarker.indexOf('>>>'); - const exitCodeStr = endIdx >= 0 ? afterMarker.substring(0, endIdx) : afterMarker.trim(); - const exitCode = parseInt(exitCodeStr, 10); - return { - found: true, - exitCode: isNaN(exitCode) ? -1 : exitCode, - outputBeforeSentinel, - }; + return { found: false, exitCode: -1, outputBeforeSentinel: content }; } function prepareOutputForModel(rawOutput: string): string { diff --git a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts index 5f6c455d9de0bd..113dd7061d44e2 100644 --- a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts @@ -9,7 +9,7 @@ import { DeferredPromise } from '../../../../base/common/async.js'; import { URI } from '../../../../base/common/uri.js'; import * as platform from '../../../../base/common/platform.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, type IDisposable } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, type IDisposable } from '../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { IAgentConfigurationService } from '../../node/agentConfigurationService.js'; import { IEnvironmentService } from '../../../environment/common/environment.js'; @@ -40,11 +40,13 @@ class TestAgentHostTerminalManager implements IAgentHostTerminalManager { commandDetectionSupported = false; readonly commandFinishedListenerRegistered = new DeferredPromise(); private readonly _onCommandFinished = new Emitter(); + private readonly _onData = new Emitter(); private readonly _onExit = new Emitter(); private readonly _onClaimChanged = new Emitter(); private readonly _onDidSendText = new Emitter(); readonly onDidSendText = this._onDidSendText.event; private readonly _altBufferPromises: DeferredPromise[] = []; + private _content: string | undefined; async createTerminal(params: CreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise { this.created.push({ params, options: { ...options, shell: options?.shell ?? this.defaultShell } }); @@ -57,7 +59,7 @@ class TestAgentHostTerminalManager implements IAgentHostTerminalManager { this.writeInput(uri, formatTerminalText(data, options)); this._onDidSendText.fire(); } - onData(): IDisposable { return Disposable.None; } + onData(_uri: string, cb: (data: string) => void): IDisposable { return this._onData.event(cb); } onExit(_uri: string, cb: (exitCode: number) => void): IDisposable { return this._onExit.event(cb); } onClaimChanged(_uri: string, cb: (claim: TerminalClaim) => void): IDisposable { return this._onClaimChanged.event(cb); } onCommandFinished(_uri: string, cb: (event: ICommandFinishedEvent) => void): IDisposable { @@ -77,7 +79,7 @@ class TestAgentHostTerminalManager implements IAgentHostTerminalManager { }); return deferred.p; } - getContent(): string | undefined { return undefined; } + getContent(): string | undefined { return this._content; } getClaim(): TerminalClaim | undefined { return undefined; } hasTerminal(uri: string): boolean { return this.existingTerminalUris.has(uri); } getExitCode(): number | undefined { return undefined; } @@ -87,8 +89,10 @@ class TestAgentHostTerminalManager implements IAgentHostTerminalManager { getTerminalState(): undefined { return undefined; } async getDefaultShell(): Promise { return this.defaultShell; } fireCommandFinished(event: ICommandFinishedEvent): void { this._onCommandFinished.fire(event); } + fireData(data: string): void { this._onData.fire(data); } fireExit(exitCode: number): void { this._onExit.fire(exitCode); } fireClaimChanged(claim: TerminalClaim): void { this._onClaimChanged.fire(claim); } + setContent(content: string | undefined): void { this._content = content; } fireDidEnterAltBuffer(): void { for (const promise of [...this._altBufferPromises]) { promise.complete(); @@ -386,6 +390,41 @@ suite('CopilotShellTools', () => { assert.match(terminalManager.writes[1].data, /^echo "<<>>"\r$/); }); + test('primary shell tool ignores echoed sentinel command text', async () => { + const { instantiationService, terminalManager } = createServices(); + + const shellManager = disposables.add(instantiationService.createInstance(ShellManager, URI.parse('copilot:/session-1'), undefined)); + const tools = await createShellTools(shellManager, terminalManager, new NullLogService()); + const bashTool = tools.find(tool => tool.name === 'bash'); + assert.ok(bashTool); + + const invocation: ToolInvocation = { + sessionId: 'session-1', + toolCallId: 'tool-1', + toolName: 'bash', + arguments: { command: 'echo MOCKED_AGENT_HOST_SANDBOX_RESPONSE', timeout: 1000 }, + }; + const resultPromise = bashTool.handler!({ command: 'echo MOCKED_AGENT_HOST_SANDBOX_RESPONSE', timeout: 1000 }, invocation) as Promise; + await waitForSentTexts(terminalManager, 2); + + const sentinelMatch = terminalManager.writes[1].data.match(/<<>/); + assert.ok(sentinelMatch, 'sentinel marker should be present'); + const sentinelId = sentinelMatch[1]; + const content = [ + ' echo MOCKED_AGENT_HOST_SANDBOX_RESPONSE', + 'MOCKED_AGENT_HOST_SANDBOX_RESPONSE', + `echo "<<>>"`, + `<<>>`, + ].join('\r\n'); + terminalManager.setContent(content); + terminalManager.fireData(content); + + const result = await resultPromise; + assert.strictEqual(result.resultType, 'success'); + assert.match(result.textResultForLlm, /Exit code: 0/); + assert.match(result.textResultForLlm, /MOCKED_AGENT_HOST_SANDBOX_RESPONSE/); + }); + test('primary shell tool forces bracketed paste with shell integration', async () => { const { instantiationService, terminalManager } = createServices(); terminalManager.commandDetectionSupported = true; diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 6e953e60f144b1..386d2e3225cd7d 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -158,8 +158,8 @@ export class PlaywrightDriver { /** * Get the accessibility snapshot of the current window. */ - async getAccessibilitySnapshot(): Promise Promise ? T : never> { - return await this.page.accessibility.snapshot(); + async getAccessibilitySnapshot(): Promise { + return await this.page.ariaSnapshot({ mode: 'ai' }); } /** diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index 32ee0852cb9069..e1349d0718825b 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -1,19 +1,19 @@ #### editor/codeEditor/CodeEditor/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/67bfb687fd2818bd53771a60660541b9ed6f38b80d37da0aac15d267ecaeacec) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/39a05bcaca290be0d933d158af78e9d3602d803aecccbe110d2beaa338dce249) #### editor/codeEditor/CodeEditor/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/0469dd8d0a587d94a1eaec514c79917b93b9a38694ef2b767bb1892819ae0a55) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/c8b875cc4350b4022b93248fe13fe649e858ac2433c0fe6a03ed05f0bfb9b5e2) #### editor/inlineChatZoneWidget/InlineChatZoneWidget/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/34445847f76a97dce08d34a25919c6a7f273425a1c602526d3798c9079f83723) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/5d3f5b9f1f76888f2f9dd768c86395fd8a9985f2005d2fa13cc2d5bdc47bbf60) #### editor/inlineChatZoneWidget/InlineChatZoneWidget/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/b3b7ad4ccee01fe410c769addfc113b643549abf5cd53243d9709e5afecc7ec2) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/0c3faaa55d8324f533c2b6ac924beea8a7e83c53f88ba259771a68d9992d74ec) #### editor/inlineChatZoneWidget/InlineChatZoneWidgetTerminated/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/2fbc12507b59ff950d9612d2df92e6b39d8bf0bf500478e42eca2ead4d1ae206) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/a981db192b0d8b08b8ddc77853e01f9e1fd3ae411fcee97444fce68f1b10b851) #### editor/inlineChatZoneWidget/InlineChatZoneWidgetTerminated/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/4632ab04d1fdd7db9ab0e00cce10aefb7a6344eb8869dfce740309a8801cab73) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/515c76fe31f38499f2768e8939eebf68b78f5a01ea1af5841d1abc2b41d32fe4) diff --git a/test/componentFixtures/playwright/package-lock.json b/test/componentFixtures/playwright/package-lock.json index 0474afe05c7490..95e0e7f6ddca79 100644 --- a/test/componentFixtures/playwright/package-lock.json +++ b/test/componentFixtures/playwright/package-lock.json @@ -9,19 +9,19 @@ "version": "0.1.0", "license": "MIT", "devDependencies": { - "@playwright/test": "^1.52.0", + "@playwright/test": "^1.61.1", "@types/node": "24.x", "typescript": "^5.8.0" } }, "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.58.2" + "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -56,13 +56,13 @@ } }, "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.2" + "playwright-core": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -75,9 +75,9 @@ } }, "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/test/componentFixtures/playwright/package.json b/test/componentFixtures/playwright/package.json index debce5e338b767..04df31ad630303 100644 --- a/test/componentFixtures/playwright/package.json +++ b/test/componentFixtures/playwright/package.json @@ -10,7 +10,7 @@ "test:debug": "playwright test --debug" }, "devDependencies": { - "@playwright/test": "^1.52.0", + "@playwright/test": "^1.61.1", "@types/node": "24.x", "typescript": "^5.8.0" } diff --git a/test/mcp/src/automationTools/windows.ts b/test/mcp/src/automationTools/windows.ts index ab6076ec234349..4004626f29fc00 100644 --- a/test/mcp/src/automationTools/windows.ts +++ b/test/mcp/src/automationTools/windows.ts @@ -95,7 +95,7 @@ export function applyWindowTools(server: McpServer, appService: ApplicationServi const snapshot = await driver.getAccessibilitySnapshot(); const url = driver.currentPage.url(); - return textResponse(`Page snapshot (URL: ${url}):\n\n${JSON.stringify(snapshot, null, 2)}`); + return textResponse(`Page snapshot (URL: ${url}):\n\n${snapshot}`); } )); diff --git a/test/smoke/src/areas/agentsWindow/agentsWindow.test.ts b/test/smoke/src/areas/agentsWindow/agentsWindow.test.ts index dffc8d1f7662c3..063dedcb9f97b6 100644 --- a/test/smoke/src/areas/agentsWindow/agentsWindow.test.ts +++ b/test/smoke/src/areas/agentsWindow/agentsWindow.test.ts @@ -661,6 +661,15 @@ export function setup(logger: Logger) { // turns the sandbox on for the auto-approve path used by the sandbox test. 'chat.agentHost.customTerminalTool.enabled': true, 'chat.agent.sandbox.enabled': 'on', + // CI macOS runners commonly resolve the default shell as /bin/sh, which + // exercises the sentinel-based completion parser path. Force the same + // profile on macOS so local runs cover the same branch. + ...(process.platform === 'darwin' ? { + 'terminal.integrated.profiles.osx': { + 'Smoke AgentHost Sandbox sh': { path: '/bin/sh' }, + }, + 'terminal.integrated.defaultProfile.osx': 'Smoke AgentHost Sandbox sh', + } : {}), }, }); @@ -761,6 +770,13 @@ export function setup(logger: Logger) { engineShellRun, `expected the AgentHost's own shell engine ([ShellManager]) to have run the command in ${agentHostLogPath}` ); + if (process.platform === 'darwin') { + assert.match( + agentHostLog, + /\[ShellManager\] Created \w+ shell .*executable=\/bin\/sh\)/, + `expected the macOS AgentHost sandbox smoke test to run under /bin/sh (CI parity and sentinel-parser coverage), in ${agentHostLogPath}` + ); + } assert.doesNotMatch( agentHostLog, /Applied SDK sandboxConfig/, diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 0a34d3d30f087c..7a1737b9016231 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -134,7 +134,19 @@ function getTestTypeSuffix(): string { } } -const testDataPath = path.join(os.tmpdir(), `vscsmoke-${getTestTypeSuffix()}`); +function getTmpDir(): string { + const tmpDir = os.tmpdir(); + if (process.platform === 'win32') { + try { + return fs.realpathSync.native(tmpDir); + } catch { + // ignore and fall back to the short path + } + } + return tmpDir; +} + +const testDataPath = path.join(getTmpDir(), `vscsmoke-${getTestTypeSuffix()}`); if (fs.existsSync(testDataPath)) { fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 }); } From 5fa5adcec924c086fb7d00f32790e04a91a99340 Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci Date: Mon, 29 Jun 2026 12:00:59 +0200 Subject: [PATCH 038/589] agentHost: surface SDK download as one progress stream per provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK download is provider-global — one physical fetch shared by every session of a provider — but each new-session composer minted its own `progressToken` and the host fanned one frame out per token. So clicking New Session repeatedly stacked duplicate download toasts, and switching provider left the old toast hung (its provisional session was discarded, so no terminal frame ever arrived for that token). Fix the modelling at the source: the host now emits a SINGLE `progress` stream per download, keyed by the download's own stable identity (the package id), rather than fanning per session token. The per-`createSession` `progressToken` is kept purely as an opt-in signal — the host tracks which sessions are interested and emits while at least one is, but the token it echoes is the download's, shared across the provider. The client keys indicators by `progressToken` (now stable per download) and dismisses on the host's terminal frame, so there is exactly one indicator with a deterministic lifecycle — no message-keying or idle-timeout heuristics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../platform/agentHost/node/agentService.ts | 82 ++++++++++--------- .../browser/baseAgentHostSessionsProvider.ts | 41 +++++----- 2 files changed, 65 insertions(+), 58 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 6ef83fb9364f5d..d1b15d03f5e1b3 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -120,16 +120,17 @@ export class AgentService extends Disposable implements IAgentService { /** Maps each active session URI (toString) to its owning provider. */ private readonly _sessionToProvider = new Map(); /** - * Pending download-progress tokens, keyed by provider id then session URI - * (toString). A session is registered here when its `createSession` carries - * a {@link IAgentCreateSessionConfig.progressToken} and removed once it + * Sessions that have opted in to bring-up progress, keyed by provider id. + * A session is added here when its `createSession` carries a + * {@link IAgentCreateSessionConfig.progressToken} and removed once it * materializes (the SDK is now resolved) or is disposed. The SDK download is - * host-level and shared across every session of a provider, and its progress - * events are keyed by package id (which equals the provider id) — so on a - * download frame we fan it out as one `progress` notification per waiting - * session's token. See {@link emitDownloadProgress}. + * host-level and shared across every session of a provider, so this only + * records *interest*: as long as one or more sessions of a provider is + * registered, {@link emitDownloadProgress} surfaces that provider's download as a single + * progress stream keyed by the download's own identity (the package id), + * rather than one stream per session. */ - private readonly _downloadProgressTokens = new Map>(); + private readonly _downloadProgressInterest = new Map>(); /** Subscriptions to provider progress events; cleared when providers change. */ private readonly _providerSubscriptions = this._register(new DisposableStore()); private readonly _authService: AgentHostAuthenticationService; @@ -617,16 +618,18 @@ export class AgentService extends Disposable implements IAgentService { this._logService.trace(`[AgentService] createSession: provider=${provider.id} model=${config?.model?.id ?? '(default)'}`); this._sessionToProvider.set(session.toString(), provider.id); - // Register the client's opt-in progress token so a cold SDK download - // triggered at materialization (first message) reports back to the - // `createSession` call that requested it. Cleared on materialize/dispose. + // Record this session's opt-in so a cold SDK download triggered at + // materialization (first message) is surfaced as progress. The download + // is provider-global, so we only track interest here; emission is keyed + // by the download's own identity, not this token. Cleared on + // materialize/dispose. if (config?.progressToken) { - let bySession = this._downloadProgressTokens.get(provider.id); - if (!bySession) { - bySession = new Map(); - this._downloadProgressTokens.set(provider.id, bySession); + let sessions = this._downloadProgressInterest.get(provider.id); + if (!sessions) { + sessions = new Set(); + this._downloadProgressInterest.set(provider.id, sessions); } - bySession.set(session.toString(), config.progressToken); + sessions.add(session.toString()); } this._logService.trace(`[AgentService] createSession returned: ${session.toString()}`); @@ -843,7 +846,7 @@ export class AgentService extends Disposable implements IAgentService { const sessionKey = e.session.toString(); // The session is now materialized — its SDK is resolved (any cold // download already finished), so no further progress is expected for it. - this._clearDownloadProgressToken(sessionKey); + this._clearDownloadProgressInterest(sessionKey); const state = this._stateManager.getSessionState(sessionKey); if (!state) { this._logService.warn(`[AgentService] onDidMaterializeSession for unknown session: ${sessionKey}`); @@ -883,24 +886,26 @@ export class AgentService extends Disposable implements IAgentService { this._changesetCoordinator.onSessionMaterialized(sessionKey); } - /** Remove a session's pending download-progress token, if any. */ - private _clearDownloadProgressToken(sessionKey: string): void { - for (const [provider, bySession] of this._downloadProgressTokens) { - if (bySession.delete(sessionKey) && bySession.size === 0) { - this._downloadProgressTokens.delete(provider); + /** Drop a session's download-progress opt-in, if any. */ + private _clearDownloadProgressInterest(sessionKey: string): void { + for (const [provider, sessions] of this._downloadProgressInterest) { + if (sessions.delete(sessionKey) && sessions.size === 0) { + this._downloadProgressInterest.delete(provider); } } } /** - * Fan a host-level SDK download-progress sample out to the clients waiting - * on it. The downloader fires process-global frames keyed by package id - * (which equals the provider id); we emit one generic `progress` - * notification per session of that provider that supplied a - * {@link IAgentCreateSessionConfig.progressToken} on `createSession`, so the - * client can correlate it back to that call. A terminal frame reports - * `total === progress` (using `receivedBytes` when the size was never known) - * so clients dismiss the indicator, and clears the provider's tokens. + * Surface a host-level SDK download as client progress. The downloader fires + * process-global frames keyed by package id (which equals the provider id); + * because the download is shared across every session of that provider, we + * emit a SINGLE `progress` stream keyed by that package id — not one per + * session — so the client shows exactly one indicator no matter how many + * sessions of the provider are awaiting it. Frames are only emitted while at + * least one session has opted in (supplied a + * {@link IAgentCreateSessionConfig.progressToken} on `createSession`). A + * terminal frame reports `total === progress` (using `receivedBytes` when the + * size was never known) so the client dismisses the indicator deterministically. * * `displayName` is the provider's brand noun (e.g. `Claude`). It is woven * into the notification's localized, human-readable `message` (e.g. @@ -908,8 +913,8 @@ export class AgentService extends Disposable implements IAgentService { * verbatim without knowing the resource is an agent SDK. */ emitDownloadProgress(packageId: string, displayName: string, receivedBytes: number, totalBytes: number | undefined, terminal: boolean): void { - const bySession = this._downloadProgressTokens.get(packageId); - if (!bySession || bySession.size === 0) { + const sessions = this._downloadProgressInterest.get(packageId); + if (!sessions || sessions.size === 0) { return; } // On a terminal frame force `progress === total` so clients treat the @@ -918,11 +923,12 @@ export class AgentService extends Disposable implements IAgentService { // the real error surfaces via the session-failure path). const total = terminal ? receivedBytes : totalBytes; const message = localize('agentHost.download.agentSdkTitle', "Downloading {0} agent…", displayName); - for (const progressToken of new Set(bySession.values())) { - this._stateManager.emitProgress({ progressToken, progress: receivedBytes, total, message }); - } + // `progressToken` is the download's own stable identity (the package id), + // shared by every session of the provider, so the client coalesces all + // frames into one indicator and dismisses it on the terminal frame. + this._stateManager.emitProgress({ progressToken: packageId, progress: receivedBytes, total, message }); if (terminal) { - this._downloadProgressTokens.delete(packageId); + this._downloadProgressInterest.delete(packageId); } } @@ -990,7 +996,7 @@ export class AgentService extends Disposable implements IAgentService { if (provider) { await provider.disposeSession(session); this._sessionToProvider.delete(session.toString()); - this._clearDownloadProgressToken(session.toString()); + this._clearDownloadProgressInterest(session.toString()); } this._changesetCoordinator.onSessionDisposed(session.toString()); this._sideEffects.cancelSessionTitleGeneration(session.toString()); @@ -2260,7 +2266,7 @@ export class AgentService extends Disposable implements IAgentService { } await Promise.all(promises); this._sessionToProvider.clear(); - this._downloadProgressTokens.clear(); + this._downloadProgressInterest.clear(); } // ---- helpers ------------------------------------------------------------ diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts index f56de968d27ea1..2730ffd387ac0d 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -1479,11 +1479,14 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement protected readonly _sessionCache = new Map(); /** - * Active progress indicators keyed by `progressToken`. Each entry owns one - * long-running notification progress (opened on the first frame) that is - * driven via {@link IActiveDownload.report} and dismissed via - * {@link IActiveDownload.complete} once `progress >= total`. See - * {@link _handleProgress}. + * Active progress indicators keyed by `progressToken`. Today's only + * producer is the agent host's lazy, first-use SDK download, which is + * provider-global: the host emits a single stream per download keyed by the + * download's own stable identity (so distinct sessions of a provider share + * one indicator). Each entry owns one long-running notification progress + * (opened on the first frame), driven via {@link IActiveDownload.report} and + * dismissed via {@link IActiveDownload.complete} once `progress >= total`. + * See {@link _handleProgress}. */ private readonly _activeDownloads = new Map(); @@ -3451,14 +3454,14 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement /** * Render a generic `progress` notification as a notification progress bar. - * Progress is correlated to its originating `createSession` call by - * {@link ProgressParams.progressToken}; today the only producer is the - * agent host's lazy, first-use SDK download (shared across a provider's - * sessions). We surface one notification per `progressToken`: determinate - * when the host knows the `total` (`Content-Length`), or a byte-count spinner - * otherwise. The operation is complete — and the notification dismissed — - * once `progress >= total`. The human-readable brand noun rides on - * {@link ProgressParams.message}. + * Progress is correlated by {@link ProgressParams.progressToken}; today's + * only producer is the agent host's lazy, first-use SDK download, which the + * host surfaces as a single stream per provider keyed by the download's own + * stable identity — so one indicator per download regardless of how many + * sessions await it. Determinate when the host knows the `total` + * (`Content-Length`), or a byte-count spinner otherwise. The operation is + * complete — and the notification dismissed — once `progress >= total`. The + * human-readable brand noun rides on {@link ProgressParams.message}. */ private _handleProgress(progress: ProgressParams): void { // New AI UI must stay hidden when the user has turned AI features off. @@ -3479,15 +3482,13 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement let entry = this._activeDownloads.get(progress.progressToken); if (!entry) { - // First frame for this token (or one we joined late): open one - // long-running notification progress and drive it via `report` - // until a terminal frame resolves `deferred`. + // First frame for this download: open one long-running notification + // progress and drive it via `report` until a terminal frame resolves + // `deferred`. `message` is the host-supplied, already-localized title + // (e.g. "Downloading Claude agent…"); render it verbatim so this stays + // a generic indicator that makes no assumption about what's downloading. const deferred = new DeferredPromise(); let report: ((step: IProgressStep) => void) | undefined; - // `message` is the host-supplied, already-localized human-readable - // title (e.g. "Downloading Claude agent…"). Render it verbatim so - // this stays a generic progress indicator that makes no assumption - // about what is being downloaded; fall back only if a producer omits it. const title = progress.message ?? localize('agentHost.download.titleFallback', "Downloading…"); this._progressService.withProgress( { From 717cc576423b1b2e7bc3857808ee7dd309639c8b Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:17:43 +0200 Subject: [PATCH 039/589] Merge pull request #323452 from microsoft/alexr00/awful-newt Add flaky test pipelines --- .../darwin/product-smoke-flaky-darwin.yml | 61 ++++++ .../linux/product-smoke-flaky-linux.yml | 76 ++++++++ build/azure-pipelines/product-smoke-flaky.yml | 181 ++++++++++++++++++ .../win32/product-smoke-flaky-win32.yml | 60 ++++++ 4 files changed, 378 insertions(+) create mode 100644 build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml create mode 100644 build/azure-pipelines/linux/product-smoke-flaky-linux.yml create mode 100644 build/azure-pipelines/product-smoke-flaky.yml create mode 100644 build/azure-pipelines/win32/product-smoke-flaky-win32.yml diff --git a/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml b/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml new file mode 100644 index 00000000000000..1980fbb23ea0f9 --- /dev/null +++ b/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml @@ -0,0 +1,61 @@ +parameters: + - name: iterations + type: object + +jobs: + - job: macOSSmokeFlaky + displayName: macOS (Electron) + timeoutInMinutes: 300 + variables: + VSCODE_ARCH: arm64 + BUILDS_API_URL: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/crashes + artifactName: crash-dump-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Crash Reports + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/logs + artifactName: logs-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Log Files + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + steps: + # Build VS Code from source. No VSCODE_RUN_*_TESTS flags are passed, so the + # compile template builds the client/server but skips compiling the test + # suites and skips the unit/integration/smoke test template entirely — we + # run only the smoke tests below, nothing else. + - template: ./steps/product-build-darwin-compile.yml@self + parameters: + VSCODE_ARCH: arm64 + VSCODE_CIBUILD: true + + - script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" + env: + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) + displayName: Download Electron and Playwright + retryCountOnTaskFailure: 3 + + # The compile template only compiles the smoke suite when a test flag is + # set; since we pass none, compile it explicitly here. + - script: npm run compile --prefix test/smoke + displayName: Compile smoke tests + + # Run the Electron smoke test once per iteration. continueOnError lets every + # iteration run even if some fail, and gives each run its own timeline record + # so the SmokeFlaky function can tally passes vs. failures. + - ${{ each i in parameters.iterations }}: + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + APP_NAME="`ls $APP_ROOT | head -n 1`" + npm run smoketest-no-compile -- --tracing --build "$APP_ROOT/$APP_NAME" + displayName: "🧪 Smoke test iteration ${{ i }}/${{ length(parameters.iterations) }} (Electron)" + continueOnError: true + timeoutInMinutes: 20 diff --git a/build/azure-pipelines/linux/product-smoke-flaky-linux.yml b/build/azure-pipelines/linux/product-smoke-flaky-linux.yml new file mode 100644 index 00000000000000..7618c9b31264c7 --- /dev/null +++ b/build/azure-pipelines/linux/product-smoke-flaky-linux.yml @@ -0,0 +1,76 @@ +parameters: + - name: VSCODE_QUALITY + type: string + - name: iterations + type: object + +jobs: + - job: LinuxSmokeFlaky + displayName: Linux (Electron) + timeoutInMinutes: 300 + variables: + DISPLAY: ":10" + NPM_ARCH: x64 + VSCODE_ARCH: x64 + BUILDS_API_URL: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/crashes + artifactName: crash-dump-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Crash Reports + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/logs + artifactName: logs-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Log Files + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + steps: + # Build VS Code from source. No VSCODE_RUN_*_TESTS flags are passed, so the + # compile template builds the client/server but skips compiling the test + # suites and skips the unit/integration/smoke test template entirely — we + # run only the smoke tests below, nothing else. + - template: ./steps/product-build-linux-compile.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_CIBUILD: true + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + + - script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" + env: + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) + displayName: Download Electron and Playwright + retryCountOnTaskFailure: 3 + + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + ELECTRON_ROOT=.build/electron + sudo chown root $APP_ROOT/chrome-sandbox + sudo chown root $ELECTRON_ROOT/chrome-sandbox + sudo chmod 4755 $APP_ROOT/chrome-sandbox + sudo chmod 4755 $ELECTRON_ROOT/chrome-sandbox + displayName: Change setuid helper binary permission + + # The compile template only compiles the smoke suite when a test flag is + # set; since we pass none, compile it explicitly here. + - script: npm run compile --prefix test/smoke + displayName: Compile smoke tests + + # Run the Electron smoke test once per iteration. continueOnError lets every + # iteration run even if some fail, and gives each run its own timeline record + # so the SmokeFlaky function can tally passes vs. failures. + - ${{ each i in parameters.iterations }}: + - script: | + set -e + npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" + env: + TMPDIR: $(Agent.TempDirectory) + displayName: "🧪 Smoke test iteration ${{ i }}/${{ length(parameters.iterations) }} (Electron)" + continueOnError: true + timeoutInMinutes: 20 diff --git a/build/azure-pipelines/product-smoke-flaky.yml b/build/azure-pipelines/product-smoke-flaky.yml new file mode 100644 index 00000000000000..2588e57679e637 --- /dev/null +++ b/build/azure-pipelines/product-smoke-flaky.yml @@ -0,0 +1,181 @@ +# Daily smoke-test flaky-detection pipeline. +# +# Builds VS Code from source on Linux, Windows and macOS and runs ONLY the +# Electron smoke tests, repeated 20 times per platform. Each repetition is its +# own step (continueOnError) so a single flaky failure does not abort the run and +# every iteration produces its own timeline record. The pass/fail tally is read +# back from the build timeline by the vscode-probot `SmokeFlaky` Azure Function +# (wired up via an ADO service hook on build completion), which posts a summary +# to the Slack #build channel. +# +# We build from source rather than testing a published build on purpose: product +# builds are not always released, so depending on a published artifact would make +# us miss the daily schedule. Building here keeps the run self-contained. + +pr: none +trigger: none + +schedules: + # Once per day at 04:00 UTC (= 5am CET / 6am CEST) — a quiet window for the ADO + # org. `always: true` forces the run even when there are no new commits, since + # flaky behavior is environmental and worth sampling every day regardless of + # source changes. + - cron: "0 4 * * *" + displayName: Daily at 04:00 UTC / 5am CET (smoke flaky detection) + branches: + include: + - main + always: true + +parameters: + - name: VSCODE_QUALITY + displayName: Quality + type: string + default: insider + values: + - exploration + - insider + - stable + - name: NPM_REGISTRY + displayName: "Custom NPM Registry" + type: string + default: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/npm/registry/" + # The smoke test is run once per entry in this list. 20 entries => 20 runs per + # platform (the flaky-detection sample size). Exposed as an object so it can be + # shortened for manual debugging runs from the ADO UI. + - name: iterations + displayName: "Smoke test iterations (one run per entry)" + type: object + default: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + - name: VSCODE_BUILD_MACOS + displayName: "🎯 macOS arm64" + type: boolean + default: true + - name: VSCODE_BUILD_LINUX + displayName: "🎯 Linux x64" + type: boolean + default: true + - name: VSCODE_BUILD_WIN32 + displayName: "🎯 Windows x64" + type: boolean + default: true + +variables: + - name: VSCODE_QUALITY + value: ${{ parameters.VSCODE_QUALITY }} + - name: NPM_REGISTRY + value: ${{ parameters.NPM_REGISTRY }} + # Compile templates skip publish/codesign/artifact-staging when VSCODE_CIBUILD + # is true; it's passed directly as a job parameter. VSCODE_STEP_ON_IT must be + # 'false' to keep the build steps enabled. + - name: VSCODE_STEP_ON_IT + value: false + - name: VSCODE_MIXIN_REPO + value: microsoft/vscode-distro + - name: VSCODE_OVERWRITE_TPN + value: "true" + - name: skipComponentGovernanceDetection + value: true + - name: Codeql.SkipTaskAutoInjection + value: true + +name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.VSCODE_QUALITY }} smoke flaky)" + +resources: + repositories: + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + - repository: distro + type: github + name: microsoft/vscode-distro + endpoint: Monaco + ref: main + - repository: capi + type: github + name: microsoft/vscode-capi + endpoint: Monaco + ref: main + - repository: encrypt + type: github + name: microsoft/vscode-encrypt + endpoint: Monaco + ref: main + - repository: vsda + type: github + name: microsoft/vsda + endpoint: Monaco + ref: main + - repository: regexp + type: github + name: microsoft/vscode-regexp-languagedetection + endpoint: Monaco + ref: main + - repository: vscode_loc + type: github + name: microsoft/vscode-extensions-loc + endpoint: Monaco + ref: main + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + sdl: + sourceRepositoriesToScan: + exclude: + - repository: distro + - repository: capi + - repository: encrypt + - repository: vsda + - repository: regexp + - repository: vscode_loc + tsa: + enabled: false + codeql: + compiled: + enabled: false + justificationForDisabling: Smoke test repetition only, not a shipping build + credscan: + suppressionsFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/CredScanSuppressions.json + eslint: + enabled: false + binskim: + enabled: false + sourceAnalysisPool: 1es-windows-2022-x64 + createAdoIssuesForJustificationsForDisablement: false + stages: + - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: + - stage: Linux + dependsOn: [] + pool: + name: 1es-ubuntu-22.04-x64 + os: linux + jobs: + - template: linux/product-smoke-flaky-linux.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + iterations: ${{ parameters.iterations }} + + - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: + - stage: Windows + dependsOn: [] + pool: + name: 1es-windows-2022-x64 + os: windows + jobs: + - template: win32/product-smoke-flaky-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + iterations: ${{ parameters.iterations }} + + - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: + - stage: macOS + dependsOn: [] + pool: + name: AcesShared + os: macOS + jobs: + - template: darwin/product-smoke-flaky-darwin.yml@self + parameters: + iterations: ${{ parameters.iterations }} diff --git a/build/azure-pipelines/win32/product-smoke-flaky-win32.yml b/build/azure-pipelines/win32/product-smoke-flaky-win32.yml new file mode 100644 index 00000000000000..a72800550a3869 --- /dev/null +++ b/build/azure-pipelines/win32/product-smoke-flaky-win32.yml @@ -0,0 +1,60 @@ +parameters: + - name: VSCODE_QUALITY + type: string + - name: iterations + type: object + +jobs: + - job: WindowsSmokeFlaky + displayName: Windows (Electron) + timeoutInMinutes: 300 + variables: + VSCODE_ARCH: x64 + BUILDS_API_URL: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/crashes + artifactName: crash-dump-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Crash Reports + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/.build/logs + artifactName: logs-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) + displayName: Publish Log Files + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + steps: + # Build VS Code from source. No VSCODE_RUN_*_TESTS flags are passed, so the + # compile template builds the client/server but skips compiling the test + # suites and skips the unit/integration/smoke test template entirely — we + # run only the smoke tests below, nothing else. + - template: ./steps/product-build-win32-compile.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_CIBUILD: true + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + + - powershell: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" + env: + GITHUB_TOKEN: "$(github-token-code-oss)" + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) + displayName: Download Electron and Playwright + retryCountOnTaskFailure: 3 + + # The compile template only compiles the smoke suite when a test flag is + # set; since we pass none, compile it explicitly here. + - powershell: npm run compile --prefix test/smoke + displayName: Compile smoke tests + + # Run the Electron smoke test once per iteration. continueOnError lets every + # iteration run even if some fail, and gives each run its own timeline record + # so the SmokeFlaky function can tally passes vs. failures. + - ${{ each i in parameters.iterations }}: + - powershell: npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + displayName: "🧪 Smoke test iteration ${{ i }}/${{ length(parameters.iterations) }} (Electron)" + continueOnError: true + timeoutInMinutes: 20 From 2767857fe8463c949393bcad179235b58b721403 Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" Date: Mon, 29 Jun 2026 11:47:32 +0000 Subject: [PATCH 040/589] [cherry-pick] Experiment-control Markdown default editor in Agents window, default off --- .../parts/editor/editorConfiguration.ts | 16 ++++++++-- .../editor/common/editorResolverService.ts | 31 ++++++++++++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorConfiguration.ts b/src/vs/workbench/browser/parts/editor/editorConfiguration.ts index ff6df83a56c0ae..b2739bc4821bf4 100644 --- a/src/vs/workbench/browser/parts/editor/editorConfiguration.ts +++ b/src/vs/workbench/browser/parts/editor/editorConfiguration.ts @@ -9,13 +9,14 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import { workbenchConfigurationNodeBase } from '../../../common/configuration.js'; -import { diffEditorsAssociationsSettingId, editorsAssociationsAgentsWindowDefault, editorsAssociationsSettingId, IEditorResolverService, RegisteredEditorInfo, RegisteredEditorPriority, toRegisteredEditorPriorityInfo } from '../../../services/editor/common/editorResolverService.js'; +import { diffEditorsAssociationsSettingId, editorsAssociationsAgentsWindowDefault, editorsAssociationsSettingId, IEditorResolverService, markdownDefaultEditorAgentsWindowSettingId, RegisteredEditorInfo, RegisteredEditorPriority, toRegisteredEditorPriorityInfo } from '../../../services/editor/common/editorResolverService.js'; import { IJSONSchemaMap } from '../../../../base/common/jsonSchema.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { Event } from '../../../../base/common/event.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { ByteSize, getLargeFileConfirmationLimit } from '../../../../platform/files/common/files.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; export class DynamicEditorConfigurations extends Disposable implements IWorkbenchContribution { @@ -76,7 +77,8 @@ export class DynamicEditorConfigurations extends Disposable implements IWorkbenc constructor( @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @IExtensionService extensionService: IExtensionService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); @@ -96,6 +98,13 @@ export class DynamicEditorConfigurations extends Disposable implements IWorkbenc // Registered editors (debounced to reduce perf overhead) this._register(Event.debounce(this.editorResolverService.onDidChangeEditorRegistrations, (_, e) => e)(() => this.updateDynamicEditorConfigurations())); + + // Re-register when the Agents window Markdown default editor setting changes + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(markdownDefaultEditorAgentsWindowSettingId)) { + this.updateDynamicEditorConfigurations(); + } + })); } private updateDynamicEditorConfigurations(): void { @@ -150,6 +159,7 @@ export class DynamicEditorConfigurations extends Disposable implements IWorkbenc // Registers setting for editorAssociations const oldEditorAssociationsConfigurationNode = this.editorAssociationsConfigurationNode; + const markdownDefaultEditorEnabled = this.configurationService.getValue(markdownDefaultEditorAgentsWindowSettingId) === true; this.editorAssociationsConfigurationNode = { ...workbenchConfigurationNodeBase, properties: { @@ -163,7 +173,7 @@ export class DynamicEditorConfigurations extends Disposable implements IWorkbenc } }, agentsWindow: { - default: editorsAssociationsAgentsWindowDefault + default: editorsAssociationsAgentsWindowDefault({ markdownDefaultEditor: markdownDefaultEditorEnabled }) } } } diff --git a/src/vs/workbench/services/editor/common/editorResolverService.ts b/src/vs/workbench/services/editor/common/editorResolverService.ts index 83332416098eab..34e2bbe610a4c2 100644 --- a/src/vs/workbench/services/editor/common/editorResolverService.ts +++ b/src/vs/workbench/services/editor/common/editorResolverService.ts @@ -38,18 +38,39 @@ export const editorsAssociationsSettingId = 'workbench.editorAssociations'; export const diffEditorsAssociationsSettingId = 'workbench.diffEditorAssociations'; /** - * Default value for `workbench.editorAssociations` in the Agents window. + * Setting that controls whether the Markdown editor is the default editor for + * `*.md` files in the Agents window. Gated behind an experiment so it can be + * rolled out gradually. Defaults to off. + */ +export const markdownDefaultEditorAgentsWindowSettingId = 'workbench.editor.markdownDefaultEditorInAgentsWindow'; + +/** + * Builds the default value for `workbench.editorAssociations` in the Agents window. * Shared so that dynamic re-registrations of the setting preserve the override. + * + * Each editor association can be toggled independently. Passing `undefined` + * leaves the association at its enabled default, so the static registration + * ends up with all defaults registered. Pass `false` to fall back to the + * markdown preview editor for `*.md` files. */ -export const editorsAssociationsAgentsWindowDefault: Readonly> = Object.freeze({ - '*.md': 'vscode.markdown.editor' -}); +export function editorsAssociationsAgentsWindowDefault(options?: { markdownDefaultEditor?: boolean }): Record { + return { + '*.md': options?.markdownDefaultEditor === true ? 'vscode.markdown.editor' : 'vscode.markdown.preview.editor' + }; +} const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); const editorAssociationsConfigurationNode: IConfigurationNode = { ...workbenchConfigurationNodeBase, properties: { + [markdownDefaultEditorAgentsWindowSettingId]: { + type: 'boolean', + default: false, + tags: ['experimental'], + experiment: { mode: 'startup' }, + markdownDescription: localize('editor.markdownDefaultEditorInAgentsWindow', "Controls whether the Markdown editor is used as the default editor for Markdown files in the Agents window."), + }, [editorsAssociationsSettingId]: { type: 'object', markdownDescription: localize('editor.editorAssociations', "Configure [glob patterns](https://aka.ms/vscode-glob-patterns) to editors (for example `\"*.hex\": \"hexEditor.hexedit\"`). These have precedence over the default behavior."), @@ -57,7 +78,7 @@ const editorAssociationsConfigurationNode: IConfigurationNode = { type: 'string' }, agentsWindow: { - default: editorsAssociationsAgentsWindowDefault + default: editorsAssociationsAgentsWindowDefault() } }, [diffEditorsAssociationsSettingId]: { From 5eedf13f349408d43d4dfe6ac2e65966eb5295d3 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 29 Jun 2026 16:28:30 +0200 Subject: [PATCH 041/589] feat: adding code actions for github and playwright server enablement (#323469) * feat: adding code actions for github and playwright server enablement * removing prompt validator --- .../languageProviders/promptCodeActions.ts | 53 ++++++++++++++- .../languageProviders/promptValidator.ts | 67 +++++++++++++++++-- .../promptCodeActions.test.ts | 58 +++++++++++++++- 3 files changed, 169 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts index eba207145d1558..6b619c36901b49 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts @@ -16,7 +16,7 @@ import { Selection } from '../../../../../../editor/common/core/selection.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; import { LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { MARKERS_OWNER_ID } from './promptValidator.js'; +import { MARKERS_OWNER_ID, PromptValidatorMarkerCode } from './promptValidator.js'; import { IMarkerData, IMarkerService } from '../../../../../../platform/markers/common/markers.js'; import { CodeActionKind } from '../../../../../../editor/contrib/codeAction/common/types.js'; import { getTarget, isVSCodeOrDefaultTarget } from './promptFileAttributes.js'; @@ -48,11 +48,13 @@ export class PromptCodeActionProvider implements CodeActionProvider { switch (promptType) { case PromptsType.agent: this.getUpdateToolsCodeActions(promptAST, promptType, model, range, result); + this.getEnableMcpServerCodeActions(model, range, result); await this.getMigrateModeFileCodeActions(model, result); break; case PromptsType.prompt: this.getUpdateModeCodeActions(promptAST, model, range, result); this.getUpdateToolsCodeActions(promptAST, promptType, model, range, result); + this.getEnableMcpServerCodeActions(model, range, result); break; } @@ -71,16 +73,61 @@ export class PromptCodeActionProvider implements CodeActionProvider { return markers.filter(marker => range.containsRange(marker)); } - private createCodeAction(model: ITextModel, range: Range, title: string, edits: Array): CodeAction { + private createCodeAction(model: ITextModel, range: Range, title: string, edits?: Array, command?: { id: string; title: string; arguments?: unknown[] }): CodeAction { return { title, - edit: { edits }, + ...(edits ? { edit: { edits } } : {}), + ...(command ? { command } : {}), ranges: [range], diagnostics: this.getMarkers(model, range), kind: CodeActionKind.QuickFix.value }; } + private getEnableMcpServerCodeActions(model: ITextModel, range: Range, result: CodeAction[]): void { + const markersInRange = this.getMarkersInRange(model, range); + if (markersInRange.some(marker => this.getMarkerCode(marker) === PromptValidatorMarkerCode.MissingGithubMcpServer)) { + result.push(this.createCodeAction( + model, + range, + localize('enableGithubMcpServerSetting', "Enable Built-in GitHub MCP Server"), + undefined, + { id: 'workbench.action.openSettings', title: '', arguments: ['@id:github.copilot.chat.githubMcpServer.enabled'] } + )); + result.push(this.createCodeAction( + model, + range, + localize('installGithubMcpServer', "Install GitHub MCP Server from Marketplace"), + undefined, + { id: 'workbench.extensions.search', title: '', arguments: ['@mcp github'] } + )); + } + if (markersInRange.some(marker => this.getMarkerCode(marker) === PromptValidatorMarkerCode.MissingPlaywrightMcpServer)) { + result.push(this.createCodeAction( + model, + range, + localize('installPlaywrightMcpServer', "Install Playwright MCP Server from Marketplace"), + undefined, + { id: 'workbench.extensions.search', title: '', arguments: ['@mcp playwright'] } + )); + } + } + + private getMarkerCode(marker: IMarkerData): string | undefined { + if (!marker.code) { + return undefined; + } + return typeof marker.code === 'string' ? marker.code : marker.code.value; + } + + private getMarkersInRange(model: ITextModel, range: Range): IMarkerData[] { + const markers = this.markerService.read({ resource: model.uri, owner: MARKERS_OWNER_ID }); + return markers.filter(marker => { + const markerRange = new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn); + return markerRange.intersectRanges(range); + }); + } + private getUpdateModeCodeActions(promptFile: ParsedPromptFile, model: ITextModel, range: Range, result: CodeAction[]): void { const modeAttr = promptFile.header?.getAttribute(PromptHeaderAttributes.mode); if (!modeAttr?.range.containsRange(range)) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index d3be4e1e9dc811..1bccde649277a6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -29,6 +29,11 @@ import { ILogService } from '../../../../../../platform/log/common/log.js'; export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; +export const enum PromptValidatorMarkerCode { + MissingGithubMcpServer = 'promptValidator.missingGithubMcpServer', + MissingPlaywrightMcpServer = 'promptValidator.missingPlaywrightMcpServer', +} + export class PromptValidator { constructor( @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @@ -200,7 +205,17 @@ export class PromptValidator { } } } else { - report(toMarker(localize('promptValidator.unknownVariableReference', "Unknown tool or toolset '{0}'.", variable.name), variable.range, MarkerSeverity.Hint, [MarkerTag.Unnecessary])); + const missingGithubServerMarker = this.getMissingGithubMcpServerMarker(variable.name, variable.range); + if (missingGithubServerMarker) { + report(missingGithubServerMarker); + } else { + const missingPlaywrightServerMarker = this.getMissingPlaywrightMcpServerMarker(variable.name, variable.range); + if (missingPlaywrightServerMarker) { + report(missingPlaywrightServerMarker); + } else { + report(toMarker(localize('promptValidator.unknownVariableReference', "Unknown tool or toolset '{0}'.", variable.name), variable.range, MarkerSeverity.Hint, [MarkerTag.Unnecessary])); + } + } } } else if (headerToolsMap) { const tool = this.languageModelToolsService.getToolByFullReferenceName(variable.name); @@ -527,7 +542,17 @@ export class PromptValidator { report(toMarker(localize('promptValidator.toolDeprecatedMultipleNames', "Tool or toolset '{0}' has been renamed, use the following tools instead: {1}", item.value, newNames), item.range, MarkerSeverity.Info, [MarkerTag.Deprecated])); } } else { - report(toMarker(localize('promptValidator.toolNotFound', "Unknown tool '{0}' will be ignored.", item.value), item.range, MarkerSeverity.Hint, [MarkerTag.Unnecessary])); + const missingGithubServerMarker = this.getMissingGithubMcpServerMarker(item.value, item.range); + if (missingGithubServerMarker) { + report(missingGithubServerMarker); + } else { + const missingPlaywrightServerMarker = this.getMissingPlaywrightMcpServerMarker(item.value, item.range); + if (missingPlaywrightServerMarker) { + report(missingPlaywrightServerMarker); + } else { + report(toMarker(localize('promptValidator.toolNotFound', "Unknown tool '{0}' will be ignored.", item.value), item.range, MarkerSeverity.Hint, [MarkerTag.Unnecessary])); + } + } } } } @@ -535,6 +560,40 @@ export class PromptValidator { } } + private getMissingGithubMcpServerMarker(toolReferenceName: string, range: Range): IMarkerData | undefined { + if (toolReferenceName !== 'github/*') { + return undefined; + } + return toMarker( + localize( + 'promptValidator.missingGithubMcpServer', + "Tool alias '{0}' requires the GitHub MCP server. Enable the built-in server with setting 'github.copilot.chat.githubMcpServer.enabled' or install extension 'io.github.github/github-mcp-server' from Extensions (`@mcp github`).", + toolReferenceName + ), + range, + MarkerSeverity.Warning, + undefined, + PromptValidatorMarkerCode.MissingGithubMcpServer + ); + } + + private getMissingPlaywrightMcpServerMarker(toolReferenceName: string, range: Range): IMarkerData | undefined { + if (toolReferenceName !== 'playwright/*') { + return undefined; + } + return toMarker( + localize( + 'promptValidator.missingPlaywrightMcpServer', + "Tool alias '{0}' requires the Playwright MCP server. Install it from Extensions (`@mcp playwright`).", + toolReferenceName + ), + range, + MarkerSeverity.Warning, + undefined, + PromptValidatorMarkerCode.MissingPlaywrightMcpServer + ); + } + private validateApplyTo(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.applyTo); if (!attribute) { @@ -1223,6 +1282,6 @@ export function getTarget(promptType: PromptsType, header: PromptHeader | URI): return Target.Undefined; } -function toMarker(message: string, range: Range, severity = MarkerSeverity.Error, tags?: MarkerTag[]): IMarkerData { - return { severity, message, ...(tags ? { tags } : {}), ...range }; +function toMarker(message: string, range: Range, severity = MarkerSeverity.Error, tags?: MarkerTag[], code?: string): IMarkerData { + return { severity, message, ...(tags ? { tags } : {}), ...(code ? { code } : {}), ...range }; } diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptCodeActions.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptCodeActions.test.ts index 2a1a20b946bb7b..493771fbcaf870 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptCodeActions.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptCodeActions.test.ts @@ -14,7 +14,7 @@ import { ITextModel } from '../../../../../../../editor/common/model.js'; import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { ContextKeyService } from '../../../../../../../platform/contextkey/browser/contextKeyService.js'; import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { IMarkerService } from '../../../../../../../platform/markers/common/markers.js'; +import { IMarker, IMarkerService, MarkerSeverity } from '../../../../../../../platform/markers/common/markers.js'; import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; import { ChatConfiguration } from '../../../../common/constants.js'; import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; @@ -24,6 +24,7 @@ import { getLanguageIdForPromptsType, PromptsType } from '../../../../common/pro import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; import { PromptCodeActionProvider } from '../../../../common/promptSyntax/languageProviders/promptCodeActions.js'; +import { PromptValidatorMarkerCode } from '../../../../common/promptSyntax/languageProviders/promptValidator.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; import { CodeActionKind } from '../../../../../../../editor/contrib/codeAction/common/types.js'; @@ -33,6 +34,7 @@ suite('PromptCodeActionProvider', () => { let instaService: TestInstantiationService; let codeActionProvider: PromptCodeActionProvider; let fileService: IFileService; + let markerData: IMarker[] = []; setup(async () => { const testConfigService = new TestConfigurationService(); @@ -60,7 +62,8 @@ suite('PromptCodeActionProvider', () => { }; instaService.set(ILanguageModelToolsService, toolService); - instaService.stub(IMarkerService, { read: () => [] }); + markerData = []; + instaService.stub(IMarkerService, { read: () => markerData }); fileService = { canMove: async (source: URI, target: URI) => { @@ -222,6 +225,57 @@ suite('PromptCodeActionProvider', () => { const actions = await getCodeActions(content, 2, 1, PromptsType.agent); // Range in description, not tools assert.strictEqual(actions.length, 0); }); + + test('offers quick fix to enable built-in github mcp server', async () => { + markerData = [{ + code: { value: PromptValidatorMarkerCode.MissingGithubMcpServer, target: URI.parse('https://marketplace.visualstudio.com/items?itemName=io.github.github/github-mcp-server') }, + owner: 'prompts-diagnostics-provider', + resource: URI.parse('test:///test' + getPromptFileExtension(PromptsType.agent)), + severity: MarkerSeverity.Warning, + message: 'Missing github mcp server', + startLineNumber: 4, + startColumn: 9, + endLineNumber: 4, + endColumn: 19 + }]; + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ['github/*']`, + '---', + ].join('\n'); + const actions = await getCodeActions(content, 4, 11, PromptsType.agent); + assert.deepStrictEqual(actions.map(action => action.title), [ + 'Enable Built-in GitHub MCP Server', + 'Install GitHub MCP Server from Marketplace' + ]); + }); + + test('offers quick fix to install playwright mcp server from marketplace', async () => { + markerData = [{ + code: { value: PromptValidatorMarkerCode.MissingPlaywrightMcpServer, target: URI.parse('https://marketplace.visualstudio.com/items?itemName=microsoft.playwright-mcp') }, + owner: 'prompts-diagnostics-provider', + resource: URI.parse('test:///test' + getPromptFileExtension(PromptsType.agent)), + severity: MarkerSeverity.Warning, + message: 'Missing playwright mcp server', + startLineNumber: 4, + startColumn: 9, + endLineNumber: 4, + endColumn: 21 + }]; + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ['playwrite/*']`, + '---', + ].join('\n'); + const actions = await getCodeActions(content, 4, 11, PromptsType.agent); + assert.deepStrictEqual(actions.map(action => action.title), [ + 'Install Playwright MCP Server from Marketplace' + ]); + }); }); suite('prompt code actions', () => { From 26d36469b04868c3fc89d4bc3529808b9386b97f Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Mon, 29 Jun 2026 10:29:23 -0400 Subject: [PATCH 042/589] Revert "Improve the local to native and remote to local copy, paste, and DND experience (#320685)" (#323490) This reverts commit f9070acd20a269fb07dd15389676bd6875b9db05. --- src/vs/code/electron-main/app.ts | 6 - .../files/common/remoteFileSystemProxy.ts | 18 -- .../remoteFileSystemProxyClient.ts | 134 ------------- .../remoteFileSystemProxyServer.ts | 81 -------- .../remoteFileSystemProxyMainHandler.ts | 83 -------- .../remoteFileSystemProxy.test.ts | 98 ---------- src/vs/workbench/browser/dnd.ts | 10 +- .../contrib/files/browser/explorerService.ts | 80 +------- .../electron-browser/desktop.main.ts | 8 - .../electron-browser/clipboardService.ts | 183 +----------------- 10 files changed, 13 insertions(+), 688 deletions(-) delete mode 100644 src/vs/platform/files/common/remoteFileSystemProxy.ts delete mode 100644 src/vs/platform/files/electron-browser/remoteFileSystemProxyClient.ts delete mode 100644 src/vs/platform/files/electron-browser/remoteFileSystemProxyServer.ts delete mode 100644 src/vs/platform/files/electron-main/remoteFileSystemProxyMainHandler.ts delete mode 100644 src/vs/platform/files/test/electron-main/remoteFileSystemProxy.test.ts diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 684700fc3eec95..64b416e28ae0bc 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -129,8 +129,6 @@ import { PtyHostService } from '../../platform/terminal/node/ptyHostService.js'; import { ElectronAgentHostStarter } from '../../platform/agentHost/electron-main/electronAgentHostStarter.js'; import { AgentHostProcessManager } from '../../platform/agentHost/node/agentHostService.js'; import { NODE_REMOTE_RESOURCE_CHANNEL_NAME, NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, NodeRemoteResourceResponse, NodeRemoteResourceRouter } from '../../platform/remote/common/electronRemoteResources.js'; -import { RemoteFileSystemProxyMainHandler } from '../../platform/files/electron-main/remoteFileSystemProxyMainHandler.js'; -import { REMOTE_FILE_SYSTEM_PROXY_HANDLER_CHANNEL_NAME } from '../../platform/files/common/remoteFileSystemProxy.js'; import { Lazy } from '../../base/common/lazy.js'; import { IAuxiliaryWindowsMainService } from '../../platform/auxiliaryWindow/electron-main/auxiliaryWindows.js'; import { AuxiliaryWindowsMainService } from '../../platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.js'; @@ -1311,10 +1309,6 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel(ipcBrowserViewGroupChannelName, browserViewGroupChannel); sharedProcessClient.then(client => client.registerChannel(ipcBrowserViewGroupChannelName, browserViewGroupChannel)); - // Remote File System Proxy - const remoteFileSystemProxyHandler = disposables.add(new RemoteFileSystemProxyMainHandler(accessor.get(IWindowsMainService), mainProcessElectronServer)); - mainProcessElectronServer.registerChannel(REMOTE_FILE_SYSTEM_PROXY_HANDLER_CHANNEL_NAME, remoteFileSystemProxyHandler); - // Signing const signChannel = ProxyChannel.fromService(accessor.get(ISignService), disposables); mainProcessElectronServer.registerChannel('sign', signChannel); diff --git a/src/vs/platform/files/common/remoteFileSystemProxy.ts b/src/vs/platform/files/common/remoteFileSystemProxy.ts deleted file mode 100644 index 6c6e9a475b6fc4..00000000000000 --- a/src/vs/platform/files/common/remoteFileSystemProxy.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Channel name used by each renderer process to register a server channel - * that exposes its file system operations. The main process routes calls - * from other renderers to the right window. - */ -export const REMOTE_FILE_SYSTEM_PROXY_CHANNEL_NAME = 'remoteFileSystemProxy'; - -/** - * Channel name registered by the main process handler that receives requests - * from renderers and forwards them to the window that owns the remote - * connection matching the URI's authority. - */ -export const REMOTE_FILE_SYSTEM_PROXY_HANDLER_CHANNEL_NAME = 'remoteFileSystemProxyHandler'; diff --git a/src/vs/platform/files/electron-browser/remoteFileSystemProxyClient.ts b/src/vs/platform/files/electron-browser/remoteFileSystemProxyClient.ts deleted file mode 100644 index 07c2236833040d..00000000000000 --- a/src/vs/platform/files/electron-browser/remoteFileSystemProxyClient.ts +++ /dev/null @@ -1,134 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; -import { Schemas } from '../../../base/common/network.js'; -import { URI } from '../../../base/common/uri.js'; -import { IChannel } from '../../../base/parts/ipc/common/ipc.js'; -import { - FileSystemProviderCapabilities, - FileType, - IFileChange, - IFileDeleteOptions, - IFileOverwriteOptions, - IFileService, - IFileSystemProviderWithFileReadWriteCapability, - IFileWriteOptions, - IStat, - IWatchOptions, -} from '../../files/common/files.js'; -import { ILogService } from '../../log/common/log.js'; -import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; -import { REMOTE_FILE_SYSTEM_PROXY_HANDLER_CHANNEL_NAME } from '../common/remoteFileSystemProxy.js'; - -/** - * A read-only file system provider registered in windows that do not have a - * direct remote connection. It proxies file system operations through the main - * process to a window that owns the matching remote file system provider. - * - * This enables copy/paste and drag-and-drop of files between remote and local - * workspaces. - */ -export class RemoteFileSystemProxyClient extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { - - static register( - fileService: IFileService, - mainProcessService: IMainProcessService, - logService: ILogService, - remoteAuthority: string | undefined, - ): IDisposable { - // If this window has its own remote connection, it uses the direct - // RemoteFileSystemProviderClient. Registering the proxy here would - // create a loop (main process routes back to this same window). - if (remoteAuthority) { - return Disposable.None; - } - - const disposables = new DisposableStore(); - - // Listen for activation of the vscode-remote scheme. Since this - // window has no direct remote connection, register the proxy provider - // that routes through the main process to a window that does. - disposables.add(fileService.onWillActivateFileSystemProvider(e => { - if (e.scheme === Schemas.vscodeRemote) { - e.join((async () => { - try { - const provider = new RemoteFileSystemProxyClient(mainProcessService, logService); - disposables.add(provider); - disposables.add(fileService.registerProvider(Schemas.vscodeRemote, provider)); - logService.info('RemoteFileSystemProxyClient: Registered proxy provider for vscode-remote scheme'); - } catch (error) { - logService.error('RemoteFileSystemProxyClient: Failed to register proxy provider', error); - } - })()); - } - })); - - return disposables; - } - - private readonly channel: IChannel; - - readonly onDidChangeCapabilities: Event = Event.None; - - private readonly _onDidChangeFile = this._register(new Emitter()); - readonly onDidChangeFile: Event = this._onDidChangeFile.event; - - get capabilities(): FileSystemProviderCapabilities { - return FileSystemProviderCapabilities.FileReadWrite | - FileSystemProviderCapabilities.Readonly | - FileSystemProviderCapabilities.PathCaseSensitive; - } - - private constructor( - mainProcessService: IMainProcessService, - private readonly logService: ILogService, - ) { - super(); - - // Get the channel from the main process — the main process handler will - // route calls to the appropriate renderer window based on the URI's - // remote authority - this.channel = mainProcessService.getChannel(REMOTE_FILE_SYSTEM_PROXY_HANDLER_CHANNEL_NAME); - } - - async stat(resource: URI): Promise { - this.logService.trace('RemoteFileSystemProxyClient#stat', resource.toString()); - return this.channel.call('stat', [resource]); - } - - async readdir(resource: URI): Promise<[string, FileType][]> { - this.logService.trace('RemoteFileSystemProxyClient#readdir', resource.toString()); - return this.channel.call('readdir', [resource]); - } - - async readFile(resource: URI): Promise { - this.logService.trace('RemoteFileSystemProxyClient#readFile', resource.toString()); - const buffer: VSBuffer = await this.channel.call('readFile', [resource]); - return buffer.buffer; - } - - async writeFile(_resource: URI, _content: Uint8Array, _opts: IFileWriteOptions): Promise { - throw new Error('Remote file system proxy provider is read-only'); - } - - watch(_resource: URI, _opts: IWatchOptions): IDisposable { - return Disposable.None; - } - - async mkdir(_resource: URI): Promise { - throw new Error('Remote file system proxy provider is read-only'); - } - - async delete(_resource: URI, _opts: IFileDeleteOptions): Promise { - throw new Error('Remote file system proxy provider is read-only'); - } - - async rename(_from: URI, _to: URI, _opts: IFileOverwriteOptions): Promise { - throw new Error('Remote file system proxy provider is read-only'); - } -} diff --git a/src/vs/platform/files/electron-browser/remoteFileSystemProxyServer.ts b/src/vs/platform/files/electron-browser/remoteFileSystemProxyServer.ts deleted file mode 100644 index e7e37d6ea4c00c..00000000000000 --- a/src/vs/platform/files/electron-browser/remoteFileSystemProxyServer.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from '../../../base/common/event.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; -import { VSBuffer } from '../../../base/common/buffer.js'; -import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; -import { IFileService, IFileStatWithMetadata, FileType } from '../../files/common/files.js'; -import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; -import { REMOTE_FILE_SYSTEM_PROXY_CHANNEL_NAME } from '../common/remoteFileSystemProxy.js'; - -/** - * Registered in every renderer process. Serves file system operations to other - * windows that cannot directly access this window's remote file system provider. - * The main process routes incoming requests via {@link RemoteFileSystemProxyMainHandler}. - * - * This follows the same pattern as {@link ElectronRemoteResourceLoader}. - */ -export class RemoteFileSystemProxyServer extends Disposable { - - constructor( - @IMainProcessService mainProcessService: IMainProcessService, - @IFileService private readonly fileService: IFileService, - ) { - super(); - - const channel: IServerChannel = { - listen(_: unknown, event: string): Event { - throw new Error(`Event not found: ${event}`); - }, - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - call: (_: unknown, command: string, arg?: any): Promise => { - const args = arg as unknown[]; - switch (command) { - case 'stat': return this.stat(URI.revive(args[0] as UriComponents)); - case 'readdir': return this.readdir(URI.revive(args[0] as UriComponents)); - case 'readFile': return this.readFile(URI.revive(args[0] as UriComponents)); - case 'exists': return this.exists(URI.revive(args[0] as UriComponents)); - case 'resolve': return this.resolve(URI.revive(args[0] as UriComponents), args[1] as boolean); - } - - throw new Error(`Call not found: ${command}`); - } - }; - - mainProcessService.registerChannel(REMOTE_FILE_SYSTEM_PROXY_CHANNEL_NAME, channel); - } - - private async stat(uri: URI): Promise<{ type: FileType; size: number; mtime: number; ctime: number }> { - const provider = this.fileService.getProvider(uri.scheme); - if (!provider) { - throw new Error(`No provider for scheme: ${uri.scheme}`); - } - return provider.stat(uri); - } - - private async readdir(uri: URI): Promise<[string, FileType][]> { - const provider = this.fileService.getProvider(uri.scheme); - if (!provider) { - throw new Error(`No provider for scheme: ${uri.scheme}`); - } - return provider.readdir(uri); - } - - private async readFile(uri: URI): Promise { - const content = await this.fileService.readFile(uri); - return content.value; - } - - private async exists(uri: URI): Promise { - return this.fileService.exists(uri); - } - - private async resolve(uri: URI, resolveMetadata: boolean): Promise { - return this.fileService.resolve(uri, { resolveMetadata }) as Promise; - } -} diff --git a/src/vs/platform/files/electron-main/remoteFileSystemProxyMainHandler.ts b/src/vs/platform/files/electron-main/remoteFileSystemProxyMainHandler.ts deleted file mode 100644 index 0f5918a53ddaf5..00000000000000 --- a/src/vs/platform/files/electron-main/remoteFileSystemProxyMainHandler.ts +++ /dev/null @@ -1,83 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from '../../../base/common/event.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; -import { Schemas } from '../../../base/common/network.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; -import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; -import { REMOTE_FILE_SYSTEM_PROXY_CHANNEL_NAME } from '../common/remoteFileSystemProxy.js'; - -export interface IRemoteFileSystemProxyWindowsService { - getWindows(): readonly { readonly id: number; readonly remoteAuthority?: string }[]; -} - -export interface IRemoteFileSystemProxyIPCServer { - getChannel(channelName: string, clientFilter: (client: { ctx: string }) => boolean): IChannel; -} - -/** - * Main process channel that routes remote file system operations from one - * renderer to another. When a local window needs to read a `vscode-remote://` - * file, this handler finds the renderer window connected to the matching remote - * authority and forwards the request through its - * {@link REMOTE_FILE_SYSTEM_PROXY_CHANNEL_NAME} server channel. - */ -export class RemoteFileSystemProxyMainHandler extends Disposable implements IServerChannel { - - constructor( - private readonly windowsMainService: IRemoteFileSystemProxyWindowsService, - private readonly electronIpcServer: IRemoteFileSystemProxyIPCServer, - ) { - super(); - } - - listen(_: unknown, event: string): Event { - throw new Error(`Event not found: ${event}`); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async call(_: unknown, command: string, arg?: any): Promise { - const args = arg as unknown[]; - const uri = URI.revive(args[0] as UriComponents); - - // Only handle vscode-remote:// URIs to avoid routing unrelated URIs - // that happen to have an authority (e.g. UNC paths on Windows). - if (uri.scheme !== Schemas.vscodeRemote) { - throw new Error(`Unsupported scheme: ${uri.scheme}. Only ${Schemas.vscodeRemote} URIs are supported.`); - } - - // Find a window that has a remote connection matching this URI's authority - const targetWindow = this.findWindowForAuthority(uri.authority); - if (!targetWindow) { - throw new Error(`No window found with remote authority: ${uri.authority}`); - } - - // Get the remote file system proxy channel registered by the target renderer - const targetChannel = this.getRendererChannel(targetWindow.id); - - // Forward the call to the target renderer - return targetChannel.call(command, arg); - } - - private findWindowForAuthority(authority: string): { id: number } | undefined { - const windows = this.windowsMainService.getWindows(); - for (const window of windows) { - if (window.remoteAuthority === authority) { - return { id: window.id }; - } - } - return undefined; - } - - private getRendererChannel(windowId: number): IChannel { - // Get the channel registered by the target renderer window. - // The connection context format is `window:{id}`. - return this.electronIpcServer.getChannel( - REMOTE_FILE_SYSTEM_PROXY_CHANNEL_NAME, - (client) => client.ctx === `window:${windowId}`, - ); - } -} diff --git a/src/vs/platform/files/test/electron-main/remoteFileSystemProxy.test.ts b/src/vs/platform/files/test/electron-main/remoteFileSystemProxy.test.ts deleted file mode 100644 index e1ccfff90cb0c0..00000000000000 --- a/src/vs/platform/files/test/electron-main/remoteFileSystemProxy.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { Event } from '../../../../base/common/event.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { IRemoteFileSystemProxyIPCServer, IRemoteFileSystemProxyWindowsService, RemoteFileSystemProxyMainHandler } from '../../electron-main/remoteFileSystemProxyMainHandler.js'; - -function createMockChannel(callImpl: (command: string, arg?: unknown) => Promise = async () => 'ok'): IChannel { - return { - call: callImpl as IChannel['call'], - listen: (() => Event.None) as IChannel['listen'], - }; -} - -suite('RemoteFileSystemProxyMainHandler', () => { - - const disposables = new DisposableStore(); - - teardown(() => { - disposables.clear(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('throws when no window matches the URI authority', async () => { - const windowsService: IRemoteFileSystemProxyWindowsService = { - getWindows: () => [ - { id: 1 }, - { id: 2, remoteAuthority: 'ssh-remote+myhost' }, - { id: 3, remoteAuthority: 'wsl+ubuntu' }, - ], - }; - - const mockServer: IRemoteFileSystemProxyIPCServer = { - getChannel: () => createMockChannel(), - }; - - const handler = disposables.add(new RemoteFileSystemProxyMainHandler(windowsService, mockServer)); - - await assert.rejects( - () => handler.call(undefined, 'stat', [URI.parse('vscode-remote://unknown-host/test')]), - /No window found with remote authority/ - ); - }); - - test('throws for non-vscode-remote URIs', async () => { - const windowsService: IRemoteFileSystemProxyWindowsService = { - getWindows: () => [ - { id: 1, remoteAuthority: 'ssh-remote+myhost' }, - ], - }; - - const mockServer: IRemoteFileSystemProxyIPCServer = { - getChannel: () => createMockChannel(), - }; - - const handler = disposables.add(new RemoteFileSystemProxyMainHandler(windowsService, mockServer)); - - await assert.rejects( - () => handler.call(undefined, 'stat', [URI.parse('file:///local/path')]), - /Unsupported scheme/ - ); - }); - - test('routes call to correct window based on URI authority', async () => { - let calledWindowCtx: string | undefined; - - const windowsService: IRemoteFileSystemProxyWindowsService = { - getWindows: () => [ - { id: 1 }, - { id: 2, remoteAuthority: 'ssh-remote+myhost' }, - ], - }; - - const mockServer: IRemoteFileSystemProxyIPCServer = { - getChannel: (_name: string, filter: (client: { ctx: string }) => boolean) => { - const connections = [ - { ctx: 'window:1' }, - { ctx: 'window:2' }, - ]; - calledWindowCtx = connections.find(filter)?.ctx; - return createMockChannel(); - }, - }; - - const handler = disposables.add(new RemoteFileSystemProxyMainHandler(windowsService, mockServer)); - - await handler.call(undefined, 'stat', [URI.parse('vscode-remote://ssh-remote+myhost/some/path')]); - - assert.strictEqual(calledWindowCtx, 'window:2'); - }); -}); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index c8a537999f0cd0..89fa20a1ebe733 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -238,14 +238,8 @@ export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEdito if (!options?.disableStandardTransfer) { // Text: allows to paste into text-capable areas - // Only include file:// URIs — remote URIs are not meaningful to - // native apps and macOS would create .webloc URL bookmark files - // when these are dragged to Finder. - const nativeResources = fileSystemResources.filter(({ resource }) => resource.scheme === Schemas.file); - if (nativeResources.length) { - const lineDelimiter = isWindows ? '\r\n' : '\n'; - event.dataTransfer.setData(DataTransfers.TEXT, nativeResources.map(({ resource }) => labelService.getUriLabel(resource, { noPrefix: true })).join(lineDelimiter)); - } + const lineDelimiter = isWindows ? '\r\n' : '\n'; + event.dataTransfer.setData(DataTransfers.TEXT, fileSystemResources.map(({ resource }) => labelService.getUriLabel(resource, { noPrefix: true })).join(lineDelimiter)); // Download URL: enables support to drag a tab as file to desktop // Requirements: diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index d1a69738fc1564..90e83d0d1ca412 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -10,7 +10,7 @@ import { IFilesConfiguration, ISortOrderConfiguration, SortOrder, LexicographicO import { ExplorerItem, ExplorerModel } from '../common/explorerModel.js'; import { URI } from '../../../../base/common/uri.js'; import { FileOperationEvent, FileOperation, IFileService, FileChangesEvent, FileChangeType, IResolveFileOptions } from '../../../../platform/files/common/files.js'; -import { dirname, basename, joinPath } from '../../../../base/common/resources.js'; +import { dirname, basename } from '../../../../base/common/resources.js'; import { IConfigurationService, IConfigurationChangeEvent } from '../../../../platform/configuration/common/configuration.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -26,10 +26,6 @@ import { IHostService } from '../../../services/host/browser/host.js'; import { IExpression } from '../../../../base/common/glob.js'; import { ResourceGlobMatcher } from '../../../common/resources.js'; import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; export const UNDO_REDO_SOURCE = new UndoRedoSource(); @@ -47,7 +43,6 @@ export class ExplorerService implements IExplorerService { private onFileChangesScheduler: RunOnceScheduler; private fileChangeEvents: FileChangesEvent[] = []; private revealExcludeMatcher: ResourceGlobMatcher; - private remoteClipboardTempDir: URI | undefined; constructor( @IFileService private fileService: IFileService, @@ -59,9 +54,7 @@ export class ExplorerService implements IExplorerService { @IBulkEditService private readonly bulkEditService: IBulkEditService, @IProgressService private readonly progressService: IProgressService, @IHostService hostService: IHostService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @ILogService private readonly logService: ILogService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { this.config = this.configurationService.getValue('explorer'); @@ -266,66 +259,11 @@ export class ExplorerService implements IExplorerService { async setToCopy(items: ExplorerItem[], cut: boolean): Promise { const previouslyCutItems = this.cutItems; this.cutItems = cut ? items : undefined; - - const resources = items.map(s => s.resource); - - // For remote resources, download to a temp location so they can be - // pasted both in other VS Code windows and into native file managers. - // We replace remote URIs with local file:// URIs pointing to temp copies. - const clipboardResources = await this.resolveClipboardResources(resources); - - await this.clipboardService.writeResources(clipboardResources); + await this.clipboardService.writeResources(items.map(s => s.resource)); this.view?.itemsCopied(items, cut, previouslyCutItems); } - /** - * Returns `file://` URIs for all resources. Local files pass through - * unchanged. Remote files are downloaded to a temp directory and their - * temp `file://` URIs are returned instead. - */ - private async resolveClipboardResources(resources: URI[]): Promise { - const result: URI[] = []; - const remoteResources = resources.filter(r => r.scheme !== Schemas.file); - - // Local files: use as-is - for (const resource of resources) { - if (resource.scheme === Schemas.file) { - result.push(resource); - } - } - - // Remote files: download to temp - if (remoteResources.length > 0) { - try { - // Clean up previous temp dir before creating a new one - await this.cleanupRemoteClipboardTempDir(); - - const tempDir = joinPath(this.environmentService.cacheHome, 'remote-clipboard', generateUuid()); - await this.fileService.createFolder(tempDir); - this.remoteClipboardTempDir = tempDir; - - for (const resource of remoteResources) { - // Place each file in its own unique subfolder to avoid - // name collisions (e.g. two different folders both have index.ts) - const uniqueDir = joinPath(tempDir, generateUuid()); - await this.fileService.createFolder(uniqueDir); - const target = joinPath(uniqueDir, basename(resource)); - await this.fileService.copy(resource, target, true); - result.push(target); - } - } catch (error) { - // If download fails, fall back to the original remote URIs. - // VS Code cross-window paste will still work via the proxy - // provider, but native paste will not. - this.logService.warn('Failed to download remote files for clipboard', error); - result.push(...remoteResources); - } - } - - return result; - } - isCut(item: ExplorerItem): boolean { return !!this.cutItems && this.cutItems.some(i => this.uriIdentityService.extUri.isEqual(i.resource, item.resource)); } @@ -565,20 +503,8 @@ export class ExplorerService implements IExplorerService { } dispose(): void { - this.cleanupRemoteClipboardTempDir(); this.disposables.dispose(); } - - private async cleanupRemoteClipboardTempDir(): Promise { - if (this.remoteClipboardTempDir) { - try { - await this.fileService.del(this.remoteClipboardTempDir, { recursive: true }); - } catch { - // Best-effort cleanup - } - this.remoteClipboardTempDir = undefined; - } - } } function doesFileEventAffect(item: ExplorerItem, view: IExplorerView, events: FileChangesEvent[], types: FileChangeType[]): boolean { diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 00f3ee08665271..03ed0459d0e98a 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -61,8 +61,6 @@ import { IUserDataProfileService } from '../services/userDataProfile/common/user import { BrowserSocketFactory } from '../../platform/remote/browser/browserSocketFactory.js'; import { RemoteSocketFactoryService, IRemoteSocketFactoryService } from '../../platform/remote/common/remoteSocketFactoryService.js'; import { ElectronRemoteResourceLoader } from '../../platform/remote/electron-browser/electronRemoteResourceLoader.js'; -import { RemoteFileSystemProxyServer } from '../../platform/files/electron-browser/remoteFileSystemProxyServer.js'; -import { RemoteFileSystemProxyClient } from '../../platform/files/electron-browser/remoteFileSystemProxyClient.js'; import { IConfigurationService } from '../../platform/configuration/common/configuration.js'; import { applyZoom } from '../../platform/window/electron-browser/window.js'; import { mainWindow } from '../../base/browser/window.js'; @@ -292,12 +290,6 @@ export class DesktopMain extends Disposable { // Remote Files this._register(RemoteFileSystemProviderClient.register(remoteAgentService, fileService, logService)); - // Remote File System Proxy - // Server: allows other windows to read remote files from this window's providers - this._register(new RemoteFileSystemProxyServer(mainProcessService, fileService)); - // Client: allows this window to read remote files from other windows' providers - this._register(RemoteFileSystemProxyClient.register(fileService, mainProcessService, logService, environmentService.remoteAuthority)); - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // // NOTE: Please do NOT register services here. Use `registerSingleton()` diff --git a/src/vs/workbench/services/clipboard/electron-browser/clipboardService.ts b/src/vs/workbench/services/clipboard/electron-browser/clipboardService.ts index fbdf70cd759cc6..98cca5604bba3e 100644 --- a/src/vs/workbench/services/clipboard/electron-browser/clipboardService.ts +++ b/src/vs/workbench/services/clipboard/electron-browser/clipboardService.ts @@ -5,8 +5,7 @@ import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { URI } from '../../../../base/common/uri.js'; -import { isMacintosh, isLinux, isWindows } from '../../../../base/common/platform.js'; -import { Schemas } from '../../../../base/common/network.js'; +import { isMacintosh } from '../../../../base/common/platform.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; @@ -15,9 +14,6 @@ import { ILogService } from '../../../../platform/log/common/log.js'; export class NativeClipboardService implements IClipboardService { private static readonly FILE_FORMAT = 'code/file-list'; // Clipboard format for files - private static readonly MAC_FILE_FORMAT = 'NSFilenamesPboardType'; // macOS Finder clipboard format - private static readonly LINUX_FILE_FORMAT = 'text/uri-list'; // freedesktop.org clipboard format for file managers - private static readonly WINDOWS_FILE_FORMAT = 'FileNameW'; // Windows Explorer clipboard format (single file, UTF-16LE) declare readonly _serviceBrand: undefined; @@ -60,94 +56,21 @@ export class NativeClipboardService implements IClipboardService { } async writeResources(resources: URI[]): Promise { - if (!resources.length) { - return; + if (resources.length) { + return this.nativeHostService.writeClipboardBuffer(NativeClipboardService.FILE_FORMAT, this.resourcesToBuffer(resources)); } - - const allLocal = resources.every(r => r.scheme === Schemas.file); - - // When all resources are local files, write in the platform's native - // file clipboard format so that native file managers can paste them. - // Electron's clipboard API only supports one buffer format per call, - // so we pick the most useful one. - if (allLocal) { - if (isMacintosh) { - const plist = this.filesToPlist(resources.map(r => r.fsPath)); - return this.nativeHostService.writeClipboardBuffer( - NativeClipboardService.MAC_FILE_FORMAT, - VSBuffer.fromString(plist) - ); - } - if (isLinux) { - const uriList = resources.map(r => r.toString()).join('\r\n'); - return this.nativeHostService.writeClipboardBuffer( - NativeClipboardService.LINUX_FILE_FORMAT, - VSBuffer.fromString(uriList) - ); - } - if (isWindows && resources.length === 1) { - // FileNameW supports a single null-terminated UTF-16LE file path. - // For multiple files CF_HDROP would be needed, which requires - // a predefined format ID that Electron cannot write. - return this.nativeHostService.writeClipboardBuffer( - NativeClipboardService.WINDOWS_FILE_FORMAT, - this.filePathToUtf16LE(resources[0].fsPath) - ); - } - } - - // Default (Windows, or mixed local/remote): write VS Code custom format - return this.nativeHostService.writeClipboardBuffer( - NativeClipboardService.FILE_FORMAT, - VSBuffer.fromString(resources.map(r => r.toString()).join('\n')) - ); } async readResources(): Promise { - // Try VS Code's custom format first - const codeBuffer = await this.nativeHostService.readClipboardBuffer(NativeClipboardService.FILE_FORMAT); - const codeResources = this.bufferToResources(codeBuffer); - if (codeResources.length > 0) { - return codeResources; - } - - // Fall back to reading platform-native file clipboard formats - if (isMacintosh) { - const macBuffer = await this.nativeHostService.readClipboardBuffer(NativeClipboardService.MAC_FILE_FORMAT); - return this.plistToFiles(macBuffer); - } - - if (isLinux) { - const linuxBuffer = await this.nativeHostService.readClipboardBuffer(NativeClipboardService.LINUX_FILE_FORMAT); - return this.uriListToFiles(linuxBuffer); - } - - if (isWindows) { - const winBuffer = await this.nativeHostService.readClipboardBuffer(NativeClipboardService.WINDOWS_FILE_FORMAT); - return this.fileNameWToFile(winBuffer); - } - - return []; + return this.bufferToResources(await this.nativeHostService.readClipboardBuffer(NativeClipboardService.FILE_FORMAT)); } async hasResources(): Promise { - if (await this.nativeHostService.hasClipboard(NativeClipboardService.FILE_FORMAT)) { - return true; - } - - if (isMacintosh) { - return this.nativeHostService.hasClipboard(NativeClipboardService.MAC_FILE_FORMAT); - } - - if (isLinux) { - return this.nativeHostService.hasClipboard(NativeClipboardService.LINUX_FILE_FORMAT); - } - - if (isWindows) { - return this.nativeHostService.hasClipboard(NativeClipboardService.WINDOWS_FILE_FORMAT); - } + return this.nativeHostService.hasClipboard(NativeClipboardService.FILE_FORMAT); + } - return false; + private resourcesToBuffer(resources: URI[]): VSBuffer { + return VSBuffer.fromString(resources.map(r => r.toString()).join('\n')); } private bufferToResources(buffer: VSBuffer): URI[] { @@ -166,96 +89,6 @@ export class NativeClipboardService implements IClipboardService { return []; // do not trust clipboard data } } - - private filesToPlist(paths: string[]): string { - return '\n' - + '\n' - + '\n\n' - + paths.map(p => `${p.replace(/&/g, '&').replace(//g, '>')}`).join('\n') - + '\n\n'; - } - - private plistToFiles(buffer: VSBuffer): URI[] { - if (!buffer) { - return []; - } - - const content = buffer.toString(); - if (!content) { - return []; - } - - try { - // Extract ... values from the plist - const paths: URI[] = []; - const regex = /([^<]+)<\/string>/g; - let match: RegExpExecArray | null; - while ((match = regex.exec(content)) !== null) { - const path = match[1] - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&'); - paths.push(URI.file(path)); - } - return paths; - } catch (error) { - return []; // do not trust clipboard data - } - } - - private uriListToFiles(buffer: VSBuffer): URI[] { - if (!buffer) { - return []; - } - - const content = buffer.toString(); - if (!content) { - return []; - } - - try { - // text/uri-list: lines starting with # are comments, entries separated by \r\n. - // Only include file:// URIs — non-file URIs are not meaningful for - // file paste flows and could cause confusing errors. - return content.split(/\r?\n/) - .filter(line => line.length > 0 && !line.startsWith('#')) - .map(line => URI.parse(line)) - .filter(uri => uri.scheme === Schemas.file); - } catch (error) { - return []; // do not trust clipboard data - } - } - - private filePathToUtf16LE(path: string): VSBuffer { - // FileNameW expects a null-terminated UTF-16LE encoded string. - // Uint16Array naturally uses the platform's char encoding (UTF-16). - const encoded = new Uint16Array(path.length + 1); - for (let i = 0; i < path.length; i++) { - encoded[i] = path.charCodeAt(i); - } - // Last element is already 0 (null terminator) - return VSBuffer.wrap(new Uint8Array(encoded.buffer)); - } - - private fileNameWToFile(buffer: VSBuffer): URI[] { - if (!buffer || buffer.byteLength < 4) { - return []; - } - - try { - // Read null-terminated UTF-16LE string - const u16 = new Uint16Array(buffer.buffer.buffer, buffer.buffer.byteOffset, Math.floor(buffer.byteLength / 2)); - const nullIdx = u16.indexOf(0); - const path = String.fromCharCode(...u16.subarray(0, nullIdx === -1 ? u16.length : nullIdx)); - if (path.length > 0) { - return [URI.file(path)]; - } - } catch (error) { - // do not trust clipboard data - } - - return []; - } } registerSingleton(IClipboardService, NativeClipboardService, InstantiationType.Delayed); From 698183c1491d7cb55c7125f4facd7c185791fa28 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:39:44 +0200 Subject: [PATCH 043/589] Always show last-updated time in sessions list (#323499) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/sessions/browser/views/sessionsList.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index 708d4b2da3c6ab..a044c245fd72b1 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -269,7 +269,7 @@ class SessionItemRenderer implements ITreeRenderer = this._onDidChangeItemHeight.event; constructor( - private readonly options: { grouping: () => SessionsGrouping; sorting: () => SessionsSorting; isPinned: (session: ISession) => boolean; isRead: (session: ISession) => boolean; visibleSessions: IObservable; getMultiSelectedSessions: (session: ISession) => ISession[] }, + private readonly options: { grouping: () => SessionsGrouping; isPinned: (session: ISession) => boolean; isRead: (session: ISession) => boolean; visibleSessions: IObservable; getMultiSelectedSessions: (session: ISession) => ISession[] }, private readonly approvalModel: AgentSessionApprovalModel | undefined, private readonly instantiationService: IInstantiationService, private readonly contextKeyService: IContextKeyService, @@ -416,7 +416,7 @@ class SessionItemRenderer implements ITreeRenderer accessor.get(IAgentSessionsService)); const sessionRenderer = new SessionItemRenderer( - { grouping: this.options.grouping, sorting: this.options.sorting, isPinned: s => this.isSessionPinned(s), isRead: s => this.isSessionRead(s), visibleSessions: this._sessionsService.visibleSessions, getMultiSelectedSessions: s => this.getMultiSelectedSessions(s) }, + { grouping: this.options.grouping, isPinned: s => this.isSessionPinned(s), isRead: s => this.isSessionRead(s), visibleSessions: this._sessionsService.visibleSessions, getMultiSelectedSessions: s => this.getMultiSelectedSessions(s) }, approvalModel, instantiationService, contextKeyService, From 6804af066860e47590ca8e8ecd62b6d60decea2f Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:48:54 +0000 Subject: [PATCH 044/589] Update endgame notebook milestones to 1.127.0 (#323407) * Update endgame notebook milestones to 1.127.0 * signing commit * signing commit * signing commit * signing commit --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Giuseppe Cianci Co-authored-by: Giuseppe Cianci <39117631+Giuspepe@users.noreply.github.com> --- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 0b26098f8edaf6..b2c13bf7d13802 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.126.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified" + "value": "$MILESTONE=milestone:\"1.127.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index d7a3f66d69a061..201378fdc8eb7c 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.126.0\"\n\n$MINE=assignee:@me" + "value": "$MILESTONE=milestone:\"1.127.0\"\n\n$MINE=assignee:@me" }, { "kind": 2, From 051422f15fd26646c3c47cd1654debc413f5d660 Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci Date: Mon, 29 Jun 2026 16:50:50 +0200 Subject: [PATCH 045/589] signing commit From 773dcd76f533ed235d2cc60b442ab135b0c2d90a Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:55:19 +0200 Subject: [PATCH 046/589] sessions: fix PR hover padding and horizontal scrollbar (#323504) Adjust the description and branch row padding in the PR metadata hover and stop the hover from overflowing its container. The hover element was fixed at width 520px while the hover widget caps its contents at max-width ~500px, causing a horizontal scrollbar. Change max-width to 100% so it fits the available space, and add overflow-wrap to the title/description to guard against long unbreakable words. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/github/browser/media/pullRequestHover.css | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/sessions/contrib/github/browser/media/pullRequestHover.css b/src/vs/sessions/contrib/github/browser/media/pullRequestHover.css index 7fe3d3146f1e2b..00cc9d73f4ca80 100644 --- a/src/vs/sessions/contrib/github/browser/media/pullRequestHover.css +++ b/src/vs/sessions/contrib/github/browser/media/pullRequestHover.css @@ -8,7 +8,7 @@ display: flex; flex-direction: column; width: 520px; - max-width: calc(100vw - var(--vscode-spacing-size320)); + max-width: 100%; color: var(--vscode-editorHoverWidget-foreground); } @@ -39,6 +39,7 @@ font-size: var(--vscode-agents-fontSize-heading2, 18px); font-weight: var(--vscode-agents-fontWeight-semiBold, 600); line-height: 1.25; + overflow-wrap: anywhere; } .sessions-pr-hover-description { @@ -47,11 +48,12 @@ -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; - padding: var(--vscode-spacing-size120) var(--vscode-spacing-size160); + padding: var(--vscode-spacing-size120) var(--vscode-spacing-size160) 0 var(--vscode-spacing-size160); border-top: var(--vscode-strokeThickness) solid var(--vscode-editorHoverWidget-border); font-size: var(--vscode-agents-fontSize-body1); line-height: 1.4; color: var(--vscode-descriptionForeground); + overflow-wrap: anywhere; } .sessions-pr-hover-branches { @@ -59,7 +61,7 @@ align-items: center; gap: var(--vscode-spacing-size80); min-width: 0; - padding: 0 var(--vscode-spacing-size160) var(--vscode-spacing-size120); + padding: var(--vscode-spacing-size120) var(--vscode-spacing-size160); } .sessions-pr-hover-branch { From 0605fd85e1858f699ba716d16648fc6f8af094b2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:03:09 +0000 Subject: [PATCH 047/589] AgentHost - restore changesets when reloading the window (#323494) * AgentHost - restore changesets when reloading the window * Tweak the fix so that we only use the state information to initialize the changesets --- .../browser/baseAgentHostSessionsProvider.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts index 2df8b48604675f..31bf9d91acfa3b 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -2987,6 +2987,39 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement this._seedRunningConfigFromState(sessionId, state); this._applySessionMetaFromState(sessionId, state); this._applyChatCatalogFromState(sessionId, state); + + if (!previous) { + // This is the first time we've seen this session and the initial + // list of changesets are included in the state, so we use that to + // initialize the changeset catalogue.v Subsequent updates will be + // handled by handling the ActionType.SessionChangesetsChanged + // action. + this._applyChangesetsFromState(sessionId, state); + } + } + + /** + * Seed the cached adapter's changeset catalogue from an AHP + * {@link SessionState}. The catalogue otherwise only flows in via the live + * `SessionChangesetsChanged` action, which the host emits only when entries + * are added or removed. On restore (e.g. after a reload) nothing mutates, so + * that action never fires and the catalogue would stay empty. The restored + * `SessionState` snapshot carries the persisted `changesets`, so apply it + * here to surface the catalogue immediately. + */ + private _applyChangesetsFromState(sessionId: string, state: SessionState): void { + if (state.changesets === undefined) { + return; + } + const rawId = this._rawIdFromChatId(sessionId); + if (!rawId) { + return; + } + const cached = this._sessionCache.get(rawId); + if (!cached) { + return; + } + cached.updateChangesets(state.changesets); } /** From d720ce91b889d93556e8332ace2081fa23a73c41 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Mon, 29 Jun 2026 16:27:44 +0100 Subject: [PATCH 048/589] Style Overrides: Add margin adjustments for right-aligned activity bar in floating panels (#323442) * Add margin adjustments for right-aligned activity bar in floating panels Co-authored-by: Copilot * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: mrleemurray Co-authored-by: Copilot Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/vs/workbench/browser/media/floatingPanels.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/browser/media/floatingPanels.css b/src/vs/workbench/browser/media/floatingPanels.css index 32a00d5b442f73..85d618ab243a21 100644 --- a/src/vs/workbench/browser/media/floatingPanels.css +++ b/src/vs/workbench/browser/media/floatingPanels.css @@ -108,6 +108,12 @@ margin-bottom: var(--vscode-spacing-size40); } +/* When the activity bar is on the right, mirror the gutter so the bar stays centered. */ +.monaco-workbench.floating-panels.activitybar-right .part.activitybar { + margin-left: 0; + margin-right: var(--vscode-spacing-size40); +} + /* The status bar draws its top border via a pseudo-element — hide it too. */ .monaco-workbench.floating-panels .part.statusbar::after { display: none !important; From 45eb9152155d0aabff764cf80f76a78c36f9187d Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 30 Jun 2026 01:28:10 +1000 Subject: [PATCH 049/589] Enhance agent selection resolution during session materialization (#323495) * feat: enhance agent selection resolution during session materialization * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../agentHost/node/copilot/copilotAgent.ts | 47 +++++- .../agentHost/test/node/copilotAgent.test.ts | 155 +++++++++++++++++- 2 files changed, 197 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 2b4c8b2605bd7c..c3b71083852dd1 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -1300,14 +1300,16 @@ export class CopilotAgent extends Disposable implements IAgent { const shellManager = this._instantiationService.createInstance(ShellManager, sessionUri, workingDirectory); let agentSession: CopilotAgentSession | undefined; + let agent: AgentSelection | undefined; try { - const resolvedAgentName = provisional.agent ? await this._resolveAgentName(provisional.sessionUri, snapshot, provisional.agent) : undefined; + const resolvedAgent = await this._resolveAgentWhenMaterializing(provisional, snapshot, workingDirectory); + agent = resolvedAgent?.agent; const launchPlan: CopilotSessionLaunchPlan = { kind: 'create', client, sessionId, workingDirectory, - resolvedAgentName, + resolvedAgentName: resolvedAgent?.name, snapshot, activeClientToolSet: activeClient.toolSet, shellManager, @@ -1329,8 +1331,8 @@ export class CopilotAgent extends Disposable implements IAgent { this._provisionalSessions.delete(sessionId); await this._storeSessionMetadata(sessionUri, provisional.model, workingDirectory, customizationDirectory, project, true); - if (provisional.agent !== undefined) { - await this._storeSessionAgentMetadata(sessionUri, provisional.agent); + if (agent !== undefined) { + await this._storeSessionAgentMetadata(sessionUri, agent); } // Capture the per-session baseline (turn/0) git checkpoint so @@ -1348,6 +1350,43 @@ export class CopilotAgent extends Disposable implements IAgent { return agentSession; } + private async _resolveAgentWhenMaterializing(provisional: IProvisionalSession, snapshot: IActiveClientSnapshot, workingDirectory: URI | undefined): Promise<{ agent: AgentSelection; name: string } | undefined> { + const agent = provisional.agent; + if (!agent) { + return undefined; + } + const alternativeAgent = this._getAlternativeAgentForWorktree(provisional, workingDirectory); + + const [originalAgentName, alternativeAgentName] = await Promise.all([ + this._resolveAgentName(provisional.sessionUri, snapshot, agent), + alternativeAgent ? this._resolveAgentName(provisional.sessionUri, snapshot, alternativeAgent) : Promise.resolve(undefined), + ]); + + if (originalAgentName) { + return { agent: agent, name: originalAgentName }; + } + if (alternativeAgentName && alternativeAgent) { + this._logService.info(`[Copilot] Agent file ${agent.uri} is in the original repo; using worktree agent ${alternativeAgent?.uri}`); + return { agent: alternativeAgent, name: alternativeAgentName }; + } + return undefined; + } + private _getAlternativeAgentForWorktree(provisional: IProvisionalSession, workingDirectory: URI | undefined): AgentSelection | undefined { + const agent = provisional.agent; + if (!agent) { + return undefined; + } + if (!provisional.workingDirectory || !workingDirectory) { + return undefined; + } + if (isEqual(provisional.workingDirectory, workingDirectory)) { + return undefined; + } + const agentUri = URI.parse(agent.uri); + const alternativeAgentUri = rebaseUnder(agentUri, provisional.workingDirectory, workingDirectory); + return alternativeAgentUri ? { uri: alternativeAgentUri.toString() } : undefined; + } + async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise { const gitInfo = params.workingDirectory ? await this._getGitInfo(params.workingDirectory) : undefined; diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index 1e8ae1925bcc22..2d66709cc29d57 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -33,7 +33,7 @@ import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPlu import { AgentSession, GITHUB_COPILOT_PROTECTED_RESOURCE, type AgentSignal, type IAgentActionSignal, type IAgentSessionMetadata } from '../../common/agentService.js'; import { ISessionDataService } from '../../common/sessionDataService.js'; import { buildDefaultChatUri, buildSubagentSessionUri, buildChatUri, CustomizationLoadStatus, MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, TurnState, customizationId, type ClientPluginCustomization, type MarkdownResponsePart, type PluginCustomization, type ToolCallResult, type Turn, RuleCustomization } from '../../common/state/sessionState.js'; -import { CustomizationType, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js'; +import { CustomizationType, type AgentSelection, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js'; import { ActionType, type ChatAction, type IDeltaAction, type SessionAction } from '../../common/state/sessionActions.js'; import { AgentConfigurationService, IAgentConfigurationService } from '../../node/agentConfigurationService.js'; @@ -3207,4 +3207,157 @@ suite('CopilotAgent', () => { }); }); + + suite('custom agent worktree translation', () => { + + // The new methods under test are private; reach in the same way the + // surrounding suites do (e.g. `customization anchoring`). + type AgentInternals = { + _getAlternativeAgentForWorktree(provisional: unknown, workingDirectory: URI | undefined): AgentSelection | undefined; + _resolveAgentWhenMaterializing(provisional: unknown, snapshot: IActiveClientSnapshot, workingDirectory: URI | undefined): Promise<{ agent: AgentSelection; name: string } | undefined>; + _resolveAgentName(sessionUri: URI, snapshot: IActiveClientSnapshot, agent: AgentSelection): Promise; + _resolveSessionWorkingDirectory(config: unknown, sessionId: string, prompt?: string): Promise; + _createAgentSession(launchPlan: CopilotSessionLaunchPlan, customizationDirectory: URI | undefined, activeClient: unknown, channelUri?: URI): CopilotAgentSession; + _readSessionMetadata(session: URI): Promise<{ agent?: AgentSelection }>; + }; + + const repo = URI.file('/repo'); + const worktree = URI.joinPath(URI.file('/repo.worktrees'), 'agents-x'); + const repoAgentUri = URI.joinPath(repo, '.github', 'agents', 'agent.md').toString(); + const worktreeAgentUri = URI.joinPath(worktree, '.github', 'agents', 'agent.md').toString(); + const emptySnapshot: IActiveClientSnapshot = { tools: [], plugins: [], mcpServers: {} }; + + function provisional(workingDirectory: URI | undefined, agent: AgentSelection | undefined): unknown { + const sessionUri = AgentSession.uri('copilotcli', 'prov-agent'); + return { sessionId: AgentSession.id(sessionUri), sessionUri, workingDirectory, model: undefined, agent, project: undefined }; + } + + test('_getAlternativeAgentForWorktree rewrites a repo agent path onto the worktree', async () => { + const agent = createTestAgent(disposables, { copilotClient: new TestCopilotClient([]) }); + try { + const internals = agent as unknown as AgentInternals; + assert.deepStrictEqual( + internals._getAlternativeAgentForWorktree(provisional(repo, { uri: repoAgentUri }), worktree), + { uri: worktreeAgentUri }, + ); + } finally { + await disposeAgent(agent); + } + }); + + test('_getAlternativeAgentForWorktree returns undefined when there is nothing to translate', async () => { + const agent = createTestAgent(disposables, { copilotClient: new TestCopilotClient([]) }); + try { + const internals = agent as unknown as AgentInternals; + const outsideRepoAgent: AgentSelection = { uri: URI.file('/home/me/.copilot/agents/agent.md').toString() }; + assert.deepStrictEqual( + { + noAgent: internals._getAlternativeAgentForWorktree(provisional(repo, undefined), worktree), + folderIsolation: internals._getAlternativeAgentForWorktree(provisional(repo, { uri: repoAgentUri }), undefined), + sameWorkingDirectory: internals._getAlternativeAgentForWorktree(provisional(repo, { uri: repoAgentUri }), repo), + agentOutsideRepo: internals._getAlternativeAgentForWorktree(provisional(repo, outsideRepoAgent), worktree), + }, + { + noAgent: undefined, + folderIsolation: undefined, + sameWorkingDirectory: undefined, + agentOutsideRepo: undefined, + }, + ); + } finally { + await disposeAgent(agent); + } + }); + + test('_resolveAgentWhenMaterializing keeps the original agent for folder isolation (no worktree)', async () => { + const agent = createTestAgent(disposables, { copilotClient: new TestCopilotClient([]) }); + try { + const internals = agent as unknown as AgentInternals; + internals._resolveAgentName = async (_sessionUri, _snapshot, selection) => selection.uri === repoAgentUri ? 'Repo Agent' : undefined; + // Folder isolation: the resolved working directory equals the + // user-picked folder, so there is no worktree copy to translate to + // and the originally selected agent is kept as-is. + assert.deepStrictEqual( + await internals._resolveAgentWhenMaterializing(provisional(repo, { uri: repoAgentUri }), emptySnapshot, repo), + { agent: { uri: repoAgentUri }, name: 'Repo Agent' }, + ); + } finally { + await disposeAgent(agent); + } + }); + + test('_resolveAgentWhenMaterializing returns undefined when no agent is selected or neither resolves', async () => { + const agent = createTestAgent(disposables, { copilotClient: new TestCopilotClient([]) }); + try { + const internals = agent as unknown as AgentInternals; + internals._resolveAgentName = async () => undefined; + assert.deepStrictEqual( + { + noAgent: await internals._resolveAgentWhenMaterializing(provisional(repo, undefined), emptySnapshot, worktree), + neitherResolves: await internals._resolveAgentWhenMaterializing(provisional(repo, { uri: repoAgentUri }), emptySnapshot, worktree), + }, + { noAgent: undefined, neitherResolves: undefined }, + ); + } finally { + await disposeAgent(agent); + } + }); + + test('materialization rewrites a repo agent to its worktree copy and persists it (no resolution stubbing)', async () => { + // End-to-end through real customization discovery: the same custom + // agent file exists in both the original repo and the worktree. The + // user selects the repo copy, but once the worktree is materialized + // discovery re-anchors there, so the persisted/launched agent must be + // the worktree copy — proving the translation against real resolution + // rather than a stubbed `_resolveAgentName`. + const fileService = disposables.add(new FileService(new NullLogService())); + disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider()))); + + const repoFolder = URI.from({ scheme: Schemas.inMemory, path: '/repo' }); + const worktreeFolder = URI.from({ scheme: Schemas.inMemory, path: '/repo.worktrees/agents-x' }); + const repoAgentFile = URI.joinPath(repoFolder, '.github', 'agents', 'agent.md'); + const worktreeAgentFile = URI.joinPath(worktreeFolder, '.github', 'agents', 'agent.md'); + const agentContents = VSBuffer.fromString('---\nname: My Agent\ndescription: a custom agent\n---\nbody'); + await fileService.writeFile(repoAgentFile, agentContents); + await fileService.writeFile(worktreeAgentFile, agentContents); + + const client = new TestCopilotClient([]); + client.createSession = async () => new MockCopilotSession() as unknown as CopilotSession; + + const sessionDataService = disposables.add(new TestSessionDataService()); + const { agent } = createTestAgentContext(disposables, { sessionDataService, copilotClient: client, fileService }); + + let launchAgentName: string | undefined; + const internals = agent as unknown as AgentInternals; + internals._resolveSessionWorkingDirectory = async () => worktreeFolder; + const originalCreateAgentSession = internals._createAgentSession; + internals._createAgentSession = (launchPlan, customizationDirectory, activeClient, channelUri) => { + launchAgentName = launchPlan.resolvedAgentName; + return originalCreateAgentSession.call(agent, launchPlan, customizationDirectory, activeClient, channelUri); + }; + + try { + await agent.authenticate('https://api.github.com', 'token'); + const result = await agent.createSession({ + session: AgentSession.uri('copilotcli', 'agent-translate'), + workingDirectory: repoFolder, + agent: { uri: repoAgentFile.toString() }, + }); + assert.strictEqual(result.provisional, true); + await agent.sendMessage(result.session, URI.parse(buildDefaultChatUri(result.session)), 'hello'); + + // `_readSessionMetadata` reads back the exact agent field the + // resume path consumes, so asserting it stands in for restore. + const stored = await internals._readSessionMetadata(result.session); + assert.deepStrictEqual( + { storedAgent: stored.agent, launchAgentName }, + { storedAgent: { uri: worktreeAgentFile.toString() }, launchAgentName: 'My Agent' }, + 'the repo agent must be rewritten to its worktree copy, both for the SDK launch and the persisted metadata the restore path reads', + ); + } finally { + await disposeAgent(agent); + } + }); + + }); }); From 2c20325da8565fe6c6a1fa080c8f4766130a82de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:37:41 +0000 Subject: [PATCH 050/589] Initial plan From 56ad8dbd40e0cbf4599ab2fc57da974b0082032c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:40:35 +0000 Subject: [PATCH 051/589] Update migrate tooltip to mention Ollama provider Co-authored-by: vritant24 <13074644+vritant24@users.noreply.github.com> --- .../contrib/chat/browser/chatManagement/chatModelsWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 04c6c176ccfc7e..07f18dbe8eed1c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -561,7 +561,7 @@ class ModelNameColumnRenderer extends ModelsTableColumnRenderer Date: Mon, 29 Jun 2026 17:04:27 +0100 Subject: [PATCH 052/589] Update activity bar class for right alignment in floating panels (#323517) style: update activity bar class for right alignment in floating panels Co-authored-by: mrleemurray --- src/vs/workbench/browser/media/floatingPanels.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/media/floatingPanels.css b/src/vs/workbench/browser/media/floatingPanels.css index 85d618ab243a21..7f28295d02d9e4 100644 --- a/src/vs/workbench/browser/media/floatingPanels.css +++ b/src/vs/workbench/browser/media/floatingPanels.css @@ -109,7 +109,7 @@ } /* When the activity bar is on the right, mirror the gutter so the bar stays centered. */ -.monaco-workbench.floating-panels.activitybar-right .part.activitybar { +.monaco-workbench.floating-panels .part.activitybar.right { margin-left: 0; margin-right: var(--vscode-spacing-size40); } From f79cadbbbf4b8189e8cfe5d540d21e326cd7d4ca Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:05:36 +0200 Subject: [PATCH 053/589] Fix paths on flaky smoke pipeline (#323519) --- build/azure-pipelines/product-smoke-flaky.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines/product-smoke-flaky.yml b/build/azure-pipelines/product-smoke-flaky.yml index 2588e57679e637..53b9f5a61f41a5 100644 --- a/build/azure-pipelines/product-smoke-flaky.yml +++ b/build/azure-pipelines/product-smoke-flaky.yml @@ -152,7 +152,7 @@ extends: name: 1es-ubuntu-22.04-x64 os: linux jobs: - - template: linux/product-smoke-flaky-linux.yml@self + - template: build/azure-pipelines/linux/product-smoke-flaky-linux.yml@self parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} iterations: ${{ parameters.iterations }} @@ -164,7 +164,7 @@ extends: name: 1es-windows-2022-x64 os: windows jobs: - - template: win32/product-smoke-flaky-win32.yml@self + - template: build/azure-pipelines/win32/product-smoke-flaky-win32.yml@self parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} iterations: ${{ parameters.iterations }} @@ -176,6 +176,6 @@ extends: name: AcesShared os: macOS jobs: - - template: darwin/product-smoke-flaky-darwin.yml@self + - template: build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml@self parameters: iterations: ${{ parameters.iterations }} From a9f0f574fced779121cfbe595a039f420346894d Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 29 Jun 2026 18:37:26 +0200 Subject: [PATCH 054/589] adds proposed createQuickDiffInformation API, adopts it in markdown editor. (#323470) * adds proposed createQuickDiffInformation API, adopts it in markdown editor. * Extends textEditorDiffInformation proposal instead of having new quickDiff proposal --- .../markdown-editor-src/editor.ts | 11 ++- .../markdown-language-features/package.json | 3 +- .../src/preview/markdownEditorProvider.ts | 79 +++++++++++++++++ .../markdown-language-features/tsconfig.json | 3 +- .../api/browser/mainThreadQuickDiff.ts | 55 +++++++++++- .../workbench/api/common/extHost.api.impl.ts | 6 +- .../workbench/api/common/extHost.protocol.ts | 3 + .../workbench/api/common/extHostQuickDiff.ts | 84 ++++++++++++++++++- .../contrib/scm/browser/quickDiffModel.ts | 30 ++++--- ...de.proposed.textEditorDiffInformation.d.ts | 35 ++++++++ 10 files changed, 291 insertions(+), 18 deletions(-) diff --git a/extensions/markdown-language-features/markdown-editor-src/editor.ts b/extensions/markdown-language-features/markdown-editor-src/editor.ts index 8f46a419f06d15..b05eb9d7da2ac3 100644 --- a/extensions/markdown-language-features/markdown-editor-src/editor.ts +++ b/extensions/markdown-language-features/markdown-editor-src/editor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorController, EditorModel, EditorView, StringEdit, StringValue, findNodeOffsetById, taskCheckboxRange } from '@vscode/markdown-editor'; +import { EditorController, EditorModel, EditorView, GutterMarker, OffsetRange, StringEdit, StringValue, findNodeOffsetById, taskCheckboxRange } from '@vscode/markdown-editor'; import { Disposable, autorun } from '@vscode/markdown-editor/observables'; import mermaid from 'mermaid'; import 'katex/dist/katex.min.css'; @@ -54,6 +54,14 @@ class Editor extends Disposable { this.isUpdatingFromExtension = false; break; } + case 'gutterMarkers': { + const markers: GutterMarker[] = message.markers.map((marker: { start: number; endExclusive: number; type: GutterMarker['type'] }) => ({ + range: OffsetRange.fromTo(marker.start, marker.endExclusive), + type: marker.type, + })); + this.model.gutterMarkers.set(markers, undefined); + break; + } } }); @@ -100,6 +108,7 @@ class Editor extends Disposable { return div; }, })); + this._register(new EditorController(model, view)); host.appendChild(view.element); diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 87ab67029912a6..e6812f69107c83 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -10,7 +10,8 @@ "enabledApiProposals": [ "customEditorDiffs", "documentDiff", - "documentSyntaxHighlighting" + "documentSyntaxHighlighting", + "textEditorDiffInformation" ], "engines": { "vscode": "^1.70.0" diff --git a/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts b/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts index 3f008ca23a8550..998cbb19ea16f5 100644 --- a/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts +++ b/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts @@ -76,14 +76,55 @@ export class MarkdownEditorProvider extends Disposable implements vscode.CustomT }); const highlight = this.#wireHighlight(webview); + const quickDiff = this.#wireQuickDiff(document, webview); webviewPanel.onDidDispose(() => { onMessage.dispose(); onDocumentChange.dispose(); highlight.dispose(); + quickDiff.dispose(); }); } + /** + * Forwards the source-control change information for the document (the same + * added/modified/deleted line changes shown in the editor gutter) to the + * webview, where it is painted in the Markdown editor's gutter. Line ranges + * are converted to source character offsets here, since the webview works in + * offsets. + */ + #wireQuickDiff(document: vscode.TextDocument, webview: vscode.Webview): vscode.Disposable { + const diffProvider = vscode.window.createSourceControlDiffInformation(document.uri); + + const postMarkers = () => { + const diffInformation = diffProvider.diffInformation; + // The changes are computed asynchronously against a specific document + // version. Only map them to offsets while that version still matches the + // document we hold, otherwise the line positions could be stale. A newer + // diff for the current version will arrive via onDidChange. + if (!diffInformation || diffInformation.isStale) { + return; + } + webview.postMessage({ type: 'gutterMarkers', markers: toGutterMarkers(document, diffInformation.changes) }); + }; + + const onChange = diffProvider.onDidChange(postMarkers); + // Re-send once the webview has (re)initialized its model, and whenever the + // document settles on the version the changes were computed for. + const onMessage = webview.onDidReceiveMessage((message) => { + if (message.type === 'ready') { + postMarkers(); + } + }); + const onDocumentChange = vscode.workspace.onDidChangeTextDocument((e) => { + if (e.document.uri.toString() === document.uri.toString()) { + postMarkers(); + } + }); + + return vscode.Disposable.from(diffProvider, onChange, onMessage, onDocumentChange); + } + /** * Proxies the webview's syntax highlighting requests to the * `documentSyntaxHighlighting` proposed API, since the webview cannot call @@ -143,3 +184,41 @@ function getNonce(): string { } return text; } + +interface GutterMarkerMessage { + readonly start: number; + readonly endExclusive: number; + readonly type: 'added' | 'modified' | 'deleted'; +} + +/** + * Converts the line-based source control changes into source character offset + * ranges understood by the Markdown editor's `gutterMarkers`. Added/modified + * changes map to the offset span of their modified lines; deleted changes map to + * an empty range at the boundary where the removed text used to be. + * + * Line ranges use {@link vscode.TextEditorLineRange} semantics: 1-based + * `startLineNumber` and exclusive `endLineNumberExclusive`. + */ +function toGutterMarkers(document: vscode.TextDocument, changes: readonly vscode.TextEditorChange[]): GutterMarkerMessage[] { + const markers: GutterMarkerMessage[] = []; + for (const change of changes) { + if (change.kind === vscode.TextEditorChangeKind.Deletion) { + // The modified range is empty; place an empty marker at the start of the + // line where the removed content used to be. + const line = Math.max(0, change.modified.startLineNumber - 1); + const offset = document.offsetAt(new vscode.Position(line, 0)); + markers.push({ start: offset, endExclusive: offset, type: 'deleted' }); + continue; + } + + const start = document.offsetAt(new vscode.Position(change.modified.startLineNumber - 1, 0)); + const endExclusive = document.offsetAt(document.lineAt(change.modified.endLineNumberExclusive - 2).range.end); + markers.push({ + start, + endExclusive, + type: change.kind === vscode.TextEditorChangeKind.Addition ? 'added' : 'modified', + }); + } + return markers; +} diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index 682b9727806ba5..7384236714cf4c 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -13,6 +13,7 @@ "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.customEditorDiffs.d.ts", "../../src/vscode-dts/vscode.proposed.documentDiff.d.ts", - "../../src/vscode-dts/vscode.proposed.documentSyntaxHighlighting.d.ts" + "../../src/vscode-dts/vscode.proposed.documentSyntaxHighlighting.d.ts", + "../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts" ] } diff --git a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts index 2d15ed9a843dd1..1288c4b7dc47ac 100644 --- a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts +++ b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../base/common/cancellation.js'; -import { DisposableMap, IDisposable } from '../../../base/common/lifecycle.js'; +import { DisposableMap, DisposableStore, IDisposable, IReference } from '../../../base/common/lifecycle.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; -import { ExtHostContext, ExtHostQuickDiffShape, IDocumentFilterDto, MainContext, MainThreadQuickDiffShape } from '../common/extHost.protocol.js'; +import { ExtHostContext, ExtHostQuickDiffShape, IDocumentFilterDto, ITextEditorChange, ITextEditorDiffInformation, MainContext, MainThreadQuickDiffShape } from '../common/extHost.protocol.js'; import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js'; +import { IQuickDiffModelService, QuickDiffModel } from '../../contrib/scm/browser/quickDiffModel.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; @extHostNamedCustomer(MainContext.MainThreadQuickDiff) @@ -15,10 +16,12 @@ export class MainThreadQuickDiff implements MainThreadQuickDiffShape { private readonly proxy: ExtHostQuickDiffShape; private providerDisposables = new DisposableMap(); + private informationDisposables = new DisposableMap(); constructor( extHostContext: IExtHostContext, - @IQuickDiffService private readonly quickDiffService: IQuickDiffService + @IQuickDiffService private readonly quickDiffService: IQuickDiffService, + @IQuickDiffModelService private readonly quickDiffModelService: IQuickDiffModelService ) { this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostQuickDiff); } @@ -44,7 +47,53 @@ export class MainThreadQuickDiff implements MainThreadQuickDiffShape { } } + async $createSourceControlDiffInformation(handle: number, uri: UriComponents): Promise { + const reference = this.quickDiffModelService.createQuickDiffModelReference(URI.revive(uri)); + if (!reference) { + return; + } + + const store = new DisposableStore(); + store.add(reference); + store.add(reference.object.onDidChange(() => this.sendSourceControlDiffInformation(handle, reference))); + this.informationDisposables.set(handle, store); + + // Push the current state so the extension host has an initial value. + this.sendSourceControlDiffInformation(handle, reference); + } + + async $disposeSourceControlDiffInformation(handle: number): Promise { + if (this.informationDisposables.has(handle)) { + this.informationDisposables.deleteAndDispose(handle); + } + } + + private sendSourceControlDiffInformation(handle: number, reference: IReference): void { + const model = reference.object; + const primaryResult = model.getQuickDiffResults().find(result => result.providerKind === 'primary'); + if (!primaryResult) { + this.proxy.$acceptSourceControlDiffInformation(handle, undefined); + return; + } + + const changes: ITextEditorChange[] = primaryResult.changes2.map(change => [ + change.original.startLineNumber, + change.original.endLineNumberExclusive, + change.modified.startLineNumber, + change.modified.endLineNumberExclusive, + ]); + + const diffInformation: ITextEditorDiffInformation = { + documentVersion: model.changesVersionId, + original: primaryResult.original, + modified: primaryResult.modified, + changes, + }; + this.proxy.$acceptSourceControlDiffInformation(handle, diffInformation); + } + dispose(): void { this.providerDisposables.dispose(); + this.informationDisposables.dispose(); } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 5797fa97c2747f..d40a0c0e8441c1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -228,7 +228,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService(rpcProtocol, extHostLogService, extHostDocumentsAndEditors)); const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, createExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands)); const extHostSCM = rpcProtocol.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(rpcProtocol, extHostCommands, extHostDocuments, extHostLogService)); - const extHostQuickDiff = rpcProtocol.set(ExtHostContext.ExtHostQuickDiff, new ExtHostQuickDiff(rpcProtocol, uriTransformer)); + const extHostQuickDiff = rpcProtocol.set(ExtHostContext.ExtHostQuickDiff, new ExtHostQuickDiff(rpcProtocol, extHostDocuments, uriTransformer)); const extHostShare = rpcProtocol.set(ExtHostContext.ExtHostShare, new ExtHostShare(rpcProtocol, uriTransformer)); const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, createExtHostComments(rpcProtocol, extHostCommands, extHostDocuments)); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol)); @@ -1053,6 +1053,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'quickDiffProvider'); return extHostQuickDiff.registerQuickDiffProvider(extension, checkSelector(selector), quickDiffProvider, id, label, rootUri); }, + createSourceControlDiffInformation(uri: vscode.Uri): vscode.SourceControlDiffInformationProvider { + checkProposedApiEnabled(extension, 'textEditorDiffInformation'); + return extHostQuickDiff.createSourceControlDiffInformation(uri); + }, get tabGroups(): vscode.TabGroups { return extHostEditorTabs.tabGroups; }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4e914803f807cb..ea07afb270ac8c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2194,6 +2194,8 @@ export interface MainThreadSCMShape extends IDisposable { export interface MainThreadQuickDiffShape extends IDisposable { $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string, rootUri: UriComponents | undefined): Promise; $unregisterQuickDiffProvider(handle: number): Promise; + $createSourceControlDiffInformation(handle: number, uri: UriComponents): Promise; + $disposeSourceControlDiffInformation(handle: number): Promise; } export interface IDocumentDiffLineChangeDto { @@ -3190,6 +3192,7 @@ export interface ExtHostSCMShape { export interface ExtHostQuickDiffShape { $provideOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise; + $acceptSourceControlDiffInformation(handle: number, diffInformation: ITextEditorDiffInformation | undefined): void; } export interface ExtHostShareShape { diff --git a/src/vs/workbench/api/common/extHostQuickDiff.ts b/src/vs/workbench/api/common/extHostQuickDiff.ts index b17b67c6a3d85d..3ce817c4af2848 100644 --- a/src/vs/workbench/api/common/extHostQuickDiff.ts +++ b/src/vs/workbench/api/common/extHostQuickDiff.ts @@ -5,21 +5,91 @@ import type * as vscode from 'vscode'; import { CancellationToken } from '../../../base/common/cancellation.js'; +import { Emitter } from '../../../base/common/event.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; -import { ExtHostQuickDiffShape, IMainContext, MainContext, MainThreadQuickDiffShape } from './extHost.protocol.js'; +import { ExtHostQuickDiffShape, IMainContext, ITextEditorDiffInformation, MainContext, MainThreadQuickDiffShape } from './extHost.protocol.js'; import { asPromise } from '../../../base/common/async.js'; import { DocumentSelector } from './extHostTypeConverters.js'; +import { TextEditorChangeKind } from './extHostTypes.js'; +import { ExtHostDocuments } from './extHostDocuments.js'; import { IURITransformer } from '../../../base/common/uriIpc.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +class ExtHostSourceControlDiffInformation implements vscode.SourceControlDiffInformationProvider { + + private readonly _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + private _diffInformation: vscode.TextEditorDiffInformation | undefined; + get diffInformation(): vscode.TextEditorDiffInformation | undefined { return this._diffInformation; } + + constructor( + private readonly handle: number, + private readonly proxy: MainThreadQuickDiffShape, + private readonly documents: ExtHostDocuments, + private readonly onDispose: (handle: number) => void + ) { } + + $acceptDiffInformation(diffInformation: ITextEditorDiffInformation | undefined): void { + if (!diffInformation) { + this._diffInformation = undefined; + this._onDidChange.fire(); + return; + } + + const documents = this.documents; + const original = URI.revive(diffInformation.original); + const modified = URI.revive(diffInformation.modified); + + const changes = diffInformation.changes.map(change => { + const [originalStartLineNumber, originalEndLineNumberExclusive, modifiedStartLineNumber, modifiedEndLineNumberExclusive] = change; + + let kind: vscode.TextEditorChangeKind; + if (originalStartLineNumber === originalEndLineNumberExclusive) { + kind = TextEditorChangeKind.Addition; + } else if (modifiedStartLineNumber === modifiedEndLineNumberExclusive) { + kind = TextEditorChangeKind.Deletion; + } else { + kind = TextEditorChangeKind.Modification; + } + + return { + original: { startLineNumber: originalStartLineNumber, endLineNumberExclusive: originalEndLineNumberExclusive }, + modified: { startLineNumber: modifiedStartLineNumber, endLineNumberExclusive: modifiedEndLineNumberExclusive }, + kind + } satisfies vscode.TextEditorChange; + }); + + this._diffInformation = Object.freeze({ + documentVersion: diffInformation.documentVersion, + original, + modified, + changes, + get isStale(): boolean { + const document = documents.getDocumentData(modified); + return document?.document.version !== diffInformation.documentVersion; + } + }); + this._onDidChange.fire(); + } + + dispose(): void { + this.proxy.$disposeSourceControlDiffInformation(this.handle); + this._onDidChange.dispose(); + this.onDispose(this.handle); + } +} + export class ExtHostQuickDiff implements ExtHostQuickDiffShape { private static handlePool: number = 0; private proxy: MainThreadQuickDiffShape; private providers: Map = new Map(); + private informations: Map = new Map(); constructor( mainContext: IMainContext, + private readonly documents: ExtHostDocuments, private readonly uriTransformer: IURITransformer | undefined ) { this.proxy = mainContext.getProxy(MainContext.MainThreadQuickDiff); @@ -37,6 +107,10 @@ export class ExtHostQuickDiff implements ExtHostQuickDiffShape { .then(r => r || null); } + $acceptSourceControlDiffInformation(handle: number, diffInformation: ITextEditorDiffInformation | undefined): void { + this.informations.get(handle)?.$acceptDiffInformation(diffInformation); + } + registerQuickDiffProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, id: string, label: string, rootUri?: vscode.Uri): vscode.Disposable { const handle = ExtHostQuickDiff.handlePool++; this.providers.set(handle, quickDiffProvider); @@ -50,4 +124,12 @@ export class ExtHostQuickDiff implements ExtHostQuickDiffShape { } }; } + + createSourceControlDiffInformation(uri: vscode.Uri): vscode.SourceControlDiffInformationProvider { + const handle = ExtHostQuickDiff.handlePool++; + const information = new ExtHostSourceControlDiffInformation(handle, this.proxy, this.documents, h => this.informations.delete(h)); + this.informations.set(handle, information); + this.proxy.$createSourceControlDiffInformation(handle, uri); + return information; + } } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index 7d239d13157838..7d9546fd66642b 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -95,7 +95,7 @@ export class QuickDiffModelService implements IQuickDiffModelService { export class QuickDiffModel extends Disposable { - private readonly _model: ITextFileEditorModel; + private readonly _model: IResolvedTextFileEditorModel; private readonly _originalEditorModels = new ResourceMap(); private readonly _originalEditorModelsDisposables = this._register(new DisposableStore()); get originalTextModels(): Iterable { @@ -116,6 +116,13 @@ export class QuickDiffModel extends Disposable { private _changes: QuickDiffChange[] = []; get changes(): QuickDiffChange[] { return this._changes; } + private _changesVersionId: number = 0; + /** + * The version id of the modified text model that {@link changes} were + * computed against. Matches {@link ITextModel.getVersionId}. + */ + get changesVersionId(): number { return this._changesVersionId; } + /** * Map of quick diff name to the index of the change in `this.changes` */ @@ -138,6 +145,7 @@ export class QuickDiffModel extends Disposable { ) { super(); this._model = textFileModel; + this._changesVersionId = textFileModel.textEditorModel.getVersionId(); this._register(textFileModel.textEditorModel.onDidChangeContent(() => this.triggerDiff())); this._register( @@ -155,7 +163,7 @@ export class QuickDiffModel extends Disposable { this._quickDiffs = []; this._originalEditorModels.clear(); this._quickDiffsPromise = undefined; - this.setChanges([], [], new Map()); + this.setChanges([], [], new Map(), this._model.textEditorModel.getVersionId()); this.triggerDiff(); })); @@ -199,7 +207,7 @@ export class QuickDiffModel extends Disposable { const editorModel = this._originalEditorModels.get(originalUri); return editorModel ? { - modified: this._model.textEditorModel!, + modified: this._model.textEditorModel, original: editorModel.textEditorModel } : undefined; } @@ -224,40 +232,42 @@ export class QuickDiffModel extends Disposable { this._diffDelayer .trigger(async () => { - const result: { allChanges: QuickDiffChange[]; changes: QuickDiffChange[]; mapChanges: Map } | null = await this.diff(); + const result: { allChanges: QuickDiffChange[]; changes: QuickDiffChange[]; mapChanges: Map; versionId: number } | null = await this.diff(); const editorModels = Array.from(this._originalEditorModels.values()); if (!result || this._disposed || this._model.isDisposed() || editorModels.some(editorModel => editorModel.isDisposed())) { return; // disposed } - this.setChanges(result.allChanges, result.changes, result.mapChanges); + this.setChanges(result.allChanges, result.changes, result.mapChanges, result.versionId); }) .catch(err => onUnexpectedError(err)); } - private setChanges(allChanges: QuickDiffChange[], changes: QuickDiffChange[], mapChanges: Map): void { + private setChanges(allChanges: QuickDiffChange[], changes: QuickDiffChange[], mapChanges: Map, versionId: number): void { const diff = sortedDiff(this.changes, changes, (a, b) => compareChanges(a.change, b.change)); this._allChanges = allChanges; this._changes = changes; this._quickDiffChanges = mapChanges; + this._changesVersionId = versionId; this._onDidChange.fire({ changes, diff }); } - private diff(): Promise<{ allChanges: QuickDiffChange[]; changes: QuickDiffChange[]; mapChanges: Map } | null> { + private diff(): Promise<{ allChanges: QuickDiffChange[]; changes: QuickDiffChange[]; mapChanges: Map; versionId: number } | null> { const location = this.environmentService.isSessionsWindow ? ProgressLocation.Window : ProgressLocation.Scm; return this.progressService.withProgress({ location, delay: 250 }, async () => { + const versionId = this._model.textEditorModel.getVersionId(); const originalURIs = await this.getQuickDiffsPromise(); if (this._disposed || this._model.isDisposed() || (originalURIs.length === 0)) { // Disposed - return Promise.resolve({ allChanges: [], changes: [], mapChanges: new Map() }); + return Promise.resolve({ allChanges: [], changes: [], mapChanges: new Map(), versionId }); } const quickDiffs = originalURIs .filter(quickDiff => this.editorWorkerService.canComputeDirtyDiff(quickDiff.originalResource, this._model.resource)); if (quickDiffs.length === 0) { // All files are too large - return Promise.resolve({ allChanges: [], changes: [], mapChanges: new Map() }); + return Promise.resolve({ allChanges: [], changes: [], mapChanges: new Map(), versionId }); } const quickDiffPrimary = quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); @@ -331,7 +341,7 @@ export class QuickDiffModel extends Disposable { map.get(providerId)!.push(i); } - return { allChanges: allDiffsSorted, changes: diffsSorted, mapChanges: map }; + return { allChanges: allDiffsSorted, changes: diffsSorted, mapChanges: map, versionId }; }); } diff --git a/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts index faa18c2932d610..e940734ad823a0 100644 --- a/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts +++ b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts @@ -40,8 +40,43 @@ declare module 'vscode' { readonly diffInformation: TextEditorDiffInformation[] | undefined; } + /** + * A live, disposable view of the source control diff information of a resource + * that is not necessarily shown in a {@link TextEditor} (for example a document + * rendered by a custom editor). + */ + export interface SourceControlDiffInformationProvider extends Disposable { + /** + * The current diff information for the resource, reflecting the primary + * source control provider (e.g. git), or `undefined` while it is unavailable + * (for example before the first diff has been computed or when the resource + * is not under source control). + */ + readonly diffInformation: TextEditorDiffInformation | undefined; + + /** + * An event that fires whenever {@link diffInformation} changes. + */ + readonly onDidChange: Event; + } + export namespace window { export const onDidChangeTextEditorDiffInformation: Event; + + /** + * Observe the source control diff information for a resource. + * + * Unlike {@link TextEditor.diffInformation}, this does not require the + * resource to be shown in a {@link TextEditor}; the resource only needs to be + * backed by an open {@link TextDocument}. This is useful for custom editors, + * which render their document in a webview rather than a text editor. + * + * The returned provider must be disposed once it is no longer needed. + * + * @param uri The resource to observe. + * @returns A live, disposable view of the resource's diff information. + */ + export function createSourceControlDiffInformation(uri: Uri): SourceControlDiffInformationProvider; } } From 30da3c1965b223ef403d5577997bba371ab4ba22 Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Mon, 29 Jun 2026 09:53:54 -0700 Subject: [PATCH 055/589] Fix intermittent Apply CG NOTICE failures (gate to publishing builds + continueOnError) (#323516) --- .../steps/product-build-darwin-compile.yml | 25 +++++++++++-------- .../steps/product-build-linux-compile.yml | 25 +++++++++++-------- .../steps/product-build-win32-compile.yml | 25 +++++++++++-------- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml index b5437077cd3a6b..20aa65bd407330 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml @@ -121,11 +121,13 @@ steps: # Start pulling the CG-generated NOTICE from the parallel Quality stage # in the background (overwrites repo-root ThirdPartyNotices.txt before packaging). - - script: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadNotice.ts - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) - displayName: Pull CG NOTICE (background) + # Publish builds only (CIBUILD=false) — CI test jobs never ship a notice. + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - script: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Pull CG NOTICE (background) - template: ../../common/install-builtin-extensions.yml@self @@ -169,11 +171,14 @@ steps: # Block on the CG NOTICE download and overwrite ThirdPartyNotices.txt # before gulp packages it (and before any later signing/notarization). # Non-fatal: falls back to legacy on any problem. - - script: npx deemon --attach -- node build/azure-pipelines/common/downloadNotice.ts - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) - displayName: Apply CG NOTICE + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - script: npx deemon --attach -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Apply CG NOTICE + # deemon --attach can exit non-zero if the background daemon died; never fail for that. + continueOnError: true - script: | set -e diff --git a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml index 3972a9927fddcb..7c60614f64ce6c 100644 --- a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml +++ b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml @@ -172,11 +172,13 @@ steps: # Start pulling the CG-generated NOTICE from the parallel Quality stage # in the background (overwrites repo-root ThirdPartyNotices.txt before packaging). - - script: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadNotice.ts - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) - displayName: Pull CG NOTICE (background) + # Publish builds only (CIBUILD=false) — CI test jobs never ship a notice. + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - script: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Pull CG NOTICE (background) - template: ../../common/install-builtin-extensions.yml@self @@ -221,11 +223,14 @@ steps: # Block on the CG NOTICE download and overwrite ThirdPartyNotices.txt # right before gulp packages it. Non-fatal: falls back to legacy on any problem. - - script: npx deemon --attach -- node build/azure-pipelines/common/downloadNotice.ts - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) - displayName: Apply CG NOTICE + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - script: npx deemon --attach -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Apply CG NOTICE + # deemon --attach can exit non-zero if the background daemon died; never fail for that. + continueOnError: true - script: | set -e diff --git a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml index c14cbcc6de28a2..416c522d7d1958 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml @@ -113,11 +113,13 @@ steps: # Start pulling the CG-generated NOTICE from the parallel Quality stage # in the background (overwrites repo-root ThirdPartyNotices.txt before packaging). - - pwsh: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadNotice.ts - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) - displayName: Pull CG NOTICE (background) + # Publish builds only (CIBUILD=false) — CI test jobs never ship a notice. + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - pwsh: npx deemon --detach --wait -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Pull CG NOTICE (background) - template: ../../common/install-builtin-extensions.yml@self @@ -167,11 +169,14 @@ steps: # Block on the CG NOTICE download and overwrite ThirdPartyNotices.txt # right before gulp packages it. Non-fatal: falls back to legacy on any problem. - - pwsh: npx deemon --attach -- node build/azure-pipelines/common/downloadNotice.ts - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) - displayName: Apply CG NOTICE + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - pwsh: npx deemon --attach -- node build/azure-pipelines/common/downloadNotice.ts + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_OVERWRITE_TPN: $(VSCODE_OVERWRITE_TPN) + displayName: Apply CG NOTICE + # deemon --attach can exit non-zero if the background daemon died; never fail for that. + continueOnError: true - powershell: | . build/azure-pipelines/win32/exec.ps1 From 19c9f45e467a381b590bab4909d9c1cdcd75050f Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 29 Jun 2026 09:55:10 -0700 Subject: [PATCH 056/589] Fix agent host client tool stalls (#323526) * Fix agent host client tool stalls Prefer tools from the sending active client when multiple active clients provide the same tool. Ensure client tool failures during prepare invocation and completions from pending confirmation state are reported back so tool calls do not stall. Fixes #323225 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix pending permission cleanup for client tool completion Ensure client tool completions that unblock pending permission requests go through the normal permission response cleanup path so pending edit content and registry entries are removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../platform/agentHost/common/agentService.ts | 2 +- .../agentHost/node/activeClientState.ts | 12 +- .../platform/agentHost/node/agentService.ts | 2 +- .../agentHost/node/agentSideEffects.ts | 9 +- .../claudeSessionClientToolsModel.ts | 6 +- .../agentHost/node/copilot/copilotAgent.ts | 10 +- .../node/copilot/copilotAgentSession.ts | 16 +- .../test/node/agentSideEffects.test.ts | 20 +++ .../claudeSessionClientToolsModel.test.ts | 15 ++ .../agentHost/test/node/copilotAgent.test.ts | 141 ++++++++++++++++-- .../platform/agentHost/test/node/mockAgent.ts | 5 +- .../agentHost/agentHostSessionHandler.ts | 34 +++-- .../agentHostClientTools.test.ts | 91 ++++++++++- 13 files changed, 309 insertions(+), 54 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index a49cfce19d2f4e..c6109b298ef816 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -1090,7 +1090,7 @@ export interface IAgent { sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise; /** Send a user message into a chat within an existing session. */ - sendMessage(session: URI, chat: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string): Promise; + sendMessage(session: URI, chat: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string): Promise; /** * Create an additional chat within an existing session, backed by a new diff --git a/src/vs/platform/agentHost/node/activeClientState.ts b/src/vs/platform/agentHost/node/activeClientState.ts index b94c5bf8bd089c..d758a0e2180947 100644 --- a/src/vs/platform/agentHost/node/activeClientState.ts +++ b/src/vs/platform/agentHost/node/activeClientState.ts @@ -123,12 +123,14 @@ export class ActiveClientToolSet { } /** - * The `clientId` that owns the merged tool named `toolName` (the - * first-inserted contributor of that name), or `undefined` when no active - * client provides it. Used to stamp client tool calls with their owning - * client at invocation time. + * The `clientId` that owns the tool named `toolName`, or `undefined` when + * no active client provides it. When `preferredClientId` currently provides + * the tool it wins; otherwise the first-inserted contributor wins. */ - ownerOf(toolName: string): string | undefined { + ownerOf(toolName: string, preferredClientId?: string): string | undefined { + if (preferredClientId && this.get(preferredClientId).some(tool => tool.name === toolName)) { + return preferredClientId; + } for (const [clientId, tools] of this._byClient) { if (tools.some(tool => tool.name === toolName)) { return clientId; diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index ab51dc9b33d530..508a9e6eec063f 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -1282,7 +1282,7 @@ export class AgentService extends Disposable implements IAgentService { if (action.type === ActionType.RootConfigChanged) { this._configurationService.persistRootConfig(); } - this._sideEffects.handleAction(channel, action); + this._sideEffects.handleAction(channel, action, clientId); } private _needsAsyncRewrite(channel: string, action: SessionAction | ChatAction | TerminalAction | ClientAnnotationsAction | IRootConfigChangedAction): action is ChatTurnStartedAction | ChatPendingMessageSetAction { diff --git a/src/vs/platform/agentHost/node/agentSideEffects.ts b/src/vs/platform/agentHost/node/agentSideEffects.ts index 5d806634ce2fd4..2213417294ddd2 100644 --- a/src/vs/platform/agentHost/node/agentSideEffects.ts +++ b/src/vs/platform/agentHost/node/agentSideEffects.ts @@ -744,7 +744,7 @@ export class AgentSideEffects extends Disposable { ); } - handleAction(channel: ProtocolURI, action: StateAction): void { + handleAction(channel: ProtocolURI, action: StateAction, clientId?: string): void { const chatChannel = isAhpChatChannel(channel) ? channel : undefined; const sessionChannel = chatChannel ? parseRequiredSessionUriFromChatUri(chatChannel) : channel; switch (action.type) { @@ -790,6 +790,7 @@ export class AgentSideEffects extends Disposable { chat: channel, message: action.message, turnId: action.turnId, + senderClientId: clientId, }); break; } @@ -1142,6 +1143,7 @@ export class AgentSideEffects extends Disposable { chat: session, message: msg.message, turnId, + senderClientId: undefined, }); } @@ -1169,8 +1171,9 @@ export class AgentSideEffects extends Disposable { chat: ProtocolURI; message: Message; turnId: string; + senderClientId: string | undefined; }): Promise { - const { agent, sessionChannel, turnChannel, chat, message, turnId } = options; + const { agent, sessionChannel, turnChannel, chat, message, turnId, senderClientId } = options; const sessionUri = URI.parse(sessionChannel); const chatUri = URI.parse(chat); @@ -1192,7 +1195,7 @@ export class AgentSideEffects extends Disposable { await Promise.all(selectionUpdates); - await agent.sendMessage(URI.parse(sessionChannel), chatUri, message.text, message.attachments, turnId).catch(err => { + await agent.sendMessage(URI.parse(sessionChannel), chatUri, message.text, message.attachments, turnId, senderClientId).catch(err => { const errCode = (err as { code?: number })?.code; this._logService.error(`[AgentSideEffects] sendMessage failed for session=${turnChannel}: code=${errCode}, message=${err instanceof Error ? err.message : String(err)}, type=${err?.constructor?.name}`, err); this._stateManager.dispatchServerAction(turnChannel, { diff --git a/src/vs/platform/agentHost/node/claude/clientTools/claudeSessionClientToolsModel.ts b/src/vs/platform/agentHost/node/claude/clientTools/claudeSessionClientToolsModel.ts index de679d950e3229..df21a7efaa01cd 100644 --- a/src/vs/platform/agentHost/node/claude/clientTools/claudeSessionClientToolsModel.ts +++ b/src/vs/platform/agentHost/node/claude/clientTools/claudeSessionClientToolsModel.ts @@ -52,9 +52,9 @@ export class SessionClientToolsModel { } } - /** The `clientId` that owns the merged tool named `toolName`, or `undefined`. */ - ownerOf(toolName: string): string | undefined { - return this._toolSet.ownerOf(toolName); + /** The `clientId` that owns the tool named `toolName`, or `undefined`. */ + ownerOf(toolName: string, preferredClientId?: string): string | undefined { + return this._toolSet.ownerOf(toolName, preferredClientId); } } diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index c3b71083852dd1..b44694bde1181d 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -1505,7 +1505,7 @@ export class CopilotAgent extends Disposable implements IAgent { } } - async sendMessage(session: URI, chat: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string): Promise { + async sendMessage(session: URI, chat: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string): Promise { // Additional (non-default) chats are backed by their own SDK // conversation tracked in `_chatSessions`, keyed by the chat URI. if (!isDefaultChatUri(chat)) { @@ -1514,9 +1514,9 @@ export class CopilotAgent extends Disposable implements IAgent { throw new Error(`[Copilot] sendMessage for unknown chat: ${chat.toString()}`); } if (turnId) { - entry.resetTurnState(turnId); + entry.resetTurnState(turnId, senderClientId); } - await entry.send(prompt, attachments, turnId, this._resolveSdkMode(session)); + await entry.send(prompt, attachments, turnId, this._resolveSdkMode(session), senderClientId); return; } const sessionId = AgentSession.id(session); @@ -1554,7 +1554,7 @@ export class CopilotAgent extends Disposable implements IAgent { // next text/reasoning chunk (and any host-emitted announcement) // allocates a fresh response part. if (turnId) { - entry.resetTurnState(turnId); + entry.resetTurnState(turnId, senderClientId); } // Emit any pending first-turn announcement (e.g. worktree @@ -1570,7 +1570,7 @@ export class CopilotAgent extends Disposable implements IAgent { try { const sdkMode = this._resolveSdkMode(session); - await entry.send(prompt, attachments, turnId, sdkMode); + await entry.send(prompt, attachments, turnId, sdkMode, senderClientId); } catch (err) { const errCode = (err as { code?: number })?.code; const errMsg = err instanceof Error ? err.message : String(err); diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index 2bd5a276591c0b..f3547258990b62 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -400,7 +400,7 @@ class CopilotTurn { /** Current reasoning response part IDs for this turn, keyed by `parentToolCallId ?? ''`. */ readonly reasoningPartIds = new Map(); - constructor(readonly id: string) { } + constructor(readonly id: string, readonly senderClientId: string | undefined) { } get state(): CopilotTurnState { return this._state; } get isPending(): boolean { return this._state === 'pending'; } @@ -769,8 +769,8 @@ export class CopilotAgentSession extends Disposable { * from a previous turn so the next text/reasoning chunk allocates a new * response part. The turn becomes `running` on the first SDK event. */ - resetTurnState(turnId: string): void { - this._currentTurn = new CopilotTurn(turnId); + resetTurnState(turnId: string, senderClientId?: string): void { + this._currentTurn = new CopilotTurn(turnId, senderClientId); } private _completeActiveTurn(): void { @@ -995,6 +995,10 @@ export class CopilotAgentSession extends Disposable { binaryResultsForLlm: binaryResults?.length ? binaryResults : undefined, }); } + + // Still pending permission, so this call may have errored while getting permission. + // Go ahead and allow the call which will immediately see the buffered value. + this.respondToPermissionRequest(toolCallId, true); } /** @@ -1038,12 +1042,12 @@ export class CopilotAgentSession extends Disposable { // ---- session operations ------------------------------------------------- - async send(prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, mode?: CopilotSdkMode): Promise { + async send(prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, mode?: CopilotSdkMode, senderClientId?: string): Promise { if (turnId && this._currentTurn?.id !== turnId) { // Establish the `pending` turn for this message. Callers normally // call `resetTurnState` just before `send()`; this covers the // direct-send path and is a no-op when the turn already exists. - this.resetTurnState(turnId); + this.resetTurnState(turnId, senderClientId); } this._logService.info(`[Copilot:${this.sessionId}] sendMessage called: "${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}" (${attachments?.length ?? 0} attachments)`); @@ -2503,7 +2507,7 @@ export class CopilotAgentSession extends Disposable { let contributor: { readonly kind: ToolCallContributorKind.Client; readonly clientId: string } | { readonly kind: ToolCallContributorKind.MCP; readonly customizationId: string } | undefined; const isClientTool = this._clientToolNames.has(e.data.toolName); - const ownerClientId = isClientTool ? this._activeClientToolSet.ownerOf(e.data.toolName) : undefined; + const ownerClientId = isClientTool ? this._activeClientToolSet.ownerOf(e.data.toolName, this._currentTurn?.senderClientId) : undefined; if (ownerClientId) { contributor = { kind: ToolCallContributorKind.Client, clientId: ownerClientId }; } else if (e.data.mcpServerName) { diff --git a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts index 82218bf7fad264..399260a66b418c 100644 --- a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts @@ -249,6 +249,26 @@ suite('AgentSideEffects', () => { assert.deepStrictEqual(agent.sendMessageCalls, [{ session: URI.parse(sessionUri.toString()), prompt: 'hello world', attachments: undefined, chat: URI.parse(defaultChatUri) }]); }); + test('passes the dispatching client id to sendMessage', async () => { + setupSession(); + const action: ChatAction = { + type: ActionType.ChatTurnStarted, + turnId: 'turn-1', + message: { text: 'hello world', origin: { kind: MessageKind.User } }, + }; + sideEffects.handleAction(defaultChatUri, action, 'client-B'); + + await waitForSendMessageCalls(1); + + assert.deepStrictEqual(agent.sendMessageCalls, [{ + session: URI.parse(sessionUri.toString()), + prompt: 'hello world', + attachments: undefined, + chat: URI.parse(defaultChatUri), + senderClientId: 'client-B', + }]); + }); + test('logs telemetry when sending a direct user message', () => { setupSession(); const activeClientAction: SessionAction = { diff --git a/src/vs/platform/agentHost/test/node/clientTools/claudeSessionClientToolsModel.test.ts b/src/vs/platform/agentHost/test/node/clientTools/claudeSessionClientToolsModel.test.ts index 94f8900c0e3928..ef18fa3bbcc00f 100644 --- a/src/vs/platform/agentHost/test/node/clientTools/claudeSessionClientToolsModel.test.ts +++ b/src/vs/platform/agentHost/test/node/clientTools/claudeSessionClientToolsModel.test.ts @@ -120,6 +120,21 @@ suite('SessionClientToolsDiff', () => { assert.strictEqual(diff.model.ownerOf('shared'), 'c1', 'first-inserted client wins the shared name'); }); + test('ownerOf prefers the requested client when it provides the shared tool', () => { + const diff = disposables.add(new SessionClientToolsDiff()); + diff.model.setTools('c1', [tool({ name: 'shared', description: 'from c1' })]); + diff.model.setTools('c2', [tool({ name: 'shared', description: 'from c2' })]); + assert.deepStrictEqual({ + defaultOwner: diff.model.ownerOf('shared'), + preferredOwner: diff.model.ownerOf('shared', 'c2'), + missingPreferredOwner: diff.model.ownerOf('shared', 'missing'), + }, { + defaultOwner: 'c1', + preferredOwner: 'c2', + missingPreferredOwner: 'c1', + }); + }); + test('removeClient drops that client and re-flips dirty when the merged set changes', () => { const diff = disposables.add(new SessionClientToolsDiff()); diff.model.setTools('c1', [tool({ name: 'a' })]); diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index 2d66709cc29d57..83085c348a52ee 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { CopilotClient, CopilotSession, ModelInfo, SessionEventHandler, SessionEventPayload, SessionEventType, TypedSessionEventHandler } from '@github/copilot-sdk'; +import type { CopilotClient, CopilotSession, ModelInfo, SessionEvent, SessionEventHandler, SessionEventPayload, SessionEventType, TypedSessionEventHandler } from '@github/copilot-sdk'; import type Anthropic from '@anthropic-ai/sdk'; import type { CCAModel } from '@vscode/copilot-api'; import assert from 'assert'; import * as fs from 'fs/promises'; import * as os from 'os'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { DeferredPromise } from '../../../../base/common/async.js'; +import { DeferredPromise, timeout } from '../../../../base/common/async.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Disposable, type DisposableStore, type IDisposable, type IReference } from '../../../../base/common/lifecycle.js'; import { Event } from '../../../../base/common/event.js'; @@ -32,8 +32,8 @@ import { AgentHostConfigKey } from '../../common/agentHostCustomizationConfig.js import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js'; import { AgentSession, GITHUB_COPILOT_PROTECTED_RESOURCE, type AgentSignal, type IAgentActionSignal, type IAgentSessionMetadata } from '../../common/agentService.js'; import { ISessionDataService } from '../../common/sessionDataService.js'; -import { buildDefaultChatUri, buildSubagentSessionUri, buildChatUri, CustomizationLoadStatus, MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, TurnState, customizationId, type ClientPluginCustomization, type MarkdownResponsePart, type PluginCustomization, type ToolCallResult, type Turn, RuleCustomization } from '../../common/state/sessionState.js'; -import { CustomizationType, type AgentSelection, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js'; +import { buildDefaultChatUri, buildSubagentSessionUri, buildChatUri, CustomizationLoadStatus, MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, customizationId, type ClientPluginCustomization, type MarkdownResponsePart, type PluginCustomization, type ToolCallResult, type Turn, RuleCustomization } from '../../common/state/sessionState.js'; +import { CustomizationType, ToolCallContributorKind, type AgentSelection, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js'; import { ActionType, type ChatAction, type IDeltaAction, type SessionAction } from '../../common/state/sessionActions.js'; import { AgentConfigurationService, IAgentConfigurationService } from '../../node/agentConfigurationService.js'; @@ -49,6 +49,7 @@ import { CopilotAgentSession } from '../../node/copilot/copilotAgentSession.js'; import { CopilotBranchNameGenerator, ICopilotBranchNameGenerator, getCopilotBranchNameHintFromMessage, normalizeCopilotBranchName } from '../../node/copilot/copilotBranchNameGenerator.js'; import type { CopilotSessionLaunchPlan, IActiveClientSnapshot } from '../../node/copilot/copilotSessionLauncher.js'; import { ShellManager } from '../../node/copilot/copilotShellTools.js'; +import { registerPendingEditContentProvider } from '../../node/copilot/pendingEditContentStore.js'; import { SessionDatabase } from '../../node/sessionDatabase.js'; import { createNullSessionDataService } from '../common/sessionTestHelpers.js'; import { ActiveClientToolSet } from '../../node/activeClientState.js'; @@ -307,11 +308,38 @@ interface IFakeAgentSession { class MockCopilotSession { readonly sessionId = 'test-session-1'; + private readonly _handlers = new Set(); + private readonly _typedHandlers = new Map) => void>>(); on(_handler: SessionEventHandler): () => void; on(_eventType: K, _handler: TypedSessionEventHandler): () => void; - on(_eventTypeOrHandler: K | SessionEventHandler, _handler?: TypedSessionEventHandler): () => void { - return () => { }; + on(eventTypeOrHandler: K | SessionEventHandler, handler?: TypedSessionEventHandler): () => void { + if (typeof eventTypeOrHandler === 'function') { + this._handlers.add(eventTypeOrHandler); + return () => this._handlers.delete(eventTypeOrHandler); + } + if (!handler) { + throw new Error(`Missing handler for ${eventTypeOrHandler}`); + } + let handlers = this._typedHandlers.get(eventTypeOrHandler); + if (!handlers) { + handlers = new Set(); + this._typedHandlers.set(eventTypeOrHandler, handlers); + } + const typedHandler = handler as (event: SessionEventPayload) => void; + handlers.add(typedHandler); + return () => handlers.delete(typedHandler); + } + + emit(event: SessionEventPayload): void { + const sessionEvent = event as SessionEvent; + for (const handler of this._handlers) { + handler(sessionEvent); + } + const typedEvent = event as SessionEventPayload; + for (const handler of this._typedHandlers.get(event.type) ?? []) { + handler(typedEvent); + } } async send(): Promise { return ''; } @@ -474,24 +502,25 @@ function createTestAgent(disposables: Pick, options?: { type CopilotCreateSessionOptions = Parameters[0]; -function createAgentSessionThroughAgent(agent: CopilotAgent, instantiationService: IInstantiationService): { readonly session: CopilotAgentSession; readonly createOptions: () => CopilotCreateSessionOptions | undefined } { +function createAgentSessionThroughAgent(agent: CopilotAgent, instantiationService: IInstantiationService, options?: { readonly mockSession?: MockCopilotSession; readonly activeClientToolSet?: ActiveClientToolSet; readonly snapshot?: IActiveClientSnapshot }): { readonly session: CopilotAgentSession; readonly createOptions: () => CopilotCreateSessionOptions | undefined } { const sessionUri = AgentSession.uri('copilotcli', 'test-session-1'); const shellManager = instantiationService.createInstance(ShellManager, sessionUri, undefined); let createOptions: CopilotCreateSessionOptions | undefined; + const mockSession = options?.mockSession ?? new MockCopilotSession(); const launchPlan: CopilotSessionLaunchPlan = { kind: 'create', client: { createSession: async options => { createOptions = options; - return new MockCopilotSession() as unknown as CopilotSession; + return mockSession as unknown as CopilotSession; }, - resumeSession: async () => new MockCopilotSession() as unknown as CopilotSession, + resumeSession: async () => mockSession as unknown as CopilotSession, }, - activeClientToolSet: new ActiveClientToolSet(), + activeClientToolSet: options?.activeClientToolSet ?? new ActiveClientToolSet(), sessionId: 'test-session-1', workingDirectory: undefined, resolvedAgentName: undefined, - snapshot: { tools: [], plugins: [], mcpServers: {} }, + snapshot: options?.snapshot ?? { tools: [], plugins: [], mcpServers: {} }, shellManager, githubToken: 'token', model: undefined, @@ -1266,6 +1295,96 @@ suite('CopilotAgent', () => { } }); + test('client tool call contributor prefers the message sender when it provides the tool', async () => { + const sessionDataService = disposables.add(new TestSessionDataService()); + const { agent, instantiationService } = createTestAgentContext(disposables, { environmentServiceRegistration: 'native', sessionDataService }); + const actions: (SessionAction | ChatAction)[] = []; + disposables.add(agent.onDidSessionProgress(signal => { + if (signal.kind === 'action') { + actions.push(signal.action); + } + })); + const activeClientToolSet = new ActiveClientToolSet(); + const sharedTool: ToolDefinition = { name: 'shared', description: 'Shared tool', inputSchema: { type: 'object', properties: {} } }; + activeClientToolSet.set('client-A', [sharedTool]); + activeClientToolSet.set('client-B', [sharedTool]); + const mockSession = new MockCopilotSession(); + const createdSession = createAgentSessionThroughAgent(agent, instantiationService, { + mockSession, + activeClientToolSet, + snapshot: { tools: activeClientToolSet.merged(), plugins: [], mcpServers: {} }, + }); + const agentSession = disposables.add(createdSession.session); + try { + await agentSession.initializeSession(); + agentSession.resetTurnState('turn-1', 'client-B'); + + mockSession.emit({ + type: 'tool.execution_start', + data: { toolCallId: 'tool-1', toolName: 'shared', arguments: {} }, + } as SessionEventPayload<'tool.execution_start'>); + + const toolStart = actions.find(action => action.type === ActionType.ChatToolCallStart); + assert.deepStrictEqual(toolStart?.type === ActionType.ChatToolCallStart ? toolStart.contributor : undefined, { + kind: ToolCallContributorKind.Client, + clientId: 'client-B', + }); + } finally { + await disposeAgent(agent); + } + }); + + test('client tool completion unblocks a pending permission request', async () => { + const sessionDataService = disposables.add(new TestSessionDataService()); + const { agent, instantiationService, fileService } = createTestAgentContext(disposables, { environmentServiceRegistration: 'native', sessionDataService }); + disposables.add(registerPendingEditContentProvider(fileService)); + const createdSession = createAgentSessionThroughAgent(agent, instantiationService); + const agentSession = disposables.add(createdSession.session); + const pendingEditContentUri = new DeferredPromise(); + disposables.add(agent.onDidSessionProgress(signal => { + if (signal.kind === 'pending_confirmation') { + const uri = signal.state.edits?.items[0]?.after?.content.uri; + if (uri) { + pendingEditContentUri.complete(URI.parse(uri)); + } + } + })); + try { + await agentSession.initializeSession(); + const onPermissionRequest = createdSession.createOptions()?.onPermissionRequest; + assert.ok(onPermissionRequest); + + const permissionRequestResult = onPermissionRequest({ + kind: 'write', + toolCallId: 'tool-1', + canOfferSessionApproval: false, + diff: '--- a/file.txt\n+++ b/file.txt\n@@ -0,0 +1 @@\n+after', + fileName: URI.file('/workspace/file.txt').fsPath, + intention: 'write file', + newFileContents: 'after', + }, { sessionId: 'test-session-1' }); + const editContentUri = await pendingEditContentUri.p; + + agentSession.handleClientToolCallComplete('tool-1', { + success: false, + pastTenseMessage: 'Client tool failed', + content: [{ type: ToolResultContentType.Text, text: 'failed before approval' }], + error: { message: 'failed before approval' }, + }); + await timeout(0); + + assert.deepStrictEqual({ + permissionResult: await permissionRequestResult, + pendingEditContentExists: await fileService.exists(editContentUri), + }, { + permissionResult: { kind: 'approve-once' }, + pendingEditContentExists: false, + }); + } finally { + await disposeAgent(agent); + } + }); + test('listSessions only returns sessions with a database', async () => { const sessionDataService = disposables.add(new TestSessionDataService()); const ownedSession = AgentSession.uri('copilotcli', 'owned'); diff --git a/src/vs/platform/agentHost/test/node/mockAgent.ts b/src/vs/platform/agentHost/test/node/mockAgent.ts index 8165ea534324e3..862a8e39deb06d 100644 --- a/src/vs/platform/agentHost/test/node/mockAgent.ts +++ b/src/vs/platform/agentHost/test/node/mockAgent.ts @@ -37,6 +37,7 @@ interface IMockSendMessageCall { readonly prompt: string; readonly attachments?: readonly MessageAttachment[]; readonly chat?: URI; + readonly senderClientId?: string; } /** @@ -133,8 +134,8 @@ export class MockAgent implements IAgent { return { items: [] }; } - async sendMessage(session: URI, chat: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string): Promise { - const call = { session, prompt, attachments, chat }; + async sendMessage(session: URI, chat: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string): Promise { + const call = { session, prompt, attachments, chat, ...(senderClientId ? { senderClientId } : {}) }; this.sendMessageCalls.push(call); this._onDidSendMessage.fire(call); if (turnId) { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts index 9aa8cf0e3800bc..66ca585e3ce450 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts @@ -2278,25 +2278,29 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC if (cts.token.isCancellationRequested) { return; } - if (!approvedDispatched) { - if (err !== undefined && !isCancellationError(err)) { - this._logService.warn(`[AgentHost] Client tool rejected pre-execution: ${toolName}`, err); - } - return; - } + if (err !== undefined) { if (!isCancellationError(err)) { - this._logService.warn(`[AgentHost] Client tool invocation failed: ${toolName}`, err); + if (!approvedDispatched) { + this._logService.warn(`[AgentHost] Client tool rejected pre-execution: ${toolName}`, err); + } else { + this._logService.warn(`[AgentHost] Client tool invocation failed: ${toolName}`, err); + } } - const message = err instanceof Error ? err.message : String(err); - result = { content: [], toolResultError: message }; + + result = { content: [], toolResultError: err instanceof Error ? err.message : String(err) }; + } + + const protocolToolCall = part$.get().toolCall; + const isProtocolToolCallComplete = protocolToolCall.status === ToolCallStatus.Completed || protocolToolCall.status === ToolCallStatus.Cancelled; + if (!isProtocolToolCallComplete) { + this._dispatchAction(opts.backendSession, { + type: ActionType.ChatToolCallComplete, + turnId: opts.turnId, + toolCallId, + result: toolResultToProtocol(result ?? { content: [] }, toolName), + }, opts.chatURI); } - this._dispatchAction(opts.backendSession, { - type: ActionType.ChatToolCallComplete, - turnId: opts.turnId, - toolCallId, - result: toolResultToProtocol(result ?? { content: [] }, toolName), - }, opts.chatURI); }; // React to part$ updates: route external cancellation, and try to diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostClientTools.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostClientTools.test.ts index 41f8c5dd721dc2..7f085a925f7443 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostClientTools.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostClientTools.test.ts @@ -7,6 +7,7 @@ import assert from 'assert'; import { timeout } from '../../../../../../base/common/async.js'; import { VSBuffer } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { DisposableStore, IReference, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -241,7 +242,7 @@ suite('AgentHostClientTools', () => { suite('client tools registration', () => { - function createMockToolsService(disposables: DisposableStore, tools: IToolData[], options?: { requireConfirmation?: boolean }) { + function createMockToolsService(disposables: DisposableStore, tools: IToolData[], options?: { requireConfirmation?: boolean; throwBeforeConfirmation?: Error }) { const onDidChangeTools = disposables.add(new Emitter()); const pendingToolCalls = new Map(); const begunToolCalls: ChatToolInvocation[] = []; @@ -260,6 +261,9 @@ suite('AgentHostClientTools', () => { invokedToolCalls.push(invocation); const toolInvocation = pendingToolCalls.get(invocation.chatStreamToolCallId ?? invocation.callId); pendingToolCalls.delete(invocation.chatStreamToolCallId ?? invocation.callId); + if (options?.throwBeforeConfirmation) { + throw options.throwBeforeConfirmation; + } if (options?.requireConfirmation && toolInvocation) { toolInvocation.transitionFromStreaming({ invocationMessage: 'Run Task', @@ -418,7 +422,7 @@ suite('AgentHostClientTools', () => { function createHandlerWithMocks( disposables: DisposableStore, tools: IToolData[], - toolServiceOptions?: { requireConfirmation?: boolean }, + toolServiceOptions?: { requireConfirmation?: boolean; throwBeforeConfirmation?: Error }, ) { const instantiationService = disposables.add(new TestInstantiationService()); const connection = new MockAgentHostConnection(); @@ -554,6 +558,63 @@ suite('AgentHostClientTools', () => { source: ToolDataSource.Internal, }; + async function provideSessionWithReadyRunTaskTool(handler: AgentHostSessionHandler, connection: MockAgentHostConnection): Promise { + const sessionResource = URI.parse('agent-host-copilot:/session-1'); + const backendSession = AgentSession.uri('copilot', 'session-1').toString(); + + connection.applySessionAction(URI.parse(buildDefaultChatUri(backendSession)), { + type: ActionType.ChatTurnStarted, + turnId: 'turn-1', + message: { text: 'run the task', origin: { kind: MessageKind.User } }, + } as ChatAction); + connection.applySessionAction(URI.parse(buildDefaultChatUri(backendSession)), { + type: ActionType.ChatToolCallStart, + turnId: 'turn-1', + toolCallId: 'tool-call-1', + toolName: 'runTask', + displayName: 'Run Task', + contributor: { kind: ToolCallContributorKind.Client, clientId: connection.clientId }, + } as ChatAction); + connection.applySessionAction(URI.parse(buildDefaultChatUri(backendSession)), { + type: ActionType.ChatToolCallReady, + turnId: 'turn-1', + toolCallId: 'tool-call-1', + invocationMessage: 'Run Task', + toolInput: '{"task":"build"}', + confirmationTitle: 'Run Task', + } as ChatAction); + + await handler.provideChatSessionContent(sessionResource, CancellationToken.None); + await timeout(0); + await timeout(0); + } + + function getToolCallConfirmationAndCompletionActions(connection: MockAgentHostConnection) { + return connection.dispatchedActions + .filter(entry => isChatAction(entry.action) + && (entry.action.type === ActionType.ChatToolCallConfirmed || entry.action.type === ActionType.ChatToolCallComplete) + && entry.action.toolCallId === 'tool-call-1') + .map(entry => { + if (entry.action.type === ActionType.ChatToolCallConfirmed) { + return { + type: entry.action.type, + approved: entry.action.approved, + success: undefined, + error: undefined, + }; + } + if (entry.action.type === ActionType.ChatToolCallComplete) { + return { + type: entry.action.type, + approved: undefined, + success: entry.action.result.success, + error: entry.action.result.error?.message, + }; + } + throw new Error(`Unexpected action type: ${entry.action.type}`); + }); + } + test('maps tool data to protocol definitions', async () => { const { connection } = createHandlerWithMocks(disposables, [testRunTestsTool, testRunTaskTool, testUnlistedTool]); @@ -627,6 +688,32 @@ suite('AgentHostClientTools', () => { && entry.action.toolCallId === 'tool-call-1')); }); + test('reports client tool prepare failures before confirmation as failed completion', async () => { + const { handler, connection } = createHandlerWithMocks(disposables, [testRunTaskTool], { throwBeforeConfirmation: new Error('prepare failed') }); + + await provideSessionWithReadyRunTaskTool(handler, connection); + + assert.deepStrictEqual(getToolCallConfirmationAndCompletionActions(connection), [{ + type: ActionType.ChatToolCallComplete, + approved: undefined, + success: false, + error: 'prepare failed', + }]); + }); + + test('reports client tool cancellation before confirmation as failed completion when protocol call is not terminal', async () => { + const { handler, connection } = createHandlerWithMocks(disposables, [testRunTaskTool], { throwBeforeConfirmation: new CancellationError() }); + + await provideSessionWithReadyRunTaskTool(handler, connection); + + assert.deepStrictEqual(getToolCallConfirmationAndCompletionActions(connection), [{ + type: ActionType.ChatToolCallComplete, + approved: undefined, + success: false, + error: 'Canceled', + }]); + }); + test('auto-approves client tool confirmation as a setting when the agent host marks the call', async () => { const { handler, connection } = createHandlerWithMocks(disposables, [testRunTaskTool], { requireConfirmation: true }); const sessionResource = URI.parse('agent-host-copilot:/session-1'); From 4034ee46add3168200bd976bc6a4b4976ba86ad4 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Mon, 29 Jun 2026 09:32:23 -0700 Subject: [PATCH 057/589] chat: support custom endpoint model options - Allows custom endpoint models to replace Copilot sampling defaults so providers with fixed parameter requirements can accept requests. - Supports explicit parameter omission, which lets model servers apply required defaults instead of receiving incompatible generated values. - Preserves explicit per-request options and applies the same behavior across Chat Completions, Responses, and Messages APIs. Fixes #321514 (Commit message generated by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- extensions/copilot/package.json | 24 ++++++++++++++++ .../src/extension/byok/common/byokProvider.ts | 4 ++- .../src/extension/byok/node/openAIEndpoint.ts | 28 +++++++++++++++++-- .../vscode-node/customEndpointProvider.ts | 4 ++- .../endpoint/common/endpointProvider.ts | 6 ++++ 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 2901b721d81d57..3b4bdeab13be21 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -2139,6 +2139,30 @@ "additionalProperties": { "type": "string" } + }, + "modelOptions": { + "type": "object", + "markdownDescription": "Sampling parameters to send with requests to this model. These override Copilot's defaults but are overridden by explicit per-request values. Set a property to `null` to omit it and use the model server's default.", + "properties": { + "temperature": { + "type": [ + "number", + "null" + ], + "minimum": 0, + "markdownDescription": "Sampling temperature. Set to `null` to omit the parameter." + }, + "top_p": { + "type": [ + "number", + "null" + ], + "minimum": 0, + "maximum": 1, + "markdownDescription": "Nucleus sampling probability. Set to `null` to omit the parameter." + } + }, + "additionalProperties": false } }, "required": [ diff --git a/extensions/copilot/src/extension/byok/common/byokProvider.ts b/extensions/copilot/src/extension/byok/common/byokProvider.ts index a966f2e63c18df..5427c6904d7644 100644 --- a/extensions/copilot/src/extension/byok/common/byokProvider.ts +++ b/extensions/copilot/src/extension/byok/common/byokProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { Disposable, LanguageModelChatInformation, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart } from 'vscode'; import { CopilotToken } from '../../../platform/authentication/common/copilotToken'; -import { EndpointEditToolName, IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; +import { EndpointEditToolName, IChatModelInformation, IChatModelRequestOptions, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; import { TokenizerType } from '../../../util/common/tokenizer'; export const enum BYOKAuthType { @@ -56,6 +56,7 @@ export interface BYOKModelCapabilities { streaming?: boolean; editTools?: EndpointEditToolName[]; requestHeaders?: Record; + modelOptions?: IChatModelRequestOptions; supportedEndpoints?: ModelSupportedEndpoint[]; zeroDataRetentionEnabled?: boolean; supportsReasoningEffort?: string[]; @@ -128,6 +129,7 @@ export function resolveModelInfo(modelId: string, providerName: string, knownMod model_picker_enabled: true, supported_endpoints: knownModelInfo?.supportedEndpoints, zeroDataRetentionEnabled: knownModelInfo?.zeroDataRetentionEnabled, + modelOptions: knownModelInfo?.modelOptions, reasoningEffortFormat: knownModelInfo?.reasoningEffortFormat }; if (knownModelInfo?.requestHeaders && Object.keys(knownModelInfo.requestHeaders).length > 0) { diff --git a/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts b/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts index 0cf838e3c235a4..aed5d1b64dd52f 100644 --- a/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts +++ b/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts @@ -265,10 +265,10 @@ export class OpenAIEndpoint extends ChatEndpoint { body.previous_response_id = undefined; } this._applyReasoningEffort(body, options); - return body; + return this._applyConfiguredModelOptions(body, options); } else if (this.useMessagesApi) { // Delegate to base ChatEndpoint for Messages API dispatch - return super.createRequestBody(options); + return this._applyConfiguredModelOptions(super.createRequestBody(options), options); } else { // Handle Chat Completions: provide callback for thinking data processing const supportsThinking = !!this.modelMetadata.capabilities.supports.thinking; @@ -290,8 +290,32 @@ export class OpenAIEndpoint extends ChatEndpoint { }; const body = createCapiRequestBody(options, this.model, callback); this._applyReasoningEffort(body, options); + return this._applyConfiguredModelOptions(body, options); + } + } + + private _applyConfiguredModelOptions(body: IEndpointBody, options: ICreateEndpointBodyOptions): IEndpointBody { + const modelOptions = this.modelMetadata.modelOptions; + if (!modelOptions) { return body; } + + for (const key of ['temperature', 'top_p'] as const) { + const requestValue = options.requestOptions?.[key]; + if (requestValue !== undefined) { + body[key] = requestValue; + continue; + } + + const configuredValue = modelOptions[key]; + if (configuredValue === null) { + delete body[key]; + } else if (configuredValue !== undefined) { + body[key] = configuredValue; + } + } + + return body; } /** diff --git a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts index db5df5f555f46f..ea5896d727974c 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts @@ -6,7 +6,7 @@ import { IChatMLFetcher } from '../../../platform/chat/common/chatMLFetcher'; import { IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IDomainService } from '../../../platform/endpoint/common/domainService'; -import { EndpointEditToolName, IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; +import { EndpointEditToolName, IChatModelInformation, IChatModelRequestOptions, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { IChatWebSocketManager } from '../../../platform/networking/node/chatWebSocketManager'; @@ -98,6 +98,7 @@ interface _CustomEndpointModelConfig { streaming?: boolean; editTools?: EndpointEditToolName[]; requestHeaders?: Record; + modelOptions?: IChatModelRequestOptions; zeroDataRetentionEnabled?: boolean; supportsReasoningEffort?: string[]; reasoningEffortFormat?: 'chat-completions' | 'responses'; @@ -159,6 +160,7 @@ export class CustomEndpointBYOKModelProvider extends AbstractOpenAICompatibleLMP thinking: modelConfiguration?.thinking ?? false, streaming: modelConfiguration?.streaming, requestHeaders: modelConfiguration?.requestHeaders, + modelOptions: modelConfiguration?.modelOptions, zeroDataRetentionEnabled: modelConfiguration?.zeroDataRetentionEnabled, supportsReasoningEffort: modelConfiguration?.supportsReasoningEffort, reasoningEffortFormat: modelConfiguration?.reasoningEffortFormat diff --git a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts index 647b162de87acd..4e18189e799fc4 100644 --- a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts +++ b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts @@ -19,6 +19,11 @@ export type CustomModel = { export type EndpointEditToolName = 'find-replace' | 'multi-find-replace' | 'apply-patch' | 'code-rewrite'; +export interface IChatModelRequestOptions { + temperature?: number | null; + top_p?: number | null; +} + const allEndpointEditToolNames: ReadonlySet = new Set([ 'find-replace', 'multi-find-replace', @@ -127,6 +132,7 @@ export type IChatModelInformation = IModelAPIResponse & { capabilities: IChatModelCapabilities; urlOrRequestMetadata?: string | RequestMetadata; requestHeaders?: Readonly>; + modelOptions?: Readonly; zeroDataRetentionEnabled?: boolean; /** * BYOK-only override that forces the body shape used when forwarding the reasoning effort to the model. From e3f25e9524f65ca9e357146bfe3b612d30733fe4 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Mon, 29 Jun 2026 09:32:34 -0700 Subject: [PATCH 058/589] chat: test custom endpoint model options - Prevents regressions where model-configured sampling values are lost before endpoint request construction. - Verifies omission and precedence behavior because providers may reject generated defaults even when they accept explicit request values. - Covers Chat Completions, Responses, and Messages so every supported custom endpoint API remains compatible. (Commit message generated by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../byok/common/test/byokProvider.spec.ts | 15 ++ .../test/customEndpointProvider.spec.ts | 175 ++++++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts b/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts index 8a78e8e0e3f7e1..caaaf0dbfad25c 100644 --- a/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts +++ b/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts @@ -71,6 +71,21 @@ describe('resolveModelInfo', () => { expect(info.capabilities.supports.reasoning_effort).toBeUndefined(); expect(info.reasoningEffortFormat).toBeUndefined(); }); + + it('propagates configured model options into chat-endpoint inputs', () => { + const info = resolveModelInfo('m1', 'TestProvider', undefined, { + ...baseCapabilities, + modelOptions: { + temperature: null, + top_p: 0.95, + }, + }); + + expect(info.modelOptions).toEqual({ + temperature: null, + top_p: 0.95, + }); + }); }); describe('isClientBYOKAllowed', () => { diff --git a/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts b/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts index 9a3197f855e508..0b62c122673477 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts @@ -6,6 +6,7 @@ import { Raw } from '@vscode/prompt-tsx'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { BlockedExtensionService, IBlockedExtensionService } from '../../../../platform/chat/common/blockedExtensionService'; +import { ChatLocation } from '../../../../platform/chat/common/commonTypes'; import { IChatModelInformation, ModelSupportedEndpoint } from '../../../../platform/endpoint/common/endpointProvider'; import { ITestingServicesAccessor } from '../../../../platform/test/node/services'; import { TokenizerType } from '../../../../util/common/tokenizer'; @@ -278,6 +279,180 @@ describe('CustomEndpointBYOKModelProvider', () => { expect(endpoint.ownsAuthorization).toBe(true); }); + it('issue #321514: applies configured model options over default sampling parameters', () => { + const metadata: IChatModelInformation = { + ...makeMetadata(undefined), + modelOptions: { + temperature: 1, + top_p: 0.95, + }, + }; + const endpoint = instaService.createInstance(CustomEndpointOAIEndpoint, + metadata, + 'test-api-key', + 'https://api.example.com/v1/chat/completions'); + const body = endpoint.createRequestBody({ + debugName: 'test', + messages: [{ + role: Raw.ChatRole.User, + content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] + }], + requestId: 'test-req-custom-model-options', + postOptions: { + temperature: 0.1, + top_p: 1, + stream: true, + }, + finishedCb: undefined, + location: ChatLocation.Other, + }); + + expect({ + temperature: body.temperature, + topP: body.top_p, + }).toEqual({ + temperature: 1, + topP: 0.95, + }); + }); + + it('omits sampling parameters configured as null', () => { + const metadata: IChatModelInformation = { + ...makeMetadata(undefined), + modelOptions: { + temperature: null, + top_p: null, + }, + }; + const endpoint = instaService.createInstance(CustomEndpointOAIEndpoint, + metadata, + 'test-api-key', + 'https://api.example.com/v1/chat/completions'); + const body = endpoint.createRequestBody({ + debugName: 'test', + messages: [{ + role: Raw.ChatRole.User, + content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] + }], + requestId: 'test-req-omitted-model-options', + postOptions: { + temperature: 0.1, + top_p: 1, + stream: true, + }, + finishedCb: undefined, + location: ChatLocation.Other, + }); + + expect({ + temperature: body.temperature, + topP: body.top_p, + }).toEqual({ + temperature: undefined, + topP: undefined, + }); + }); + + it('keeps explicit per-request sampling parameters ahead of configured model options', () => { + const metadata: IChatModelInformation = { + ...makeMetadata(undefined), + modelOptions: { + temperature: 1, + top_p: null, + }, + }; + const endpoint = instaService.createInstance(CustomEndpointOAIEndpoint, + metadata, + 'test-api-key', + 'https://api.example.com/v1/chat/completions'); + const body = endpoint.createRequestBody({ + debugName: 'test', + messages: [{ + role: Raw.ChatRole.User, + content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] + }], + requestId: 'test-req-explicit-model-options', + requestOptions: { + temperature: 0.7, + top_p: 0.9, + }, + postOptions: { + temperature: 0.1, + top_p: 1, + stream: true, + }, + finishedCb: undefined, + location: ChatLocation.Other, + }); + + expect({ + temperature: body.temperature, + topP: body.top_p, + }).toEqual({ + temperature: 0.7, + topP: 0.9, + }); + }); + + it('applies configured model options to Responses and Messages API bodies', () => { + const results = [ + { + supportedEndpoints: [ModelSupportedEndpoint.Responses], + url: 'https://api.example.com/v1/responses', + }, + { + supportedEndpoints: [ModelSupportedEndpoint.Messages], + url: 'https://api.example.com/v1/messages', + }, + ].map(({ supportedEndpoints, url }) => { + const metadata: IChatModelInformation = { + ...makeMetadata(supportedEndpoints), + modelOptions: { + temperature: 1, + top_p: 0.95, + }, + }; + const endpoint = instaService.createInstance(CustomEndpointOAIEndpoint, + metadata, + 'test-api-key', + url); + const body = endpoint.createRequestBody({ + debugName: 'test', + messages: [{ + role: Raw.ChatRole.User, + content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] + }], + requestId: `test-req-${endpoint.apiType}-model-options`, + postOptions: { + temperature: 0.1, + top_p: 1, + stream: true, + }, + finishedCb: undefined, + location: ChatLocation.Other, + }); + + return { + apiType: endpoint.apiType, + temperature: body.temperature, + topP: body.top_p, + }; + }); + + expect(results).toEqual([ + { + apiType: 'responses', + temperature: 1, + topP: 0.95, + }, + { + apiType: 'messages', + temperature: 1, + topP: 0.95, + }, + ]); + }); + it('replaces default Bearer with user-supplied Authorization header on Chat Completions endpoints', () => { const metadata = makeMetadata(undefined); metadata.requestHeaders = { 'Authorization': 'Bearer user-token' }; From ac174e660b107504de23376adab3414274abecca Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 29 Jun 2026 18:28:25 +0100 Subject: [PATCH 059/589] customization UI: polish source folder pickers (#323527) * customization UI: polish source folder pickers * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * update * update --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../copilotCLICustomizationProvider.ts | 6 +- .../claudeCustomizationProvider.ts | 6 +- .../copilot/sessionCustomizationDiscovery.ts | 24 ++-- .../api/browser/mainThreadChatAgents2.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostChatAgents2.ts | 1 + .../agentCustomizationItemProvider.ts | 4 + .../agentHost/agentHostChatContribution.ts | 2 +- .../aiCustomizationListWidget.ts | 12 +- .../aiCustomizationManagementEditor.ts | 103 ++++-------------- .../customizationCreatorService.ts | 69 +++++++----- ...promptsServiceCustomizationItemProvider.ts | 1 + .../pickers/askForPromptSourceFolder.ts | 2 +- .../common/customizationHarnessService.ts | 2 + ...osed.chatSessionCustomizationProvider.d.ts | 2 + 15 files changed, 101 insertions(+), 135 deletions(-) diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts index 86e5c373287e37..f2fcb50255496d 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts @@ -92,7 +92,8 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod if (root.type === type) { folders.push({ uri: URI.joinPath(folder, ...root.path), - label: root.path.join('/'), + label: root.path[0], + source: 'local' }); } } @@ -101,7 +102,8 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod if (root.type === type) { folders.push({ uri: URI.joinPath(this.envService.userHome, ...root.path), - label: `~/${root.path.join('/')}`, + label: `~/${root.path[0]}`, + source: 'user' }); } } diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/claudeCustomizationProvider.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/claudeCustomizationProvider.ts index e324defc723318..e1f9222abbee42 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/claudeCustomizationProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/claudeCustomizationProvider.ts @@ -177,7 +177,8 @@ export class ClaudeCustomizationProvider extends Disposable implements vscode.Ch if (root.type === type) { folders.push({ uri: URI.joinPath(folder, ...root.path), - label: root.path.join('/'), + label: root.path[0], + source: 'local' }); } } @@ -186,7 +187,8 @@ export class ClaudeCustomizationProvider extends Disposable implements vscode.Ch if (root.type === type) { folders.push({ uri: URI.joinPath(this.envService.userHome, ...root.path), - label: `~/${root.path.join('/')}`, + label: `~/${root.path[0]}`, + source: 'user' }); } } diff --git a/src/vs/platform/agentHost/node/copilot/sessionCustomizationDiscovery.ts b/src/vs/platform/agentHost/node/copilot/sessionCustomizationDiscovery.ts index 1c1b2e14508e11..dbe57c2b5917df 100644 --- a/src/vs/platform/agentHost/node/copilot/sessionCustomizationDiscovery.ts +++ b/src/vs/platform/agentHost/node/copilot/sessionCustomizationDiscovery.ts @@ -132,21 +132,21 @@ interface IFixedDiscoveryFile { */ const searchRoots: { workspace: ISearchRoot[]; user: ISearchRoot[] } = { workspace: [ - { path: ['.github', 'agents'], type: DiscoveredType.Agent, name: '.github/agents' }, - { path: ['.agents', 'agents'], type: DiscoveredType.Agent, name: '.agents/agents' }, - { path: ['.claude', 'agents'], type: DiscoveredType.Agent, name: '.claude/agents' }, - { path: ['.github', 'skills'], recursive: true, type: DiscoveredType.Skill, name: '.github/skills' }, - { path: ['.agents', 'skills'], recursive: true, type: DiscoveredType.Skill, name: '.agents/skills' }, - { path: ['.claude', 'skills'], recursive: true, type: DiscoveredType.Skill, name: '.claude/skills' }, - { path: ['.github', 'instructions'], recursive: true, type: DiscoveredType.Instruction, name: '.github/instructions' }, - { path: ['.github', 'hooks'], recursive: true, type: DiscoveredType.Hook, name: '.github/hooks' }, + { path: ['.github', 'agents'], type: DiscoveredType.Agent, name: '.github' }, + { path: ['.agents', 'agents'], type: DiscoveredType.Agent, name: '.agents' }, + { path: ['.claude', 'agents'], type: DiscoveredType.Agent, name: '.claude' }, + { path: ['.github', 'skills'], recursive: true, type: DiscoveredType.Skill, name: '.github' }, + { path: ['.agents', 'skills'], recursive: true, type: DiscoveredType.Skill, name: '.agents' }, + { path: ['.claude', 'skills'], recursive: true, type: DiscoveredType.Skill, name: '.claude' }, + { path: ['.github', 'instructions'], recursive: true, type: DiscoveredType.Instruction, name: '.github' }, + { path: ['.github', 'hooks'], recursive: true, type: DiscoveredType.Hook, name: '.github' }, ], user: [ - { path: ['.copilot', 'agents'], type: DiscoveredType.Agent, name: '~/.copilot/agents' }, - { path: ['.agents', 'skills'], recursive: true, type: DiscoveredType.Skill, name: '~/.agents/skills' }, - { path: ['.copilot', 'instructions'], recursive: true, type: DiscoveredType.Instruction, name: '~/.copilot/instructions' }, - { path: ['.copilot', 'hooks'], recursive: true, type: DiscoveredType.Hook, name: '~/.copilot/hooks' }, + { path: ['.copilot', 'agents'], type: DiscoveredType.Agent, name: '~/.copilot' }, + { path: ['.agents', 'skills'], recursive: true, type: DiscoveredType.Skill, name: '~/.agents' }, + { path: ['.copilot', 'instructions'], recursive: true, type: DiscoveredType.Instruction, name: '~/.copilot' }, + { path: ['.copilot', 'hooks'], recursive: true, type: DiscoveredType.Hook, name: '~/.copilot' }, ], }; diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 2ea8dcb67d92d3..1faa3281d250ae 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -809,6 +809,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA return folders.map(folder => ({ uri: URI.revive(folder.uri), label: folder.label, + source: folder.source, })); }, }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ea07afb270ac8c..b2dbb67ab644c7 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1828,6 +1828,7 @@ export interface IChatSessionCustomizationItemDto { export interface IChatSessionCustomizationSourceFolderDto { readonly uri: UriComponents; readonly label: string; + readonly source: IChatResourceSourceDto; } export interface IChatParticipantMetadata { participant: string; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 9bcbfff380fcf6..5fcf02a0741cbe 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -875,6 +875,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return folders.map(folder => ({ uri: folder.uri, label: folder.label, + source: folder.source, } satisfies IChatSessionCustomizationSourceFolderDto)); } catch (err) { return undefined; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts index dcb81fd7bc3dfd..905de11c728735 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts @@ -137,6 +137,8 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto } async provideSourceFolders(sessionResource: URI, type: PromptsType, _token: CancellationToken): Promise { + const workingDirectory = this._customAgentsService.getWorkingDirectory(sessionResource); + const folders: ICustomizationSourceFolder[] = []; for (const customization of this._customAgentsService.getCustomizations(sessionResource)) { if (!isDirectoryCustomization(customization) || !customization.writable) { @@ -145,9 +147,11 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto if (toPromptsType(customization.contents) !== type) { continue; } + const source = workingDirectory && customization.uri.startsWith(workingDirectory + '/') ? AICustomizationSources.local : AICustomizationSources.user; folders.push({ uri: this.toRemoteUri(customization.uri), label: customization.name, + source, }); } return folders; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts index 810fbfd5d28f43..0ec64a45aa5cb6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts @@ -249,7 +249,7 @@ export class AgentHostContribution extends Disposable implements IWorkbenchContr label: localize('agentHostHarnessLabel.local', "{0} [Agent Host]", agent.displayName), icon: ThemeIcon.fromId(Codicon.server.id), // The Tools section is surfaced for the Copilot CLI agent host only. - hiddenSections: agent.provider === 'copilotcli' ? [] : [AICustomizationManagementSection.Tools], + hiddenSections: agent.provider === 'copilotcli' ? [AICustomizationManagementSection.Prompts] : [AICustomizationManagementSection.Tools, AICustomizationManagementSection.Prompts], hideGenerateButton: true, syncProvider, itemProvider, diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 58d6eb8564433d..07fa2afddcf466 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -616,8 +616,8 @@ export class AICustomizationListWidget extends Disposable { private readonly _onDidRequestCreate = this._register(new Emitter()); readonly onDidRequestCreate: Event = this._onDidRequestCreate.event; - private readonly _onDidRequestCreateManual = this._register(new Emitter<{ type: PromptsType; target: 'workspace' | 'user' | 'workspace-root'; rootFileName?: string }>()); - readonly onDidRequestCreateManual: Event<{ type: PromptsType; target: 'workspace' | 'user' | 'workspace-root'; rootFileName?: string }> = this._onDidRequestCreateManual.event; + private readonly _onDidRequestCreateManual = this._register(new Emitter<{ type: PromptsType; target: 'local' | 'user' | 'workspace-root'; rootFileName?: string }>()); + readonly onDidRequestCreateManual: Event<{ type: PromptsType; target: 'local' | 'user' | 'workspace-root'; rootFileName?: string }> = this._onDidRequestCreateManual.event; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -1098,7 +1098,7 @@ export class AICustomizationListWidget extends Disposable { actions.push({ label: `$(${Codicon.add.id}) ${localize('configureHooks', "Configure Hooks")}`, enabled: true, - run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); }, + run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'local' }); }, }); } } else if (!override?.commandId) { @@ -1107,7 +1107,7 @@ export class AICustomizationListWidget extends Disposable { label: `$(${Codicon.add.id}) ${localize('configureHooks', "Configure Hooks")}`, enabled: hasWorkspace, tooltip: hasWorkspace ? undefined : localize('configureHooksDisabled', "Open a workspace folder to configure hooks."), - run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); }, + run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'local' }); }, }); } return actions; @@ -1129,7 +1129,7 @@ export class AICustomizationListWidget extends Disposable { actions.push({ label: `$(${Codicon.add.id}) New ${createTypeLabel} (Workspace)`, enabled: true, - run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); }, + run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'local' }); }, }); addedTargets.add('workspace'); } else { @@ -1148,7 +1148,7 @@ export class AICustomizationListWidget extends Disposable { actions.push({ label: `$(${Codicon.folder.id}) New ${createTypeLabel} (Workspace)`, enabled: true, - run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); }, + run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'local' }); }, }); } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 95251decf6c5eb..20fe87b58a8f78 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -33,7 +33,7 @@ import { IListVirtualDelegate, IListRenderer } from '../../../../../base/browser import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; -import { basename, dirname, isEqual, isEqualOrParent } from '../../../../../base/common/resources.js'; +import { basename, dirname, isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js'; import { AICustomizationListWidget } from './aiCustomizationListWidget.js'; @@ -65,7 +65,7 @@ import { AGENT_MD_FILENAME } from '../../common/promptSyntax/config/promptFileLo import { getAttributeDefinition, getTarget } from '../../common/promptSyntax/languageProviders/promptFileAttributes.js'; import { INewPromptOptions, NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID, NEW_SKILL_COMMAND_ID } from '../promptSyntax/newPromptFileActions.js'; import { showConfigureHooksQuickPick } from '../promptSyntax/hookActions.js'; -import { resolveWorkspaceTargetDirectory, resolveUserTargetDirectory } from './customizationCreatorService.js'; +import { resolveWorkspaceTargetDirectory, resolveUserTargetDirectory, CustomizationLocationPicker } from './customizationCreatorService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { AICustomizationSources, IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; @@ -88,12 +88,11 @@ import { IExtension } from '../../../extensions/common/extensions.js'; import { EmbeddedMcpServerDetail } from './embeddedMcpServerDetail.js'; import { EmbeddedAgentPluginDetail } from './embeddedAgentPluginDetail.js'; import { EmbeddedExtensionToolsDetail } from './embeddedExtensionToolsDetail.js'; -import { ICustomizationHarnessService, ICustomizationSourceFolder } from '../../common/customizationHarnessService.js'; +import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js'; import { ChatConfiguration } from '../../common/constants.js'; import { AICustomizationWelcomePage } from './aiCustomizationWelcomePage.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { ResourceSet } from '../../../../../base/common/map.js'; -import { PromptsServiceCustomizationItemProvider } from './promptsServiceCustomizationItemProvider.js'; +import { showNoFoldersDialog } from '../promptSyntax/pickers/askForPromptSourceFolder.js'; const $ = DOM.$; @@ -1125,7 +1124,7 @@ export class AICustomizationManagementEditor extends EditorPane { /** * Creates a new prompt file and opens it in the embedded editor. */ - private async createNewItemManual(type: PromptsType, target: 'workspace' | 'user' | 'workspace-root', rootFileName?: string): Promise { + private async createNewItemManual(type: PromptsType, target: 'local' | 'user' | 'workspace-root', rootFileName?: string): Promise { this.telemetryService.publicLog2('chatCustomizationEditor.createItem', { section: this.selectedSection ?? 'welcome', promptType: type, @@ -1176,15 +1175,23 @@ export class AICustomizationManagementEditor extends EditorPane { } return; } - - const targetDir = await this.resolveTargetDirectoryWithPicker(type, target); + const sessionResource = this.harnessService.activeSessionResource.get(); + const picker = this.instantiationService.createInstance(CustomizationLocationPicker); + const targetDir = await picker.resolveTargetDirectoryWithPicker( + sessionResource, + type, + target, + ); if (targetDir === null) { return; // User cancelled the picker } - // targetDir may be undefined when no matching folder exists for the - // requested storage type (e.g. skills have no user-storage folder). - // Pass it through — the command handles undefined by showing its own - // folder picker via askForPromptSourceFolder. + + if (targetDir === undefined) { + // targetDir may be undefined when no matching folder exists for the + // requested storage type (e.g. skills have no user-storage folder). + await this.instantiationService.invokeFunction(showNoFoldersDialog, type); + return; + } // When the active harness overrides the file extension (e.g. Claude // rules use .md instead of .instructions.md), pass it through so the @@ -1193,11 +1200,11 @@ export class AICustomizationManagementEditor extends EditorPane { const options: INewPromptOptions = { targetFolder: targetDir, - targetStorage: target === 'user' ? PromptsStorage.user : PromptsStorage.local, + targetStorage: target === AICustomizationSources.user ? PromptsStorage.user : PromptsStorage.local, fileExtension: override?.fileExtension, openFile: async (uri) => { - const isWorkspace = target === 'workspace'; - await this.showEmbeddedEditor(uri, basename(uri), type, target === 'user' ? PromptsStorage.user : PromptsStorage.local, isWorkspace); + const isWorkspace = target === AICustomizationSources.local; + await this.showEmbeddedEditor(uri, basename(uri), type, target, isWorkspace); return this.embeddedEditor; }, }; @@ -1215,72 +1222,6 @@ export class AICustomizationManagementEditor extends EditorPane { this.listWidget.refresh(); } - /** - * Resolves the target directory for creating a new customization file. - * If multiple source folders exist for the given storage type, shows a - * picker to let the user choose. Otherwise, returns the single match. - * - * Source folders come from the active harness's item provider (via the - * items model) — each session can supply its own set of customization - * locations through `ICustomizationItemProvider.provideSourceFolders`. - * - * @returns the resolved URI, `undefined` when no folder is available, - * or `null` when the user cancelled the picker. - */ - private async resolveTargetDirectoryWithPicker(type: PromptsType, target: 'workspace' | 'user'): Promise { - const sessionResource = this.harnessService.activeSessionResource.get(); - const activeDescriptor = this.harnessService.getActiveDescriptor(); - const provider = activeDescriptor.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider); - if (!provider.provideSourceFolders) { - return undefined; - } - const allFolders = await provider.provideSourceFolders(sessionResource, type, CancellationToken.None); - if (!allFolders) { - // Provider returned no source folders for this type/session. - return undefined; - } - - const projectRoot = this.workspaceService.getActiveProjectRoot(); - const matchingFolders: ICustomizationSourceFolder[] = []; - const hasSeen = new ResourceSet(); - for (const f of allFolders) { - if (target === 'workspace') { - if (projectRoot && isEqualOrParent(f.uri, projectRoot) && !hasSeen.has(f.uri)) { - hasSeen.add(f.uri); - matchingFolders.push(f); - } - } else { - if ((!projectRoot || !isEqualOrParent(f.uri, projectRoot)) && !hasSeen.has(f.uri)) { - hasSeen.add(f.uri); - matchingFolders.push(f); - } - } - } - - if (matchingFolders.length === 0) { - // No matching folders — return undefined so the command can fall - // back to askForPromptSourceFolder (not null which means cancellation) - return undefined; - } - - if (matchingFolders.length === 1) { - return matchingFolders[0].uri; - } - - // Multiple directories — ask the user which one to use - const items: (IQuickPickItem & { uri: URI })[] = matchingFolders.map(folder => ({ - label: folder.label, - description: folder.uri.fsPath, - uri: folder.uri, - })); - - const picked = await this.quickInputService.pick(items, { - placeHolder: localize('selectTargetDirectory', "Select a directory for the new customization file"), - }); - - return picked?.uri ?? null; - } - override updateStyles(): void { // The modal provides its own panel chrome, so the split view separator // is intentionally hidden here regardless of theme. diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts index c9159a2a6b742f..3ff88b2360e218 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts @@ -11,15 +11,15 @@ import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { getPromptFileDefaultLocations } from '../../common/promptSyntax/config/promptFileLocations.js'; import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { URI } from '../../../../../base/common/uri.js'; -import { isEqualOrParent } from '../../../../../base/common/resources.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { localize } from '../../../../../nls.js'; -import { ICustomizationHarnessService, ICustomizationSourceFolder } from '../../common/customizationHarnessService.js'; -import { ResourceSet } from '../../../../../base/common/map.js'; +import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { PromptsServiceCustomizationItemProvider } from './promptsServiceCustomizationItemProvider.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { getChatSessionType } from '../../common/model/chatUri.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; /** * Service that opens an AI-guided chat session to help the user create @@ -38,12 +38,15 @@ export class CustomizationCreatorService { @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @IPromptsService private readonly promptsService: IPromptsService, @IQuickInputService private readonly quickInputService: IQuickInputService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService, - @IInstantiationService private readonly instantiationService: IInstantiationService ) { } async createWithAI(type: PromptsType): Promise { + const currentSessionResource = this.harnessService.activeSessionResource.get(); + + // Ask for the name before entering chat const typeLabel = getTypeLabel(type); const name = await this.quickInputService.input({ @@ -67,7 +70,12 @@ export class CustomizationCreatorService { // directory and have those changes tracked. // Capture project root BEFORE opening new chat (which may change active session) - const targetDir = await this.resolveTargetDirectoryWithPicker(type); + const picker = this.instantiationService.createInstance(CustomizationLocationPicker); + const targetDir = await picker.resolveTargetDirectoryWithPicker( + currentSessionResource, + type, + 'local', + ); if (targetDir === null) { return; // User cancelled the picker } @@ -108,6 +116,23 @@ export class CustomizationCreatorService { return resolveWorkspaceTargetDirectory(this.workspaceService, type); } + /** + * Resolves the user-level directory for a new customization file. + */ + async resolveUserDirectory(type: PromptsType): Promise { + return resolveUserTargetDirectory(this.promptsService, type); + } +} + + +export class CustomizationLocationPicker { + constructor( + @IQuickInputService private readonly quickInputService: IQuickInputService, + @ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILabelService private readonly labelService: ILabelService + ) { } + /** * Resolves the target directory for creating a new customization file. * If multiple source folders exist for the given storage type, shows a @@ -120,10 +145,10 @@ export class CustomizationCreatorService { * @returns the resolved URI, `undefined` when no folder is available, * or `null` when the user cancelled the picker. */ - private async resolveTargetDirectoryWithPicker(type: PromptsType): Promise { - const sessionResource = this.harnessService.activeSessionResource.get(); - const activeDescriptor = this.harnessService.getActiveDescriptor(); - const provider = activeDescriptor.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider); + public async resolveTargetDirectoryWithPicker(sessionResource: URI, type: PromptsType, target: 'local' | 'user'): Promise { + const sessionType = getChatSessionType(sessionResource); + const descriptor = this.harnessService.findHarnessById(sessionType); + const provider = descriptor?.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider); if (!provider.provideSourceFolders) { return undefined; } @@ -133,30 +158,21 @@ export class CustomizationCreatorService { return undefined; } - const projectRoot = this.workspaceService.getActiveProjectRoot(); - const matchingFolders: ICustomizationSourceFolder[] = []; - const hasSeen = new ResourceSet(); - for (const f of allFolders) { - if (projectRoot && isEqualOrParent(f.uri, projectRoot) && !hasSeen.has(f.uri)) { - hasSeen.add(f.uri); - matchingFolders.push(f); - } - } - + const matchingFolders = allFolders.filter(f => f.source === target); if (matchingFolders.length === 0) { // No matching folders — return undefined so the command can fall // back to askForPromptSourceFolder (not null which means cancellation) return undefined; } - if (matchingFolders.length === 1) { - return matchingFolders[0].uri; - } + // if (matchingFolders.length === 1) { + // return matchingFolders[0].uri; + // } // Multiple directories — ask the user which one to use const items: (IQuickPickItem & { uri: URI })[] = matchingFolders.map(folder => ({ label: folder.label, - description: folder.uri.fsPath, + description: this.labelService.getUriLabel(folder.uri, { relative: true }), uri: folder.uri, })); @@ -166,13 +182,6 @@ export class CustomizationCreatorService { return picked?.uri ?? null; } - - /** - * Resolves the user-level directory for a new customization file. - */ - async resolveUserDirectory(type: PromptsType): Promise { - return resolveUserTargetDirectory(this.promptsService, type); - } } /** diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts index f4fa4d86c56a49..9271f94cc589d9 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts @@ -66,6 +66,7 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt return folders.map(folder => ({ uri: folder.uri, label: this.promptsService.getPromptLocationLabel(folder), + source: folder.storage })); } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/askForPromptSourceFolder.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/askForPromptSourceFolder.ts index ea8724e0e58b80..57134ef087d732 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/askForPromptSourceFolder.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/askForPromptSourceFolder.ts @@ -167,7 +167,7 @@ function getPlaceholderStringforMove(type: PromptsType, isMove: boolean): string * Note! this is a temporary solution and must be replaced with a dialog to select * a custom folder path, or switch to a different prompt type */ -async function showNoFoldersDialog(accessor: ServicesAccessor, type: PromptsType): Promise { +export async function showNoFoldersDialog(accessor: ServicesAccessor, type: PromptsType): Promise { const quickInputService = accessor.get(IQuickInputService); const openerService = accessor.get(IOpenerService); diff --git a/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts b/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts index c29ef8ad2df53b..b6017b1dedd74a 100644 --- a/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts +++ b/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts @@ -235,6 +235,8 @@ export interface ICustomizationSourceFolder { readonly uri: URI; /** Display label for the picker when multiple folders are offered. */ readonly label: string; + /** Customization source for this folder (typically 'local' or 'user' for writable creation locations). */ + readonly source: AICustomizationSource; } /** diff --git a/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts index 5851658fd5b6b7..66a6e102411c3e 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts @@ -201,6 +201,8 @@ declare module 'vscode' { readonly uri: Uri; /** Display label for the picker when multiple folders are offered. */ readonly label: string; + /** Source of the customization folder. */ + readonly source: ChatSessionCustomizationSource; } // #endregion From 9bcfbb4159dd4dc3b2f8e4e47b9414bd8bfd7d51 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Mon, 29 Jun 2026 18:29:37 +0100 Subject: [PATCH 060/589] Style overrides: Add tests and refactor edge ownership logic for layout service (#323546) * Improve layout service logic for determining floating panel owners * Refactor getFloatingOuterEdgeOwners to use consistent panel group variable naming and add unit tests for edge ownership across layouts * Add test case for horizontal panel visibility in edge ownership logic Co-authored-by: Copilot --------- Co-authored-by: mrleemurray Co-authored-by: Copilot --- .../services/layout/browser/layoutService.ts | 66 ++++++++++----- .../layout/test/browser/layoutService.test.ts | 82 +++++++++++++++++++ 2 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 src/vs/workbench/services/layout/test/browser/layoutService.test.ts diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 7f2ed1f7f7ebc5..5ad50ca90fdd31 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -10,7 +10,7 @@ import { Part } from '../../../browser/part.js'; import { IDimension } from '../../../../base/browser/dom.js'; import { Direction, IViewSize } from '../../../../base/browser/ui/grid/grid.js'; import { isMacintosh, isNative, isWeb } from '../../../../base/common/platform.js'; -import { isAuxiliaryWindow } from '../../../../base/browser/window.js'; +import { isAuxiliaryWindow, mainWindow } from '../../../../base/browser/window.js'; import { CustomTitleBarVisibility, TitleBarSetting, getMenuBarVisibility, hasCustomTitlebar, hasNativeMenu, hasNativeTitlebar } from '../../../../platform/window/common/window.js'; import { isFullscreen, isWCOEnabled } from '../../../../base/browser/browser.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -120,10 +120,11 @@ export function positionToString(position: Position): string { * * The horizontal order of the parts is reconstructed from the same inputs the grid * layout uses (mirrors `Layout.adjustPartPositions` in `src/vs/workbench/browser/layout.ts`): the activity bar and primary side bar sit - * on `getSideBarPosition()`, the secondary side bar on the opposite side, and a - * vertical (left/right) panel sits immediately next to the editor on its placement - * side. The outermost *visible* part on each edge wins; the activity bar is not a - * floating card, so it yields no owner. + * on `getSideBarPosition()`, the secondary side bar on the opposite side, the editor in + * the middle, and a vertical (left/right) panel immediately next to the editor on its + * placement side. The outermost *visible* part on each edge wins; the activity bar is not + * a floating card, so it yields no owner. A hidden editor is skipped, so a maximized side + * bar (which spans the full content width) is correctly detected as the owner on both edges. * * Consumed by `AbstractPaneCompositePart` (side bars and panel) and `EditorPart` * (main editor) so the doubled-gutter decision stays in sync between them. @@ -142,23 +143,51 @@ export function getFloatingOuterEdgeOwners(layoutService: IWorkbenchLayoutServic const panelInLeftSequence = verticalPanelVisible && panelPosition === Position.LEFT; const panelInRightSequence = verticalPanelVisible && panelPosition === Position.RIGHT; - // Parts that can sit at each window edge outside the editor, ordered outermost -> - // innermost. The editor is the innermost terminal owner (handled in the resolver). - const sideBarSideParts: SINGLE_WINDOW_PARTS[] = [Parts.ACTIVITYBAR_PART, Parts.SIDEBAR_PART]; - const auxSideParts: SINGLE_WINDOW_PARTS[] = [Parts.AUXILIARYBAR_PART]; - const panelParts: SINGLE_WINDOW_PARTS[] = [Parts.PANEL_PART]; - const leftOuterParts: SINGLE_WINDOW_PARTS[] = [...(sideBarLeft ? sideBarSideParts : auxSideParts), ...(panelInLeftSequence ? panelParts : [])]; - const rightOuterParts: SINGLE_WINDOW_PARTS[] = [...(sideBarLeft ? auxSideParts : sideBarSideParts), ...(panelInRightSequence ? panelParts : [])]; + // The full window order of the floatable parts, left -> right: the activity bar and + // primary side bar sit together on `getSideBarPosition()` (activity bar outermost), the + // secondary side bar on the opposite side, a vertical panel immediately beside the editor + // on its placement side, and the editor in the middle. Each edge is resolved by walking + // this order inward to the first *visible* card, so a hidden editor (e.g. a maximized side + // bar that spans the full content width) is skipped and the spanning card is detected on + // both edges. + const sideBarGroup: Parts[] = [Parts.ACTIVITYBAR_PART, Parts.SIDEBAR_PART]; + const panelGroup: Parts[] = [Parts.PANEL_PART]; + const fullOrder: Parts[] = sideBarLeft + ? [ + ...sideBarGroup, + ...(panelInLeftSequence ? panelGroup : []), + Parts.EDITOR_PART, + ...(panelInRightSequence ? panelGroup : []), + Parts.AUXILIARYBAR_PART + ] + : [ + Parts.AUXILIARYBAR_PART, + ...(panelInLeftSequence ? panelGroup : []), + Parts.EDITOR_PART, + ...(panelInRightSequence ? panelGroup : []), + ...[...sideBarGroup].reverse() // activity bar is outermost on the right edge + ]; return { - left: resolveFloatingOuterOwner(layoutService, leftOuterParts), - right: resolveFloatingOuterOwner(layoutService, rightOuterParts) + left: resolveFloatingOuterOwner(layoutService, fullOrder), + right: resolveFloatingOuterOwner(layoutService, [...fullOrder].reverse()) }; } -function resolveFloatingOuterOwner(layoutService: IWorkbenchLayoutService, outerParts: SINGLE_WINDOW_PARTS[]): Parts | undefined { - for (const part of outerParts) { - if (!layoutService.isVisible(part)) { +/** + * Walks the given window order (outermost -> innermost from a window edge) and returns the + * first visible part as the owner of that edge. The activity bar hugs the window edge but is + * not a floating card, so a visible activity bar yields no owner. Returns `undefined` when no + * visible card sits on the edge. + */ +function resolveFloatingOuterOwner(layoutService: IWorkbenchLayoutService, orderedParts: Parts[]): Parts | undefined { + for (const part of orderedParts) { + // The editor is the only multi-window part in this order; its main-window visibility + // is what matters for the main-window floating layout. + const visible = part === Parts.EDITOR_PART + ? layoutService.isVisible(Parts.EDITOR_PART, mainWindow) + : layoutService.isVisible(part as SINGLE_WINDOW_PARTS); + if (!visible) { continue; } @@ -166,8 +195,7 @@ function resolveFloatingOuterOwner(layoutService: IWorkbenchLayoutService, outer return part === Parts.ACTIVITYBAR_PART ? undefined : part; } - // Nothing else sits on this edge: the editor is the outermost (central) card. - return Parts.EDITOR_PART; + return undefined; } /** diff --git a/src/vs/workbench/services/layout/test/browser/layoutService.test.ts b/src/vs/workbench/services/layout/test/browser/layoutService.test.ts new file mode 100644 index 00000000000000..86eb29f83c1fd5 --- /dev/null +++ b/src/vs/workbench/services/layout/test/browser/layoutService.test.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { getFloatingOuterEdgeOwners, Parts, Position } from '../../browser/layoutService.js'; +import { TestLayoutService } from '../../../../test/browser/workbenchTestServices.js'; + +suite('LayoutService - getFloatingOuterEdgeOwners', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + class ConfigurableLayoutService extends TestLayoutService { + floatingPanelsEnabled = true; + sideBarPosition = Position.LEFT; + panelPosition = Position.BOTTOM; + visibleParts = new Set(); + + override isFloatingPanelsEnabled(): boolean { return this.floatingPanelsEnabled; } + override getSideBarPosition(): Position { return this.sideBarPosition; } + override getPanelPosition(): Position { return this.panelPosition; } + override isVisible(part: Parts): boolean { return this.visibleParts.has(part); } + } + + function owners(configure: (service: ConfigurableLayoutService) => void): { left: Parts | undefined; right: Parts | undefined } { + const service = new ConfigurableLayoutService(); + configure(service); + return getFloatingOuterEdgeOwners(service); + } + + test('edge ownership across layouts', () => { + const actual = { + // Experiment disabled: no owners regardless of layout. + disabled: owners(s => { s.floatingPanelsEnabled = false; s.visibleParts = new Set([Parts.AUXILIARYBAR_PART]); }), + + // Default full layout (side bar left): activity bar hugs the left edge (no owner), + // the secondary side bar owns the right edge. + defaultFull: owners(s => { s.visibleParts = new Set([Parts.ACTIVITYBAR_PART, Parts.SIDEBAR_PART, Parts.EDITOR_PART, Parts.AUXILIARYBAR_PART]); }), + + // Maximized aux bar with the activity bar in its default (visible) position: the + // activity bar still hugs the left edge, the aux bar owns the right edge. + maximizedAuxWithActivityBar: owners(s => { s.visibleParts = new Set([Parts.ACTIVITYBAR_PART, Parts.AUXILIARYBAR_PART]); }), + + // Maximized aux bar with the activity bar not in its default position (hidden from + // the side column): the aux bar spans the full width and owns both edges. + maximizedAuxNoActivityBar: owners(s => { s.visibleParts = new Set([Parts.AUXILIARYBAR_PART]); }), + + // Same, but the side bar is on the right: the aux bar still spans and owns both edges. + maximizedAuxNoActivityBarSideBarRight: owners(s => { s.sideBarPosition = Position.RIGHT; s.visibleParts = new Set([Parts.AUXILIARYBAR_PART]); }), + + // Only the editor visible with the activity bar hidden: the editor is the sole card + // and owns both edges. + editorOnly: owners(s => { s.visibleParts = new Set([Parts.EDITOR_PART]); }), + + // Full layout with a visible left vertical panel: the panel sits between the editor + // and the side bar, so it never reaches an edge. + verticalPanelFull: owners(s => { s.panelPosition = Position.LEFT; s.visibleParts = new Set([Parts.ACTIVITYBAR_PART, Parts.SIDEBAR_PART, Parts.PANEL_PART, Parts.EDITOR_PART, Parts.AUXILIARYBAR_PART]); }), + + // Maximized left vertical panel with the activity bar hidden: the panel spans the + // full width and owns both edges. + maximizedVerticalPanel: owners(s => { s.panelPosition = Position.LEFT; s.visibleParts = new Set([Parts.PANEL_PART]); }), + + // Visible horizontal (bottom) panel: not part of the vertical order, so it owns no + // edge; the secondary side bar still owns the right edge. + horizontalPanelVisible: owners(s => { s.panelPosition = Position.BOTTOM; s.visibleParts = new Set([Parts.SIDEBAR_PART, Parts.EDITOR_PART, Parts.PANEL_PART, Parts.AUXILIARYBAR_PART]); }), + }; + + assert.deepStrictEqual(actual, { + disabled: { left: undefined, right: undefined }, + defaultFull: { left: undefined, right: Parts.AUXILIARYBAR_PART }, + maximizedAuxWithActivityBar: { left: undefined, right: Parts.AUXILIARYBAR_PART }, + maximizedAuxNoActivityBar: { left: Parts.AUXILIARYBAR_PART, right: Parts.AUXILIARYBAR_PART }, + maximizedAuxNoActivityBarSideBarRight: { left: Parts.AUXILIARYBAR_PART, right: Parts.AUXILIARYBAR_PART }, + editorOnly: { left: Parts.EDITOR_PART, right: Parts.EDITOR_PART }, + verticalPanelFull: { left: undefined, right: Parts.AUXILIARYBAR_PART }, + maximizedVerticalPanel: { left: Parts.PANEL_PART, right: Parts.PANEL_PART }, + horizontalPanelVisible: { left: Parts.SIDEBAR_PART, right: Parts.AUXILIARYBAR_PART }, + }); + }); +}); From fd5d846c3fe6d6cbc5af0e580cc526e320e5778f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 30 Jun 2026 04:07:21 +1000 Subject: [PATCH 061/589] Enhance agent handling: rebase selected agent on worktree relocation and restore from draft on session resume (#323548) * Enhance agent handling: rebase selected agent on worktree relocation and restore from draft on session resume * Refactor agent hydration logic to ensure one-shot observation and prevent leaks or overrides --- .../browser/baseAgentHostSessionsProvider.ts | 178 +++++++++++++++++- .../localAgentHostSessionsProvider.test.ts | 153 ++++++++++++++- 2 files changed, 328 insertions(+), 3 deletions(-) diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts index 31bf9d91acfa3b..006af65c025adb 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -12,7 +12,7 @@ import { IMarkdownString, MarkdownString } from '../../../../../base/common/html import { Disposable, DisposableMap, DisposableStore, IDisposable, IReference, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { equals } from '../../../../../base/common/objects.js'; import { constObservable, derived, derivedObservableWithCache, derivedOpts, IObservable, ISettableObservable, mapObservableArrayCached, observableFromEvent, observableValue, observableValueOpts, throttledObservable, transaction, waitForState } from '../../../../../base/common/observable.js'; -import { isEqual } from '../../../../../base/common/resources.js'; +import { isEqual, isEqualOrParent, relativePath } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isDefined } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -320,6 +320,11 @@ export class AgentHostSessionAdapter extends Disposable implements ISession { // list refresh). See `_applySessionMetaFromState` / `setMeta`. private _project: IAgentSessionMetadata['project']; private _workingDirectory: URI | undefined; + // The directory that the current `mode` custom-agent URI is rooted at. Used to + // compute the agent's repo-relative path so the selection can be rebased onto + // its worktree twin when the session relocates into an isolated worktree (see + // `reconcileSelectedAgent`). + private _agentBaseDir: URI | undefined; private _meta: SessionMeta | undefined; /** * Observable mirror of {@link _meta}, kept in sync with every write to @@ -649,9 +654,105 @@ export class AgentHostSessionAdapter extends Disposable implements ISession { this._getAdditionalChat(chatResource)?.setAgent(agent); } else { this.mode.set(agent ? { id: agent.uri, kind: AGENT_MODE_KIND } : undefined, undefined); + // Remember which working directory the agent URI is rooted at so the + // selection can be rebased if the session later relocates into a worktree. + this._agentBaseDir = agent ? this._workingDirectory : undefined; } } + /** + * Reconcile the selected custom-agent URI against the host's current agent + * list — e.g. the session graduated with an agent picked in the original repo + * but now runs in an isolated worktree, where the host reports the same agent + * file under the worktree path. + * + * The selection is rebased by matching the agent's repo-relative path against + * the available agents (which already carry the worktree root) rather than the + * session's reported working directory. The working directory is unreliable + * here: the worktree-pathed customizations arrive well before either the + * `SessionSummary` or `SessionState` working-directory flips to the worktree, + * so a working-directory-keyed rebase would miss the window and let the picker + * destructively reset the selection. Deriving the worktree root from the agent + * list closes that race. + * + * Mirrors the agent-host backend's code to rebase by relative path. + * The re-point is only applied to a URI that actually exists in + * the supplied agent list, so it never runs ahead of the host reporting the + * worktree agents (which would otherwise re-introduce the mismatch it fixes). + */ + reconcileSelectedAgent(agents: readonly AgentCustomization[]): void { + const current = this.mode.get(); + if (!current || agents.some(a => a.uri === current.id)) { + return; // no agent selected, or the selection is already valid + } + const base = this._agentBaseDir; + if (!base) { + return; // unknown root for the current selection — nothing to rebase against + } + const agentUri = URI.parse(current.id); + if (!isEqualOrParent(agentUri, base)) { + return; // agent lives outside the repo (e.g. a user-global agent) + } + const rel = relativePath(base, agentUri); + if (!rel) { + return; + } + const relocated = this._findRelocatedAgent(agents, agentUri, base, rel); + if (relocated) { + this.mode.set({ id: relocated.uri, kind: current.kind }, undefined); + this._agentBaseDir = relocated.root; + } + } + + /** + * Finds an available agent that is the same repo-relative file as the current + * selection but rooted under a different directory (its worktree twin). + * + * A candidate matches when its path ends with `/` on a path-segment + * boundary and the implied root (the candidate path minus that suffix) differs + * from `base`. The root is re-validated with `relativePath` so only a genuine + * relocation of the same file is accepted. Returns the matched agent's URI and + * its derived root, or `undefined` when there is no twin. + */ + private _findRelocatedAgent( + agents: readonly AgentCustomization[], + agentUri: URI, + base: URI, + rel: string, + ): { readonly uri: string; readonly root: URI } | undefined { + const suffix = `/${rel}`; + for (const agent of agents) { + const candidate = URI.parse(agent.uri); + if (candidate.scheme !== agentUri.scheme || candidate.authority !== agentUri.authority) { + continue; + } + if (!candidate.path.endsWith(suffix) || candidate.path.length === suffix.length) { + continue; // not the same relative file, or it sits at the filesystem root + } + const root = candidate.with({ path: candidate.path.slice(0, candidate.path.length - suffix.length) }); + if (isEqual(root, base) || relativePath(root, candidate) !== rel) { + continue; // same root (would have matched exactly), or not a clean relocation + } + return { uri: agent.uri, root }; + } + return undefined; + } + + /** + * Seed the selected custom agent when a session is resumed (e.g. after a + * window reload). A freshly loaded adapter starts with `mode === undefined`; + * the host persists the selection on the default chat's `ChatState.draft.agent`, + * which the provider reads and mirrors onto `session.mode` here. Guarded to + * never override a live selection (a Part 1 graduation seed or a user pick), + * keeping this a resume-only hydration. + */ + hydrateSelectedAgent(agentUri: string): void { + if (this.mode.get() !== undefined) { + return; + } + this.setChatAgent(this.resource, { uri: agentUri, name: '' }); + } + getChatModelId(chatResource: URI): string | undefined { return chatResource.fragment ? this._getAdditionalChat(chatResource)?.chat.modelId.get() @@ -2820,6 +2921,18 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement const committedSession = await this._waitForNewSession(existingKeys, chatResource.scheme); if (committedSession) { this._preserveNewSessionConfig(newSession, committedSession.sessionId); + // Carry the picked custom agent onto the committed session before + // the replace event so the agent picker doesn't reset to the + // default once the active session is swapped (the picker mirrors + // `session.mode`, which is otherwise `undefined` on the freshly + // committed adapter). The host already received the agent with the + // first turn (see `sendOptions.modeInfo`), so update only the local + // mode observable here rather than re-notifying it via `setAgent`. + if (selectedAgent) { + const committedRawId = this._rawIdFromChatId(committedSession.sessionId); + const committedAdapter = committedRawId ? this._sessionCache.get(committedRawId) : undefined; + committedAdapter?.setChatAgent(committedAdapter.resource, selectedAgent); + } // Session graduated: release the eager subscription without // firing `disposeSession`. The session handler has already // acquired its own subscription (chat widget was opened @@ -2966,6 +3079,49 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement if (value && !(value instanceof Error)) { this._applySessionStateUpdate(sessionId, value); } + + this._hydrateAgentFromDraft(connection, cached, sessionId, sessionUri, store); + } + + /** + * Resume hydration: when a session is (re)loaded and its adapter has no agent + * selected, restore the persisted selection from the default chat's + * `ChatState.draft.agent` and mirror it onto `session.mode` (the picker's + * source of truth). + * + * The agent is persisted on the chat channel — the session channel + * ({@link SessionState}) carries no draft — so we briefly observe the default + * chat's state until its draft agent arrives. The subscription is shared and + * ref-counted with the chat session handler (no extra wire cost) and lives for + * the session-state store's lifetime. Hydration is one-shot: the observer + * stops as soon as `mode` is set — by us here, or by a concurrent graduation + * seed or user pick (guarded inside + * {@link AgentHostSessionAdapter.hydrateSelectedAgent}) — so it neither leaks, + * overrides a later selection, nor keeps re-running on every chat update. + */ + private _hydrateAgentFromDraft(connection: IAgentConnection, cached: AgentHostSessionAdapter, sessionId: string, sessionUri: URI, store: DisposableStore): void { + if (cached.mode.get() !== undefined) { + return; + } + const lastDefaultChat = this._lastSessionStates.get(sessionId)?.defaultChat; + const defaultChatUri = lastDefaultChat ? URI.parse(lastDefaultChat.toString()) : URI.parse(buildDefaultChatUri(sessionUri)); + const chatRef = connection.getSubscription(StateComponents.Chat, defaultChatUri, 'BaseAgentHostSessionsProvider.draftAgent'); + store.add(chatRef); + const listener = store.add(new MutableDisposable()); + const tryHydrate = () => { + if (cached.mode.get() === undefined) { + const chatState = chatRef.object.value; + const agentUri = chatState && !(chatState instanceof Error) ? chatState.draft?.agent?.uri : undefined; + if (agentUri) { + cached.hydrateSelectedAgent(agentUri); + } + } + if (cached.mode.get() !== undefined) { + listener.clear(); // hydration is one-shot; stop observing + } + }; + listener.value = chatRef.object.onDidChange(() => tryHydrate()); + tryHydrate(); } /** @@ -2981,6 +3137,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement // change too — firing on all of them caused excessive picker // recomputes (and a feedback loop with `setAgent`). if (!previous || customizationsChanged(previous, state)) { + this._reconcileAgentFromState(sessionId, state); this._onDidChangeCustomAgents.fire(); this._onDidChangeCustomizations.fire(); } @@ -3022,6 +3179,25 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement cached.updateChangesets(state.changesets); } + /** + * Rebase the cached running adapter's selected agent against the host's agent + * list from an AHP {@link SessionState}, before the picker is notified. A + * session that has moved into an isolated worktree keeps its selection instead + * of resetting to the default once the host starts reporting worktree-pathed + * agents. See {@link AgentHostSessionAdapter.reconcileSelectedAgent}. + */ + private _reconcileAgentFromState(sessionId: string, state: SessionState): void { + const rawId = this._rawIdFromChatId(sessionId); + if (!rawId) { + return; + } + const cached = this._sessionCache.get(rawId); + if (!cached) { + return; + } + cached.reconcileSelectedAgent(getEffectiveAgents(state.customizations)); + } + /** * Reconcile the per-chat catalog of the cached running adapter from an AHP * {@link SessionState}. The adapter exposes `chats`/`mainChat` as diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts index aa9f73b4dc2092..96e2de694e15cc 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts @@ -16,7 +16,7 @@ import { AgentSession, ClaudePreferAgentHostAgentsSettingId, ClaudePreferAgentHo import type { IAgentSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js'; import type { ResolveSessionConfigResult } from '../../../../../../platform/agentHost/common/state/protocol/commands.js'; import { CustomizationLoadStatus, CustomizationType, MessageKind, SessionLifecycle, type AgentInfo, type ChangesSummary, type Customization, type RootState, type SessionConfigState, type SessionState, type SessionSummary } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; -import { buildChatUri, buildDefaultChatUri, ChangesetStatus, SessionStatus as ProtocolSessionStatus, StateComponents, type ChangesetState, type ChatSummary } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { buildChatUri, buildDefaultChatUri, ChangesetStatus, SessionStatus as ProtocolSessionStatus, StateComponents, type ChangesetState, type ChatState, type ChatSummary } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { ActionType, NotificationType, type ActionEnvelope, type IRootConfigChangedAction, type ChatAction, type SessionAction, type TerminalAction, type INotification, type ClientAnnotationsAction } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; import { SessionConfigKey } from '../../../../../../platform/agentHost/common/sessionConfigKeys.js'; import { ConfigurationTarget, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -51,7 +51,7 @@ import { IWorkbenchEnvironmentService } from '../../../../../../workbench/servic const STORAGE_KEY_REMEMBERED_SESSION_CONFIG_VALUES = 'sessions.agentHost.sessionConfigPicker.selectedValues'; -type SubscriptionState = SessionState | ChangesetState; +type SubscriptionState = SessionState | ChangesetState | ChatState; class MockAgentHostService extends mock() { declare readonly _serviceBrand: undefined; @@ -235,6 +235,11 @@ class MockAgentHostService extends mock() { this._sessionStateEmitters.get(changesetUri)?.fire(state); } + setChatState(chatUri: string, state: ChatState): void { + this._sessionStateValues.set(chatUri, state); + this._sessionStateEmitters.get(chatUri)?.fire(state); + } + setAgents(agents: AgentInfo[]): void { this._rootStateValue = { agents }; this._onDidRootStateChange.fire(this._rootStateValue); @@ -1022,6 +1027,150 @@ suite('LocalAgentHostSessionsProvider', () => { assert.deepStrictEqual(agentHost.dispatchedActions, []); }); + test('restores the selected agent from the default chat draft on resume', () => { + const provider = createProvider(disposables, agentHost); + fireSessionAdded(agentHost, 'resume-agent', { title: 'Resume Agent Session' }); + + const session = provider.getSessions().find(s => s.title.get() === 'Resume Agent Session'); + assert.ok(session); + assert.strictEqual(session!.mode.get(), undefined); + + // `getSessionConfig` opens the session-state subscription, which also opens + // the default chat subscription used to read the persisted draft agent. + provider.getSessionConfig(session!.sessionId); + + const defaultChatUri = buildDefaultChatUri(AgentSession.uri('copilotcli', 'resume-agent')); + agentHost.setChatState(defaultChatUri, { + resource: defaultChatUri, + title: 'Resume Agent Session', + status: ProtocolSessionStatus.Idle, + modifiedAt: new Date(0).toISOString(), + turns: [], + draft: { text: '', origin: { kind: MessageKind.User }, agent: { uri: 'agent://resumed' } }, + }); + + assert.deepStrictEqual(session!.mode.get(), { id: 'agent://resumed', kind: 'agent' }); + }); + + test('does not override a live agent selection with the persisted draft agent', () => { + const provider = createProvider(disposables, agentHost); + fireSessionAdded(agentHost, 'resume-nooverride', { title: 'Resume No Override' }); + + const session = provider.getSessions().find(s => s.title.get() === 'Resume No Override'); + assert.ok(session); + + // A live pick wins; a later draft snapshot must not clobber it. + provider.setAgent?.(session!.sessionId, { uri: 'agent://live', name: 'live' }); + provider.getSessionConfig(session!.sessionId); + + const defaultChatUri = buildDefaultChatUri(AgentSession.uri('copilotcli', 'resume-nooverride')); + agentHost.setChatState(defaultChatUri, { + resource: defaultChatUri, + title: 'Resume No Override', + status: ProtocolSessionStatus.Idle, + modifiedAt: new Date(0).toISOString(), + turns: [], + draft: { text: '', origin: { kind: MessageKind.User }, agent: { uri: 'agent://resumed' } }, + }); + + assert.deepStrictEqual(session!.mode.get(), { id: 'agent://live', kind: 'agent' }); + }); + + test('rebases the selected agent to its worktree twin from the agent list before the working directory flips', () => { + const provider = createProvider(disposables, agentHost); + fireSessionAdded(agentHost, 'rebase-worktree', { title: 'Rebase Worktree', workingDirectory: 'file:///Users/me/vscode' }); + + const session = provider.getSessions().find(s => s.title.get() === 'Rebase Worktree'); + assert.ok(session); + + // A folder agent is picked while the session still runs in the repo. + const folderAgent = 'file:///Users/me/vscode/.github/agents/sessions.md'; + const worktreeAgent = 'file:///Users/me/vscode.worktrees/rebase-worktree/.github/agents/sessions.md'; + provider.setAgent?.(session!.sessionId, { uri: folderAgent, name: 'sessions' }); + + // The host reports the worktree-pathed agents (the folder twin is gone) + // well before the working directory flips to the worktree. The rebase + // must derive the worktree root from the agent list, not the (still + // folder) working directory, so the selection is re-pointed in time. + provider.getSessionConfig(session!.sessionId); + agentHost.setSessionState('rebase-worktree', 'copilotcli', { + provider: 'copilotcli', + title: 'Rebase Worktree', + status: ProtocolSessionStatus.Idle, + lifecycle: SessionLifecycle.Ready, + activeClients: [], + chats: [], + customizations: [{ + type: CustomizationType.Plugin, + id: 'plugin://worktree', + uri: 'plugin://worktree', + name: 'worktree plugin', + enabled: true, + load: { kind: CustomizationLoadStatus.Loaded }, + children: [{ type: CustomizationType.Agent, id: worktreeAgent, uri: worktreeAgent, name: 'sessions' }], + }], + }); + + assert.deepStrictEqual(session!.mode.get(), { id: worktreeAgent, kind: 'agent' }); + }); + + test('leaves the selected agent untouched when the agent list has no relocated twin', () => { + const provider = createProvider(disposables, agentHost); + fireSessionAdded(agentHost, 'rebase-none', { title: 'Rebase None', workingDirectory: 'file:///Users/me/vscode' }); + + const session = provider.getSessions().find(s => s.title.get() === 'Rebase None'); + assert.ok(session); + + const folderAgent = 'file:///Users/me/vscode/.github/agents/sessions.md'; + provider.setAgent?.(session!.sessionId, { uri: folderAgent, name: 'sessions' }); + + // An unrelated agent (different repo-relative file) must not be treated + // as a relocation of the selection. + provider.getSessionConfig(session!.sessionId); + agentHost.setSessionState('rebase-none', 'copilotcli', { + provider: 'copilotcli', + title: 'Rebase None', + status: ProtocolSessionStatus.Idle, + lifecycle: SessionLifecycle.Ready, + activeClients: [], + chats: [], + customizations: [{ + type: CustomizationType.Plugin, + id: 'plugin://other', + uri: 'plugin://other', + name: 'other plugin', + enabled: true, + load: { kind: CustomizationLoadStatus.Loaded }, + children: [{ type: CustomizationType.Agent, id: 'file:///Users/me/vscode.worktrees/rebase-none/.github/agents/other.md', uri: 'file:///Users/me/vscode.worktrees/rebase-none/.github/agents/other.md', name: 'other' }], + }], + }); + + assert.deepStrictEqual(session!.mode.get(), { id: folderAgent, kind: 'agent' }); + }); + + test('carries the picked custom agent onto the committed session when a new session graduates', async () => { + // Part 1 regression: when a new (untitled) session graduates into a real + // running session on first send, the picked agent must travel onto the + // committed session's `mode`. Otherwise the picker — which mirrors + // `session.mode` — resets to the default the moment the active session is + // swapped for the freshly committed one. + const provider = createProvider(disposables, agentHost, undefined, { + openSession: true, + sendRequest: async (): Promise => { + agentHost.addSession(createSession('graduated', { summary: 'Graduated Session' })); + return { kind: 'sent' as const, data: {} as ChatSendResult extends { kind: 'sent'; data: infer D } ? D : never }; + }, + }); + + const session = provider.createNewSession(URI.parse('file:///home/user/project'), provider.sessionTypes[0].id); + provider.setAgent?.(session.sessionId, { uri: 'agent://picked', name: 'picked' }); + + const chat = await provider.createNewChat(session.sessionId); + const committed = await provider.sendRequest(session.sessionId, chat.resource, { query: 'hello' }); + + assert.deepStrictEqual(committed.mode.get(), { id: 'agent://picked', kind: 'agent' }); + }); + // ---- getCustomAgents / onDidChangeCustomAgents ------- test('getCustomAgents collects agents from session customizations, coalesced by URI and sorted by name', async () => { From 412342e7e834d5f3101dea4d1ad75c0902403fbe Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:10:33 -0700 Subject: [PATCH 062/589] Browser: fix url bar taking back focus after navigating on a new tab (#323563) --- .../electron-browser/browserEditor.ts | 2 ++ .../features/browserNavigationFeatures.ts | 27 +++++++++++++------ .../webContentsViewRendererFeature.ts | 4 +-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index e5731ec37a6070..533745a7a1fe5b 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -431,6 +431,8 @@ export class BrowserEditor extends EditorPane { private readonly _inputDisposables = this._register(new DisposableStore()); private _currentPadding: { top: number; right: number; bottom: number; left: number } = { top: 0, right: 0, bottom: 0, left: 0 }; + override get input(): BrowserEditorInput | undefined { return super.input as BrowserEditorInput | undefined; } + constructor( group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, diff --git a/src/vs/workbench/contrib/browserView/electron-browser/features/browserNavigationFeatures.ts b/src/vs/workbench/contrib/browserView/electron-browser/features/browserNavigationFeatures.ts index 3fa1932c744a2a..c756dfa8f03526 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/features/browserNavigationFeatures.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/features/browserNavigationFeatures.ts @@ -220,6 +220,13 @@ export class BrowserNavigationFeatures extends BrowserEditorContribution { private readonly _canGoForwardContext: IContextKey; private readonly _pendingTryFocus = this._register(new MutableDisposable()); + /** + * Whether a navigation has been initiated on the current tab. Once true, + * an empty URL means "navigation in flight" rather than "fresh tab", so + * {@link tryFocus} keeps focus on the page instead of reopening the picker. + */ + private _hasInitiatedNavigation = false; + constructor( editor: BrowserEditor, @IInstantiationService instantiationService: IInstantiationService, @@ -262,12 +269,19 @@ export class BrowserNavigationFeatures extends BrowserEditorContribution { } protected override onModelAttached(model: IBrowserViewModel, store: DisposableStore): void { + // A model that is already loading on attach (e.g. switching back to a + // tab mid-navigation) counts as having initiated navigation. + this._hasInitiatedNavigation = model.loading; this._updateFromModel(model); store.add(model.onDidNavigate(() => this._updateFromModel(model))); - store.add(model.onWillNavigate(url => this._navbar.previewUrl(url))); + store.add(model.onWillNavigate(url => { + this._hasInitiatedNavigation = true; + this._navbar.previewUrl(url); + })); } override onModelDetached(): void { + this._hasInitiatedNavigation = false; this._navbar.clear(); this._canGoBackContext.reset(); this._canGoForwardContext.reset(); @@ -283,16 +297,13 @@ export class BrowserNavigationFeatures extends BrowserEditorContribution { return; } - // A new tab (no URL loaded) auto-opens the picker so the user can - // immediately type / browse suggestions. For tabs that already have a - // URL (e.g. error or loading state — page-renderer focus didn't claim - // us, or input is still prerendering before the model attaches) we - // just focus the display so the URL stays visible. + // A new tab (no URL loaded) auto-opens the picker so the user can immediately type / browse suggestions. + // Otherwise we move focus into the browser editor so it doesn't stay on the tab control. const url = this.editor.model?.url ?? (input instanceof BrowserEditorInput ? input.url : undefined); - if (!url) { + if (!url && !this._hasInitiatedNavigation) { this._navbar.openUrlPicker(); } else { - this._navbar.focusUrlInput(); + this.editor.ensureBrowserFocus(); } }, 0); return true; diff --git a/src/vs/workbench/contrib/browserView/electron-browser/features/webContentsViewRendererFeature.ts b/src/vs/workbench/contrib/browserView/electron-browser/features/webContentsViewRendererFeature.ts index 34d986cea26ef4..66bab03cfcd0df 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/features/webContentsViewRendererFeature.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/features/webContentsViewRendererFeature.ts @@ -158,7 +158,7 @@ class WebContentsViewRendererFeature extends BrowserEditorContribution { } override tryFocus(): boolean { - if (!this._shouldShowPage()) { + if (!this.editor.input?.url) { return false; } this.editor.ensureBrowserFocus(); @@ -167,7 +167,7 @@ class WebContentsViewRendererFeature extends BrowserEditorContribution { } this._focusTimeout = setTimeout(() => { this._focusTimeout = undefined; - if (this._model) { + if (this._model && this._shouldShowPage()) { void this._model.focus(); } }, 0); From 7a3a960f06b5d4c993141bad179233d3f3c60699 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 29 Jun 2026 20:11:09 +0200 Subject: [PATCH 063/589] sessions: gate Copilot CLI/Claude agent-host preferences on chat.agentHost.enabled (#323567) The `chat.agents.copilotCli.hideExtensionHost` and `chat.agents.claude.preferAgentHost` settings tell the Copilot Chat sessions provider to step aside so the agent host can surface its own Copilot CLI / Claude entries. When `chat.agentHost.enabled` is off the agent host never registers those entries, so honoring the preferences would make the entries disappear entirely. Gate both on `chat.agentHost.enabled` so they are only respected when the agent host is actually enabled, and document the requirement on the settings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../common/agentHost.config.contribution.ts | 2 +- .../browser/copilotChatSessionsProvider.ts | 65 ++++++++++--------- .../copilotChatSessionsProvider.test.ts | 48 +++++++++++++- .../chat/browser/chat.shared.contribution.ts | 4 +- 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHost.config.contribution.ts b/src/vs/platform/agentHost/common/agentHost.config.contribution.ts index 09e20948078d45..cfbe600980a1c1 100644 --- a/src/vs/platform/agentHost/common/agentHost.config.contribution.ts +++ b/src/vs/platform/agentHost/common/agentHost.config.contribution.ts @@ -43,7 +43,7 @@ configurationRegistry.registerConfiguration({ }, 'chat.agents.copilotCli.hideExtensionHost': { type: 'boolean', - description: nls.localize('chat.agents.copilotCli.hideExtensionHost', "When enabled, hides the Extension Host Copilot CLI entry from the Agents window picker."), + markdownDescription: nls.localize('chat.agents.copilotCli.hideExtensionHost', "When enabled, hides the Extension Host Copilot CLI entry from the Agents window picker. Requires `#{0}#`.", AgentHostEnabledSettingId), default: false, tags: ['experimental'], experiment: { mode: 'startup' }, diff --git a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts index cfcd8597785917..2fafbf64c6a781 100644 --- a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts @@ -43,7 +43,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { SessionConfigKey } from '../../../../../platform/agentHost/common/sessionConfigKeys.js'; -import { ClaudePreferAgentHostAgentsSettingId } from '../../../../../platform/agentHost/common/agentService.js'; +import { AgentHostEnabledSettingId, ClaudePreferAgentHostAgentsSettingId } from '../../../../../platform/agentHost/common/agentService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IGitHubService } from '../../../github/browser/githubService.js'; import { computePullRequestIcon, GitHubPullRequestState } from '../../../github/common/types.js'; @@ -1438,28 +1438,50 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions private readonly _onDidGroupMembershipChange = this._register(new Emitter<{ sessionId: string }>()); private readonly _multiChatEnabled: boolean; - private _claudeEnabled: boolean; - private _preferAgentHostClaude: boolean; - private _hideExtensionHostCopilotCli: boolean; + + /** Whether the agent host is enabled via `chat.agentHost.enabled`. */ + private _isAgentHostEnabled(): boolean { + return this.configurationService.getValue(AgentHostEnabledSettingId) ?? false; + } /** * Claude is offered by this (Copilot Chat sessions) provider only when the * underlying `claudeAgent.enabled` setting is on AND the user has not opted * the agent-host implementation in via `chat.agents.claude.preferAgentHost`. * When the latter is true, the agent host registers Claude itself and this - * provider stays out of the way so the picker shows a single entry. + * provider stays out of the way so the picker shows a single entry. Stepping + * aside only makes sense when the agent host is enabled to register Claude in + * its place, so the preference is not respected unless `chat.agentHost.enabled` + * is also on. */ private _isClaudeAvailable(): boolean { - return this._claudeEnabled && !this._preferAgentHostClaude; + const claudeEnabled = this.configurationService.getValue(CLAUDE_CODE_ENABLED_SETTING) ?? false; + if (!claudeEnabled) { + return false; + } + const isAgentHostEnabled = this._isAgentHostEnabled(); + const preferAgentHost = this.configurationService.getValue(ClaudePreferAgentHostAgentsSettingId) ?? false; + if (isAgentHostEnabled && preferAgentHost) { + return false; + } + return true; } /** * The Extension Host Copilot CLI is offered by this provider unless the user * has hidden it via `chat.agents.copilotCli.hideExtensionHost`, in which case * the Agents window picker only surfaces the Agent Host Copilot CLI entry. + * Hiding it only makes sense when the agent host is enabled to surface the + * Agent Host Copilot CLI in its place, so the setting is not respected unless + * `chat.agentHost.enabled` is also on. */ private _isCopilotCliAvailable(): boolean { - return !this._hideExtensionHostCopilotCli; + const isAgentHostEnabled = this._isAgentHostEnabled(); + const hideExtensionHost = this.configurationService.getValue(ChatConfiguration.CopilotCliHideExtensionHostAgents) ?? false; + if (isAgentHostEnabled && hideExtensionHost) { + return false; + } + return true; } readonly browseActions: readonly ISessionWorkspaceBrowseAction[]; @@ -1483,32 +1505,17 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions super(); this._multiChatEnabled = this.configurationService.getValue(COPILOT_MULTI_CHAT_SETTING) ?? true; - this._claudeEnabled = this.configurationService.getValue(CLAUDE_CODE_ENABLED_SETTING); - this._preferAgentHostClaude = this.configurationService.getValue(ClaudePreferAgentHostAgentsSettingId) ?? false; - this._hideExtensionHostCopilotCli = this.configurationService.getValue(ChatConfiguration.CopilotCliHideExtensionHostAgents) ?? false; this._register(this.configurationService.onDidChangeConfiguration(e => { - const claudeEnabledChanged = e.affectsConfiguration(CLAUDE_CODE_ENABLED_SETTING); - const preferAgentHostChanged = e.affectsConfiguration(ClaudePreferAgentHostAgentsSettingId); - const hideCopilotCliChanged = e.affectsConfiguration(ChatConfiguration.CopilotCliHideExtensionHostAgents); - if (!claudeEnabledChanged && !preferAgentHostChanged && !hideCopilotCliChanged) { + const affectsSessionTypes = e.affectsConfiguration(CLAUDE_CODE_ENABLED_SETTING) + || e.affectsConfiguration(ClaudePreferAgentHostAgentsSettingId) + || e.affectsConfiguration(ChatConfiguration.CopilotCliHideExtensionHostAgents) + || e.affectsConfiguration(AgentHostEnabledSettingId); + if (!affectsSessionTypes) { return; } - const wasClaudeAvailable = this._isClaudeAvailable(); - const wasCopilotCliAvailable = this._isCopilotCliAvailable(); - if (claudeEnabledChanged) { - this._claudeEnabled = this.configurationService.getValue(CLAUDE_CODE_ENABLED_SETTING); - } - if (preferAgentHostChanged) { - this._preferAgentHostClaude = this.configurationService.getValue(ClaudePreferAgentHostAgentsSettingId) ?? false; - } - if (hideCopilotCliChanged) { - this._hideExtensionHostCopilotCli = this.configurationService.getValue(ChatConfiguration.CopilotCliHideExtensionHostAgents) ?? false; - } - if (this._isClaudeAvailable() !== wasClaudeAvailable || this._isCopilotCliAvailable() !== wasCopilotCliAvailable) { - this._onDidChangeSessionTypes.fire(); - this._refreshSessionCache(); - } + this._onDidChangeSessionTypes.fire(); + this._refreshSessionCache(); })); this.browseActions = [ diff --git a/src/vs/sessions/contrib/providers/copilotChatSessions/test/browser/copilotChatSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/copilotChatSessions/test/browser/copilotChatSessionsProvider.test.ts index 2f5098ff4f7a7a..93095bc229c191 100644 --- a/src/vs/sessions/contrib/providers/copilotChatSessions/test/browser/copilotChatSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/copilotChatSessions/test/browser/copilotChatSessionsProvider.test.ts @@ -33,7 +33,7 @@ import { ISessionChangeEvent } from '../../../../../services/sessions/common/ses import { GITHUB_REMOTE_FILE_SCHEME, SessionStatus } from '../../../../../services/sessions/common/session.js'; import { ChatConfiguration, ChatPermissionLevel } from '../../../../../../workbench/contrib/chat/common/constants.js'; import { CLAUDE_CODE_ENABLED_SETTING, CopilotChatSessionsProvider, COPILOT_PROVIDER_ID, ClaudeCodeSessionType, ICopilotChatSession } from '../../browser/copilotChatSessionsProvider.js'; -import { ClaudePreferAgentHostAgentsSettingId } from '../../../../../../platform/agentHost/common/agentService.js'; +import { AgentHostEnabledSettingId, ClaudePreferAgentHostAgentsSettingId } from '../../../../../../platform/agentHost/common/agentService.js'; import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { IUriIdentityService } from '../../../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -125,7 +125,7 @@ class MockAgentSessionsModel { function createProvider( disposables: DisposableStore, model: MockAgentSessionsModel, - opts?: { multiChatEnabled?: boolean; claudeEnabled?: boolean; preferAgentHost?: boolean; hideCopilotCli?: boolean }, + opts?: { multiChatEnabled?: boolean; claudeEnabled?: boolean; preferAgentHost?: boolean; hideCopilotCli?: boolean; agentHostEnabled?: boolean }, ): CopilotChatSessionsProvider { return createProviderWithConfig(disposables, model, opts).provider; } @@ -133,7 +133,7 @@ function createProvider( function createProviderWithConfig( disposables: DisposableStore, model: MockAgentSessionsModel, - opts?: { multiChatEnabled?: boolean; claudeEnabled?: boolean; preferAgentHost?: boolean; hideCopilotCli?: boolean }, + opts?: { multiChatEnabled?: boolean; claudeEnabled?: boolean; preferAgentHost?: boolean; hideCopilotCli?: boolean; agentHostEnabled?: boolean }, ): { provider: CopilotChatSessionsProvider; configService: TestConfigurationService } { const instantiationService = disposables.add(new TestInstantiationService()); @@ -141,6 +141,7 @@ function createProviderWithConfig( configService.setUserConfiguration('sessions.github.copilot.multiChatSessions', opts?.multiChatEnabled ?? true); configService.setUserConfiguration(CLAUDE_CODE_ENABLED_SETTING, opts?.claudeEnabled ?? true); configService.setUserConfiguration(ClaudePreferAgentHostAgentsSettingId, opts?.preferAgentHost ?? false); + configService.setUserConfiguration(AgentHostEnabledSettingId, opts?.agentHostEnabled ?? true); configService.setUserConfiguration(ChatConfiguration.CopilotCliHideExtensionHostAgents, opts?.hideCopilotCli ?? false); instantiationService.stub(IConfigurationService, configService); @@ -324,6 +325,16 @@ suite('CopilotChatSessionsProvider', () => { assert.ok(provider.sessionTypes.some(t => t.id === ClaudeCodeSessionType.id)); }); + test('preferAgentHost is not respected when chat.agentHost.enabled is false', () => { + // Yielding to the agent host's Claude only makes sense when the agent + // host is enabled to register it. With the agent host disabled the + // preference must be ignored so this provider keeps surfacing Claude; + // otherwise Claude would disappear entirely. + const provider = createProvider(disposables, model, { claudeEnabled: true, preferAgentHost: true, agentHostEnabled: false }); + assert.strictEqual(provider.sessionTypes.length, 3); + assert.ok(provider.sessionTypes.some(t => t.id === ClaudeCodeSessionType.id)); + }); + test('onDidChangeSessionTypes fires when claude setting changes', () => { const { provider, configService } = createProviderWithConfig(disposables, model); assert.strictEqual(provider.sessionTypes.length, 3); @@ -397,6 +408,37 @@ suite('CopilotChatSessionsProvider', () => { assert.ok(!provider.sessionTypes.some(t => t.id === CopilotCLISessionType.id)); }); + test('hideExtensionHost is not respected when chat.agentHost.enabled is false', () => { + // Hiding the Extension Host Copilot CLI only makes sense when the agent + // host is enabled to surface the Agent Host Copilot CLI in its place. With + // the agent host disabled the hide setting must be ignored so the entry + // stays visible. + const provider = createProvider(disposables, model, { hideCopilotCli: true, agentHostEnabled: false }); + assert.ok(provider.sessionTypes.some(t => t.id === CopilotCLISessionType.id)); + }); + + test('onDidChangeSessionTypes fires when chat.agentHost.enabled toggles the hide gate', () => { + // With the hide setting on but the agent host initially disabled, the + // Copilot CLI entry is visible. Enabling the agent host must live-apply + // the hide setting and drop the entry without a window reload. + const { provider, configService } = createProviderWithConfig(disposables, model, { hideCopilotCli: true, agentHostEnabled: false }); + assert.ok(provider.sessionTypes.some(t => t.id === CopilotCLISessionType.id)); + + let fired = false; + disposables.add(provider.onDidChangeSessionTypes(() => { fired = true; })); + + configService.setUserConfiguration(AgentHostEnabledSettingId, true); + configService.onDidChangeConfigurationEmitter.fire({ + source: ConfigurationTarget.USER, + affectedKeys: new Set([AgentHostEnabledSettingId]), + change: { keys: [AgentHostEnabledSettingId], overrides: [] }, + affectsConfiguration: (key: string) => key === AgentHostEnabledSettingId, + }); + + assert.ok(fired, 'onDidChangeSessionTypes should have fired'); + assert.ok(!provider.sessionTypes.some(t => t.id === CopilotCLISessionType.id)); + }); + test('toggling claude setting refreshes sessions list', () => { const claudeResource = URI.from({ scheme: AgentSessionProviders.Claude, path: '/claude-session' }); model.addSession(createMockAgentSession(claudeResource, { providerType: AgentSessionProviders.Claude })); diff --git a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts index 6f2ba717a91b64..ef835ec04063f2 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts @@ -12,7 +12,7 @@ import { PolicyCategory } from '../../../../base/common/policy.js'; import '../../../../platform/agentHost/common/agentHost.config.contribution.js'; import '../../../../platform/agentHost/browser/agentHost.config.contribution.js'; import '../../../../platform/agentHost/common/agentHostStarter.config.contribution.js'; -import { AgentHostAhpJsonlLoggingSettingId, AgentHostCustomTerminalToolEnabledSettingId, AgentHostOpus48PromptEnabledSettingId, AgentHostSdkSandboxEnabledSettingId, ClaudePreferAgentHostAgentsSettingId, ClaudePreferAgentHostEditorSettingId } from '../../../../platform/agentHost/common/agentService.js'; +import { AgentHostAhpJsonlLoggingSettingId, AgentHostCustomTerminalToolEnabledSettingId, AgentHostEnabledSettingId, AgentHostOpus48PromptEnabledSettingId, AgentHostSdkSandboxEnabledSettingId, ClaudePreferAgentHostAgentsSettingId, ClaudePreferAgentHostEditorSettingId } from '../../../../platform/agentHost/common/agentService.js'; import { AgentNetworkFilterService, IAgentNetworkFilterService } from '../../../../platform/networkFilter/common/networkFilterService.js'; import { AgentNetworkDomainSettingId } from '../../../../platform/networkFilter/common/settings.js'; import { COPILOT_DISABLE_BYPASS_PERMISSIONS_MODE_KEY, COPILOT_ENABLED_PLUGINS_KEY, COPILOT_EXTRA_MARKETPLACES_KEY, COPILOT_MODEL_KEY, COPILOT_STRICT_MARKETPLACES_KEY, managedSettingValue } from '../../../../platform/policy/common/copilotManagedSettings.js'; @@ -778,7 +778,7 @@ configurationRegistry.registerConfiguration({ }, [ClaudePreferAgentHostAgentsSettingId]: { type: 'boolean', - description: nls.localize('chat.agents.claude.preferAgentHost', "When enabled, Claude sessions opened from the Agents Window run inside the agent host process instead of the GitHub Copilot Chat extension. Only one Claude implementation surfaces per window."), + markdownDescription: nls.localize('chat.agents.claude.preferAgentHost', "When enabled, Claude sessions opened from the Agents Window run inside the agent host process instead of the GitHub Copilot Chat extension. Only one Claude implementation surfaces per window. Requires `#{0}#`.", AgentHostEnabledSettingId), default: false, tags: ['experimental'], experiment: { mode: 'startup' }, From bc6efebd156ed832fb34ecacfca265af7afbb270 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 29 Jun 2026 11:16:56 -0700 Subject: [PATCH 064/589] Fix subagent client tool completion routing Route client tool completions from subagent chats through their recorded parent chat so tools running under peer chat subagents resolve against the correct SDK conversation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/common/state/sessionState.ts | 5 ++ .../agentHost/node/agentSideEffects.ts | 19 +++++++- .../agentHost/node/copilot/copilotAgent.ts | 11 +---- .../test/node/agentSideEffects.test.ts | 29 ++++++++++++ .../agentHost/test/node/copilotAgent.test.ts | 46 +------------------ 5 files changed, 54 insertions(+), 56 deletions(-) diff --git a/src/vs/platform/agentHost/common/state/sessionState.ts b/src/vs/platform/agentHost/common/state/sessionState.ts index 0e76e2ef23c566..6f38508848859f 100644 --- a/src/vs/platform/agentHost/common/state/sessionState.ts +++ b/src/vs/platform/agentHost/common/state/sessionState.ts @@ -619,6 +619,11 @@ export function buildDefaultChatUri(sessionUri: ProtocolURI | ResourceURI): stri const SUBAGENT_CHAT_ID = 'subagent'; +export function isSubagentChatUri(uri: ProtocolURI | ResourceURI): boolean { + const parsed = typeof uri === 'string' ? ResourceURI.parse(uri) : uri; + return parsed.scheme === AHP_CHAT_SCHEME && parsed.authority === SUBAGENT_CHAT_ID; +} + export function buildSubagentChatUri(sessionUri: ProtocolURI | ResourceURI, toolCallId: string): string { const session = typeof sessionUri === 'string' ? sessionUri : sessionUri.toString(); const encoded = encodeBase64(VSBuffer.fromString(session), false, true); diff --git a/src/vs/platform/agentHost/node/agentSideEffects.ts b/src/vs/platform/agentHost/node/agentSideEffects.ts index 2213417294ddd2..668123d49a708a 100644 --- a/src/vs/platform/agentHost/node/agentSideEffects.ts +++ b/src/vs/platform/agentHost/node/agentSideEffects.ts @@ -28,6 +28,7 @@ import { getToolFileEdits, isAhpChatChannel, isDefaultChatUri, + isSubagentChatUri, MessageKind, parseChatUri, parseRequiredSessionUriFromChatUri, @@ -165,7 +166,7 @@ export class AgentSideEffects extends Disposable { } const sessionChannel = parseRequiredSessionUriFromChatUri(envelope.channel); const agent = this._options.getAgent(sessionChannel); - agent?.onClientToolCallComplete(URI.parse(sessionChannel), URI.parse(envelope.channel), action.toolCallId, action.result); + agent?.onClientToolCallComplete(URI.parse(sessionChannel), URI.parse(this._toolCallCompletionChat(envelope.channel)), action.toolCallId, action.result); } if (envelope.action.type === ActionType.ChatDraftChanged) { this._persistChatDraft(envelope.channel, envelope.action.draft); @@ -700,6 +701,20 @@ export class AgentSideEffects extends Disposable { return undefined; } + private _toolCallCompletionChat(chatChannel: ProtocolURI): ProtocolURI { + if (!isSubagentChatUri(chatChannel)) { + return chatChannel; + } + + for (const subagent of this._subagentChats.values()) { + if (subagent.chatUri === chatChannel) { + return this._toolCallCompletionChat(subagent.parentChatUri); + } + } + + return chatChannel; + } + // ---- Side-effect handlers -------------------------------------------------- /** @@ -934,7 +949,7 @@ export class AgentSideEffects extends Disposable { break; // Not a chat channel; ignore. } const agent = this._options.getAgent(sessionChannel); - agent?.onClientToolCallComplete(URI.parse(sessionChannel), URI.parse(chatChannel), action.toolCallId, action.result); + agent?.onClientToolCallComplete(URI.parse(sessionChannel), URI.parse(this._toolCallCompletionChat(chatChannel)), action.toolCallId, action.result); break; } } diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index b44694bde1181d..d7020d2256014b 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -1484,16 +1484,9 @@ export class CopilotAgent extends Disposable implements IAgent { // keyed by the chat URI. Mirrors the routing in `sendMessage`. if (!isDefaultChatUri(chat)) { this._chatSessions.get(chat.toString())?.handleClientToolCallComplete(toolCallId, result); - return; - } - // Default chat (and subagents): walk up the subagent chain to reach the - // root SDK session entry, since `_sessions` is keyed by root session ids. - let target = session; - let parsed; - while ((parsed = parseSubagentSessionUri(target))) { - target = parsed.parentSession; + } else { + this._sessions.get(AgentSession.id(session))?.handleClientToolCallComplete(toolCallId, result); } - this._sessions.get(AgentSession.id(target))?.handleClientToolCallComplete(toolCallId, result); } setCustomizationEnabled(uri: string, enabled: boolean): void { diff --git a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts index 399260a66b418c..4dc08c7cca9f96 100644 --- a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts @@ -1921,6 +1921,35 @@ suite('AgentSideEffects', () => { [{ session: sessionUri.toString(), chat: peerChatUri, toolCallId: 'tc-peer' }], ); }); + + test('forwards parent peer chat URI for a subagent-chat completion', () => { + setupSession(); + const peerChatUri = buildChatUri(sessionUri.toString(), 'peer-subagent-parent'); + stateManager.addChat(sessionUri.toString(), peerChatUri); + startTurn('turn-peer', peerChatUri); + disposables.add(sideEffects.registerProgressListener(agent)); + + agent.fireProgress({ + kind: 'subagent_started', + chat: URI.parse(peerChatUri), + toolCallId: 'tc-parent', + agentName: 'explore', + agentDisplayName: 'Explore', + }); + + const subagentChatUri = buildSubagentChatUri(sessionUri.toString(), 'tc-parent'); + sideEffects.handleAction(subagentChatUri, { + type: ActionType.ChatToolCallComplete, + turnId: 'turn-subagent', + toolCallId: 'tc-inner', + result: { success: true, pastTenseMessage: 'done' }, + }); + + assert.deepStrictEqual( + agent.clientToolCallCompleteCalls.map(c => ({ session: c.session.toString(), chat: c.chat?.toString(), toolCallId: c.toolCallId })), + [{ session: sessionUri.toString(), chat: peerChatUri, toolCallId: 'tc-inner' }], + ); + }); }); // ---- Session-level auto-approve (config) ---------------------------- diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index 83085c348a52ee..07163832399eca 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -32,7 +32,7 @@ import { AgentHostConfigKey } from '../../common/agentHostCustomizationConfig.js import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js'; import { AgentSession, GITHUB_COPILOT_PROTECTED_RESOURCE, type AgentSignal, type IAgentActionSignal, type IAgentSessionMetadata } from '../../common/agentService.js'; import { ISessionDataService } from '../../common/sessionDataService.js'; -import { buildDefaultChatUri, buildSubagentSessionUri, buildChatUri, CustomizationLoadStatus, MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, customizationId, type ClientPluginCustomization, type MarkdownResponsePart, type PluginCustomization, type ToolCallResult, type Turn, RuleCustomization } from '../../common/state/sessionState.js'; +import { buildDefaultChatUri, buildChatUri, CustomizationLoadStatus, MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, customizationId, type ClientPluginCustomization, type MarkdownResponsePart, type PluginCustomization, type ToolCallResult, type Turn, RuleCustomization } from '../../common/state/sessionState.js'; import { CustomizationType, ToolCallContributorKind, type AgentSelection, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js'; import { ActionType, type ChatAction, type IDeltaAction, type SessionAction } from '../../common/state/sessionActions.js'; @@ -2207,50 +2207,6 @@ suite('CopilotAgent', () => { } }); - test('routes a subagent session URI to its parent session entry', async () => { - // Regression: client-tool completions for tools running inside a - // subagent are dispatched against the subagent session URI by - // the renderer. The agent must resolve that to the parent - // session entry — only the parent owns the SDK session and the - // pending deferred for the tool call. - const agent = createTestAgent(disposables); - try { - const parentUri = AgentSession.uri('copilotcli', 'session-parent'); - const defaultChat = URI.parse(buildDefaultChatUri(parentUri)); - const { calls } = installStubSession(agent, AgentSession.id(parentUri)); - - const subagentUri = URI.parse(buildSubagentSessionUri(parentUri.toString(), 'tc-parent')); - const result: ToolCallResult = { success: true, pastTenseMessage: 'subagent tool done' }; - agent.onClientToolCallComplete(subagentUri, defaultChat, 'tc-inner', result); - - assert.deepStrictEqual(calls, [{ toolCallId: 'tc-inner', result }]); - } finally { - await disposeAgent(agent); - } - }); - - test('routes a nested subagent session URI (depth > 1) to the root session entry', async () => { - // Regression for depth > 1: a nested subagent URI like - // `copilot:/root/subagent/tc1/subagent/tc2` must walk all the way - // to the root session entry in `_sessions`, not stop at the - // intermediate parent `copilot:/root/subagent/tc1`. - const agent = createTestAgent(disposables); - try { - const rootUri = AgentSession.uri('copilotcli', 'session-root'); - const defaultChat = URI.parse(buildDefaultChatUri(rootUri)); - const { calls } = installStubSession(agent, AgentSession.id(rootUri)); - - const subagentUri = URI.parse(buildSubagentSessionUri(rootUri.toString(), 'tc-parent')); - const nestedUri = URI.parse(buildSubagentSessionUri(subagentUri.toString(), 'tc-nested')); - const result: ToolCallResult = { success: true, pastTenseMessage: 'nested done' }; - agent.onClientToolCallComplete(nestedUri, defaultChat, 'tc-inner', result); - - assert.deepStrictEqual(calls, [{ toolCallId: 'tc-inner', result }]); - } finally { - await disposeAgent(agent); - } - }); - test('is a no-op when no session entry exists for the resolved id', async () => { const agent = createTestAgent(disposables); try { From eaa14316aa4e333e11ead7b450c7c1d3d27041ef Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:21:34 +0000 Subject: [PATCH 065/589] [cherry-pick] Revert PR 323316 (#323544) Co-authored-by: vs-code-engineering[bot] --- .../sessions/browser/actions/vscodeActions.ts | 38 +++++++++++-- src/vs/sessions/browser/openInVSCodeUtils.ts | 45 --------------- .../electron-browser/actions/vscodeActions.ts | 41 ++++++++------ .../browser/resolveRemoteAuthority.test.ts | 56 +------------------ 4 files changed, 57 insertions(+), 123 deletions(-) diff --git a/src/vs/sessions/browser/actions/vscodeActions.ts b/src/vs/sessions/browser/actions/vscodeActions.ts index b8fecad2738719..59d159c1035472 100644 --- a/src/vs/sessions/browser/actions/vscodeActions.ts +++ b/src/vs/sessions/browser/actions/vscodeActions.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../base/common/codicons.js'; +import { Schemas } from '../../../base/common/network.js'; +import { URI } from '../../../base/common/uri.js'; import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js'; import { localize2 } from '../../../nls.js'; import { Action2 } from '../../../platform/actions/common/actions.js'; @@ -23,7 +25,7 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { IActionViewItemService } from '../../../platform/actions/browser/actionViewItemService.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution } from '../../../workbench/common/contributions.js'; -import { getOpenInVSCodeUri, getVSCodeProtocolScheme, resolveRemoteAuthority } from '../openInVSCodeUtils.js'; +import { resolveRemoteAuthority } from '../openInVSCodeUtils.js'; import { OpenInVSCodeTitleBarWidget } from '../widget/openInVSCodeWidget.js'; export class OpenInVSCodeAction extends Action2 { @@ -53,11 +55,21 @@ export class OpenInVSCodeAction extends Action2 { const sessionsService = accessor.get(ISessionsService); const sessionsProvidersService = accessor.get(ISessionsProvidersService); const remoteAgentHostService = accessor.get(IRemoteAgentHostService); - const scheme = getVSCodeProtocolScheme(productService); + + const scheme = productService.quality === 'stable' + ? 'vscode' + : productService.quality === 'exploration' + ? 'vscode-exploration' + : productService.quality === 'insider' + ? 'vscode-insiders' + : productService.urlProtocol; + + const params = new URLSearchParams(); + params.set('windowId', '_blank'); const activeSession = sessionsService.activeSession.get(); if (!activeSession) { - await openerService.open(getOpenInVSCodeUri(scheme, undefined, undefined, undefined), { openExternal: true }); + await openerService.open(URI.from({ scheme, query: params.toString() }), { openExternal: true }); return; } @@ -66,7 +78,7 @@ export class OpenInVSCodeAction extends Action2 { const rawFolderUri = workspace?.isVirtualWorkspace ? undefined : folder?.workingDirectory; if (!rawFolderUri) { - await openerService.open(getOpenInVSCodeUri(scheme, undefined, undefined, undefined), { openExternal: true }); + await openerService.open(URI.from({ scheme, query: params.toString() }), { openExternal: true }); return; } @@ -74,7 +86,23 @@ export class OpenInVSCodeAction extends Action2 { const remoteAuthority = resolveRemoteAuthority( activeSession.providerId, sessionsProvidersService, remoteAgentHostService); - await openerService.open(getOpenInVSCodeUri(scheme, folderUri, remoteAuthority, activeSession.resource), { openExternal: true }); + params.set('session', activeSession.resource.toString()); + + if (remoteAuthority) { + await openerService.open(URI.from({ + scheme, + authority: Schemas.vscodeRemote, + path: `/${remoteAuthority}${folderUri.path}`, + query: params.toString(), + }), { openExternal: true }); + } else { + await openerService.open(URI.from({ + scheme, + authority: Schemas.file, + path: folderUri.path, + query: params.toString(), + }), { openExternal: true }); + } } } diff --git a/src/vs/sessions/browser/openInVSCodeUtils.ts b/src/vs/sessions/browser/openInVSCodeUtils.ts index 6944305ef150e4..02e47dff63d289 100644 --- a/src/vs/sessions/browser/openInVSCodeUtils.ts +++ b/src/vs/sessions/browser/openInVSCodeUtils.ts @@ -7,9 +7,6 @@ import { IRemoteAgentHostService, IRemoteAgentHostSSHConnection, RemoteAgentHost import { ISessionsProvidersService } from '../services/sessions/browser/sessionsProvidersService.js'; import { isAgentHostProvider } from '../common/agentHostSessionsProvider.js'; import { encodeHex, VSBuffer } from '../../base/common/buffer.js'; -import { Schemas } from '../../base/common/network.js'; -import { IProductService } from '../../platform/product/common/productService.js'; -import { URI } from '../../base/common/uri.js'; /** * Resolves the VS Code remote authority for the given session provider, @@ -69,45 +66,3 @@ export function sshAuthorityString(connection: IRemoteAgentHostSSHConnection): s const json = JSON.stringify(obj); return encodeHex(VSBuffer.fromString(json)); } - -export function getVSCodeProtocolScheme(productService: Pick): string { - switch (productService.quality) { - case 'stable': - return 'vscode'; - case 'exploration': - return 'vscode-exploration'; - case 'insider': - return 'vscode-insiders'; - default: - return productService.urlProtocol; - } -} - -export function getOpenInVSCodeUri(scheme: string, folderUri: URI | undefined, remoteAuthority: string | undefined, sessionResource: URI | undefined): URI { - const params = new URLSearchParams(); - params.set('windowId', '_blank'); - - if (sessionResource) { - params.set('session', sessionResource.toString()); - } - - if (!folderUri) { - return URI.from({ scheme, query: params.toString() }); - } - - if (remoteAuthority) { - return URI.from({ - scheme, - authority: Schemas.vscodeRemote, - path: `/${remoteAuthority}${folderUri.path}`, - query: params.toString(), - }); - } - - return URI.from({ - scheme, - authority: Schemas.file, - path: folderUri.path, - query: params.toString(), - }); -} diff --git a/src/vs/sessions/electron-browser/actions/vscodeActions.ts b/src/vs/sessions/electron-browser/actions/vscodeActions.ts index dfa87e4f57e5a7..b2606fd2973d8d 100644 --- a/src/vs/sessions/electron-browser/actions/vscodeActions.ts +++ b/src/vs/sessions/electron-browser/actions/vscodeActions.ts @@ -6,6 +6,8 @@ import { Codicon } from '../../../base/common/codicons.js'; import { getWindowId } from '../../../base/browser/dom.js'; import { mainWindow } from '../../../base/browser/window.js'; +import { Schemas } from '../../../base/common/network.js'; +import { URI } from '../../../base/common/uri.js'; import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js'; import { localize2 } from '../../../nls.js'; import { Action2 } from '../../../platform/actions/common/actions.js'; @@ -14,8 +16,6 @@ import { IRemoteAgentHostService } from '../../../platform/agentHost/common/remo import { KeyCode, KeyMod } from '../../../base/common/keyCodes.js'; import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js'; import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IOpenerService } from '../../../platform/opener/common/opener.js'; -import { IProductService } from '../../../platform/product/common/productService.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { IsAuxiliaryWindowContext } from '../../../workbench/common/contextkeys.js'; import { IsPhoneLayoutContext, SessionsWelcomeVisibleContext } from '../../common/contextkeys.js'; @@ -28,7 +28,7 @@ import { OpenInVSCodeTitleBarWidget } from '../../browser/widget/openInVSCodeWid import { IActionViewItemService } from '../../../platform/actions/browser/actionViewItemService.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { Disposable } from '../../../base/common/lifecycle.js'; -import { getOpenInVSCodeUri, getVSCodeProtocolScheme, resolveRemoteAuthority } from '../../browser/openInVSCodeUtils.js'; +import { resolveRemoteAuthority } from '../../browser/openInVSCodeUtils.js'; import { INativeHostService } from '../../../platform/native/common/native.js'; export class OpenSessionInVSCodeAction extends Action2 { @@ -53,36 +53,41 @@ export class OpenSessionInVSCodeAction extends Action2 { const telemetryService = accessor.get(ITelemetryService); logSessionsInteraction(telemetryService, 'openInVSCode'); - const openerService = accessor.get(IOpenerService); - const productService = accessor.get(IProductService); - const nativeHostService = accessor.get(INativeHostService); const sessionsService = accessor.get(ISessionsService); const sessionsProvidersService = accessor.get(ISessionsProvidersService); const remoteAgentHostService = accessor.get(IRemoteAgentHostService); - const scheme = getVSCodeProtocolScheme(productService); + const nativeHostService = accessor.get(INativeHostService); + const folderUri = this.getFolderUriToOpen(sessionsService, sessionsProvidersService, remoteAgentHostService); + if (!folderUri) { + return nativeHostService.openWindow(); + } + return nativeHostService.openWindow([{ folderUri }], { forceNewWindow: true }); + } + + private getFolderUriToOpen(sessionsService: ISessionsService, sessionsProvidersService: ISessionsProvidersService, remoteAgentHostService: IRemoteAgentHostService): URI | undefined { const activeSession = sessionsService.activeSession.get(); if (!activeSession) { - await openerService.open(getOpenInVSCodeUri(scheme, undefined, undefined, undefined), { openExternal: true }); - return; + return undefined; } const workspace = activeSession.workspace.get(); - const folder = workspace?.folders[0]; - const rawFolderUri = folder?.workingDirectory; + const rawFolderUri = workspace?.folders[0]?.workingDirectory; if (!rawFolderUri) { - await openerService.open(getOpenInVSCodeUri(scheme, undefined, undefined, undefined), { openExternal: true }); - return; + return undefined; } - if (workspace?.isVirtualWorkspace) { - await nativeHostService.openWindow([{ folderUri: rawFolderUri }], { forceNewWindow: true }); - return; + if (rawFolderUri.scheme !== AGENT_HOST_SCHEME) { + return rawFolderUri; } - const folderUri = rawFolderUri.scheme === AGENT_HOST_SCHEME ? fromAgentHostUri(rawFolderUri) : rawFolderUri; const remoteAuthority = resolveRemoteAuthority(activeSession.providerId, sessionsProvidersService, remoteAgentHostService); - await openerService.open(getOpenInVSCodeUri(scheme, folderUri, remoteAuthority, activeSession.resource), { openExternal: true }); + if (!remoteAuthority) { + return rawFolderUri; + } + + const agentHostUri = fromAgentHostUri(rawFolderUri); + return agentHostUri.with({ authority: remoteAuthority, scheme: Schemas.vscodeRemote }); } } diff --git a/src/vs/sessions/test/browser/resolveRemoteAuthority.test.ts b/src/vs/sessions/test/browser/resolveRemoteAuthority.test.ts index e1b9f6b944f16b..1d9701f92d4dd1 100644 --- a/src/vs/sessions/test/browser/resolveRemoteAuthority.test.ts +++ b/src/vs/sessions/test/browser/resolveRemoteAuthority.test.ts @@ -5,10 +5,9 @@ import assert from 'assert'; import { decodeHex } from '../../../base/common/buffer.js'; -import { URI } from '../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; import { IRemoteAgentHostEntry, IRemoteAgentHostService, getEntryAddress, RemoteAgentHostEntryType } from '../../../platform/agentHost/common/remoteAgentHostService.js'; -import { getOpenInVSCodeUri, getVSCodeProtocolScheme, resolveRemoteAuthority, sshAuthorityString } from '../../browser/openInVSCodeUtils.js'; +import { resolveRemoteAuthority, sshAuthorityString } from '../../browser/openInVSCodeUtils.js'; import { ISessionsProvidersService } from '../../services/sessions/browser/sessionsProvidersService.js'; suite('resolveRemoteAuthority', () => { @@ -232,56 +231,3 @@ suite('sshAuthorityString', () => { assert.strictEqual(result, 'actualhost'); }); }); - -suite('Open in Editor URI', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('preserves the session resource when opening a local workspace', () => { - const uri = getOpenInVSCodeUri( - getVSCodeProtocolScheme({ quality: 'insider', urlProtocol: 'vscode-insiders' }), - URI.file('/workspace'), - undefined, - URI.parse('agent-host-copilotcli:/session-id'), - ); - - assert.deepStrictEqual({ - scheme: uri.scheme, - authority: uri.authority, - path: uri.path, - params: Object.fromEntries(new URLSearchParams(uri.query)), - }, { - scheme: 'vscode-insiders', - authority: 'file', - path: '/workspace', - params: { - windowId: '_blank', - session: 'agent-host-copilotcli:/session-id', - }, - }); - }); - - test('uses the remote workspace protocol URI', () => { - const uri = getOpenInVSCodeUri( - getVSCodeProtocolScheme({ quality: 'stable', urlProtocol: 'vscode' }), - URI.file('/workspace'), - 'ssh-remote+host', - URI.parse('agent-host-copilotcli:/session-id'), - ); - - assert.deepStrictEqual({ - scheme: uri.scheme, - authority: uri.authority, - path: uri.path, - params: Object.fromEntries(new URLSearchParams(uri.query)), - }, { - scheme: 'vscode', - authority: 'vscode-remote', - path: '/ssh-remote+host/workspace', - params: { - windowId: '_blank', - session: 'agent-host-copilotcli:/session-id', - }, - }); - }); -}); From 1ae64cf31d56c7e994b1c0126b87a1948f231b23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:43:49 -0700 Subject: [PATCH 066/589] build(deps): bump js-yaml and @secretlint/formatter in /extensions/copilot (#323529) build(deps): bump js-yaml and @secretlint/formatter Bumps [js-yaml](https://github.com/nodeca/js-yaml) to 4.3.0 and updates ancestor dependency [@secretlint/formatter](https://github.com/secretlint/secretlint). These dependencies need to be updated together. Updates `js-yaml` from 4.1.1 to 4.3.0 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.3.0) Updates `@secretlint/formatter` from 10.1.1 to 10.2.2 - [Release notes](https://github.com/secretlint/secretlint/releases) - [Commits](https://github.com/secretlint/secretlint/compare/v10.1.1...v10.2.2) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.3.0 dependency-type: direct:development - dependency-name: "@secretlint/formatter" dependency-version: 10.2.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/copilot/package-lock.json | 303 ++++++++++++++------------- extensions/copilot/package.json | 2 +- 2 files changed, 158 insertions(+), 147 deletions(-) diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index 2da3e52e7073e4..5c0ac774e2612e 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -106,7 +106,7 @@ "esbuild": "0.28.1", "fastq": "^1.19.1", "glob": "^11.1.0", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimist": "^1.2.8", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", @@ -5488,28 +5488,70 @@ } }, "node_modules/@secretlint/formatter": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.1.1.tgz", - "integrity": "sha512-Gpd8gTPN121SJ0h/9e6nWlZU7PitfhXUiEzW7Kyswg6kNGs+bSqmgTgWFtbo1VQ4ygJYiveWPNT05RCImBexJw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/resolver": "^10.1.1", - "@secretlint/types": "^10.1.1", - "@textlint/linter-formatter": "^14.8.4", - "@textlint/module-interop": "^14.8.4", - "@textlint/types": "^14.8.4", - "chalk": "^4.1.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", "debug": "^4.4.1", "pluralize": "^8.0.0", - "strip-ansi": "^6.0.1", + "strip-ansi": "^7.1.0", "table": "^6.9.0", - "terminal-link": "^2.1.1" + "terminal-link": "^4.0.0" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@secretlint/formatter/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/formatter/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@secretlint/node": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.1.1.tgz", @@ -5538,9 +5580,9 @@ "license": "MIT" }, "node_modules/@secretlint/resolver": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.1.1.tgz", - "integrity": "sha512-GdQzxnBtdBRjBULvZ8ERkaRqDp0njVwXrzBCav1pb0XshVk76C1cjeDqtTqM4RJ1Awo/g5U5MIWYztYv67v5Gg==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", "dev": true, "license": "MIT" }, @@ -5592,9 +5634,9 @@ } }, "node_modules/@secretlint/types": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.1.1.tgz", - "integrity": "sha512-/JGAvVkurVHkargk3AC7UxRy+Ymc+52AVBO/fZA5pShuLW2dX4O/rKc4n8cyhQiOb/3ym5ACSlLQuQ8apPfxrQ==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", "dev": true, "license": "MIT", "engines": { @@ -5721,28 +5763,28 @@ } }, "node_modules/@textlint/ast-node-types": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-14.8.4.tgz", - "integrity": "sha512-+fI7miec/r9VeniFV9ppL4jRCmHNsTxieulTUf/4tvGII3db5hGriKHC4p/diq1SkQ9Sgs7kg6UyydxZtpTz1Q==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-14.8.4.tgz", - "integrity": "sha512-sZ0UfYRDBNHnfMVBqLqqYnqTB7Ec169ljlmo+SEHR1T+dHUPYy1/DZK4p7QREXlBSFL4cnkswETCbc9xRodm4Q==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", "dev": true, "license": "MIT", "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "14.8.4", - "@textlint/resolver": "14.8.4", - "@textlint/types": "14.8.4", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", "chalk": "^4.1.2", - "debug": "^4.4.1", - "js-yaml": "^3.14.1", - "lodash": "^4.17.21", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", @@ -5750,16 +5792,6 @@ "text-table": "^0.2.0" } }, - "node_modules/@textlint/linter-formatter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/@textlint/linter-formatter/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5767,20 +5799,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@textlint/linter-formatter/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@textlint/linter-formatter/node_modules/pluralize": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", @@ -5788,13 +5806,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@textlint/linter-formatter/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@textlint/linter-formatter/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5811,27 +5822,27 @@ } }, "node_modules/@textlint/module-interop": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-14.8.4.tgz", - "integrity": "sha512-1LdPYLAVpa27NOt6EqvuFO99s4XLB0c19Hw9xKSG6xQ1K82nUEyuWhzTQKb3KJ5Qx7qj14JlXZLfnEuL6A16Bw==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-14.8.4.tgz", - "integrity": "sha512-nMDOgDAVwNU9ommh+Db0U+MCMNDPbQ/1HBNjbnHwxZkCpcT6hsAJwBe38CW/DtWVUv8yeR4R40IYNPT84srNwA==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { - "version": "14.8.4", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-14.8.4.tgz", - "integrity": "sha512-9nyY8vVXlr8hHKxa6+37omJhXWCwovMQcgMteuldYd4dOxGm14AK2nXdkgtKEUQnzLGaXy46xwLCfhQy7V7/YA==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "14.8.4" + "@textlint/ast-node-types": "15.7.1" } }, "node_modules/@tybys/wasm-util": { @@ -7446,6 +7457,22 @@ } } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -9323,6 +9350,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -9583,20 +9623,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -11381,10 +11407,20 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -15259,6 +15295,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -15639,9 +15693,9 @@ } }, "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, "license": "MIT", "dependencies": { @@ -15649,7 +15703,10 @@ "supports-color": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -15693,24 +15750,6 @@ "dev": true, "license": "MIT" }, - "node_modules/table/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/table/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -15892,46 +15931,17 @@ } }, "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-link/node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.21.3" + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-link/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -15951,7 +15961,8 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/textextensions": { "version": "6.11.0", diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 3b4bdeab13be21..527fbf28abf169 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -7155,7 +7155,7 @@ "esbuild": "0.28.1", "fastq": "^1.19.1", "glob": "^11.1.0", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimist": "^1.2.8", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", From 3cc09c02ef59b8ccfddb158e6f3e6c2acd39dcce Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Mon, 29 Jun 2026 20:45:15 +0200 Subject: [PATCH 067/589] Record proxy type (#323585) --- .../log/vscode-node/loggingActions.ts | 38 +++++++++++++++++++ src/vs/workbench/api/node/proxyResolver.ts | 18 ++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/extensions/copilot/src/extension/log/vscode-node/loggingActions.ts b/extensions/copilot/src/extension/log/vscode-node/loggingActions.ts index 0c654168a52028..627b9e366120eb 100644 --- a/extensions/copilot/src/extension/log/vscode-node/loggingActions.ts +++ b/extensions/copilot/src/extension/log/vscode-node/loggingActions.ts @@ -471,6 +471,38 @@ function getProxyEnvVariables() { return res.length ? `\n\nEnvironment Variables:${res.join('')}` : ''; } +/** + * Resolves the proxy type for a URL using the bundled `@vscode/proxy-agent` module. + * Returns one of `DIRECT`, `PROXY`, `HTTPS`, `SOCKS` or `UNKNOWN`. + */ +async function resolveProxyType(url: string, logService: ILogService): Promise { + try { + const proxyAgent = loadVSCodeModule('@vscode/proxy-agent'); + if (!proxyAgent?.resolveProxyURL) { + return 'UNKNOWN'; + } + const proxyURL = await Promise.race([proxyAgent.resolveProxyURL(url), timeoutAfter(5000)]); + if (proxyURL === 'timeout') { + return 'UNKNOWN'; + } + if (!proxyURL) { + return 'DIRECT'; + } + const scheme = proxyURL.split(':', 1)[0].toLowerCase(); + switch (scheme) { + case 'http': return 'PROXY'; + case 'https': return 'HTTPS'; + case 'socks': + case 'socks4': + case 'socks5': return 'SOCKS'; + default: return 'UNKNOWN'; + } + } catch (err) { + logService.debug(`Fetcher telemetry: Failed to resolve proxy type: ${err?.message}`); + return 'UNKNOWN'; + } +} + export class FetcherTelemetryContribution { constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -485,6 +517,7 @@ function collectFetcherTelemetry(accessor: ServicesAccessor): void { const logService = accessor.get(ILogService); const configurationService = accessor.get(IConfigurationService); const expService = accessor.get(IExperimentationService); + const capiClientService = accessor.get(ICAPIClientService); if (!vscode.env.isTelemetryEnabled || extensionContext.extensionMode !== vscode.ExtensionMode.Production || isScenarioAutomation) { return; @@ -539,6 +572,9 @@ function collectFetcherTelemetry(accessor: ServicesAccessor): void { } } + // Resolve the proxy type for the CAPI endpoint (e.g. DIRECT, PROXY, HTTPS, SOCKS). + const proxyType = await resolveProxyType(capiClientService.capiPingURL, logService); + // Second loop: send the actual telemetry event including probe results. const requestGroupId = generateUuid(); const extensionKind = extensionContext.extension.extensionKind === vscode.ExtensionKind.UI ? 'local' : 'remote'; @@ -553,6 +589,7 @@ function collectFetcherTelemetry(accessor: ServicesAccessor): void { "clientLibrary": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The fetcher library used for this request." }, "extensionKind": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the extension runs locally or remotely." }, "remoteName": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The remote name, if any." }, + "proxyType": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The resolved proxy type for the CAPI endpoint (e.g. DIRECT, PROXY, HTTPS, SOCKS, UNKNOWN)." }, "electronfetch": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Probe result for the electron-fetch fetcher." }, "nodefetch": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Probe result for the node-fetch fetcher." }, "nodehttp": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Probe result for the node-http fetcher." } @@ -563,6 +600,7 @@ function collectFetcherTelemetry(accessor: ServicesAccessor): void { clientLibrary: fetcher.getUserAgentLibrary(), extensionKind, remoteName: vscode.env.remoteName ?? 'none', + proxyType, ...probeResults, }; const response = await sendRawTelemetry(fetcher, envService, oneCollectorTelemetryUrl, extensionContext, 'GitHub.copilot-chat/fetcherTelemetry', properties); diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index f06627f88c2f8a..9b40580185cb35 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -298,6 +298,7 @@ type ProxyResolveStatsClassification = { minDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Minimum resolution time (ms)' }; maxDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Maximum resolution time (ms)' }; avgDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Average resolution time (ms)' }; + type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Sorted, comma-separated list of resolved proxy types seen during the interval (e.g. DIRECT, PROXY, HTTPS, SOCKS, EMPTY, UNKNOWN)' }; }; type ProxyResolveStatsEvent = { @@ -306,6 +307,7 @@ type ProxyResolveStatsEvent = { minDuration: number; maxDuration: number; avgDuration: number; + type: string; }; const proxyResolveStats = { @@ -313,11 +315,20 @@ const proxyResolveStats = { totalDuration: 0, minDuration: Number.MAX_SAFE_INTEGER, maxDuration: 0, + types: new Set(), lastSentTime: 0, }; const telemetryInterval = 60 * 60 * 1000; // 1 hour +function proxyResolveType(proxy: string | undefined): string { + const type = proxy ? String(proxy).trim().split(/\s+/, 1)[0] : 'EMPTY'; + if (['DIRECT', 'PROXY', 'HTTPS', 'SOCKS', 'EMPTY'].indexOf(type) === -1) { + return 'UNKNOWN'; + } + return type; +} + function sendProxyResolveStats(mainThreadTelemetry: MainThreadTelemetryShape) { if (proxyResolveStats.count > 0) { const avgDuration = proxyResolveStats.totalDuration / proxyResolveStats.count; @@ -327,12 +338,14 @@ function sendProxyResolveStats(mainThreadTelemetry: MainThreadTelemetryShape) { minDuration: proxyResolveStats.minDuration, maxDuration: proxyResolveStats.maxDuration, avgDuration, + type: [...proxyResolveStats.types].sort().join(','), }); // Reset stats after sending proxyResolveStats.count = 0; proxyResolveStats.totalDuration = 0; proxyResolveStats.minDuration = Number.MAX_SAFE_INTEGER; proxyResolveStats.maxDuration = 0; + proxyResolveStats.types.clear(); } proxyResolveStats.lastSentTime = Date.now(); } @@ -340,14 +353,17 @@ function sendProxyResolveStats(mainThreadTelemetry: MainThreadTelemetryShape) { function createTimedResolveProxy(extHostWorkspace: IExtHostWorkspaceProvider, mainThreadTelemetry: MainThreadTelemetryShape) { return async (url: string): Promise => { const startTime = performance.now(); + let proxy: string | undefined; try { - return await extHostWorkspace.resolveProxy(url); + proxy = await extHostWorkspace.resolveProxy(url); + return proxy; } finally { const duration = performance.now() - startTime; proxyResolveStats.count++; proxyResolveStats.totalDuration += duration; proxyResolveStats.minDuration = Math.min(proxyResolveStats.minDuration, duration); proxyResolveStats.maxDuration = Math.max(proxyResolveStats.maxDuration, duration); + proxyResolveStats.types.add(proxyResolveType(proxy)); // Send telemetry if at least an hour has passed since last send const now = Date.now(); From 56573d693076bf4babed1b3cedff49783ad8813e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:45:31 -0700 Subject: [PATCH 068/589] build(deps): bump markdown-it from 14.1.1 to 14.2.0 in /extensions/copilot (#321789) build(deps): bump markdown-it in /extensions/copilot Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 14.1.1 to 14.2.0. - [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) - [Commits](https://github.com/markdown-it/markdown-it/compare/14.1.1...14.2.0) --- updated-dependencies: - dependency-name: markdown-it dependency-version: 14.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/copilot/package-lock.json | 37 ++++++++++++++++++++++------ extensions/copilot/package.json | 2 +- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index 5c0ac774e2612e..722f833e9306d9 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -51,7 +51,7 @@ "isbinaryfile": "^5.0.4", "jsonc-parser": "^3.3.1", "lru-cache": "^11.1.0", - "markdown-it": "^14.1.1", + "markdown-it": "^14.2.0", "minimatch": "^10.2.1", "undici": "^7.24.1", "vscode-tas-client": "^0.1.84", @@ -12060,9 +12060,20 @@ } }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" } @@ -12261,14 +12272,24 @@ } }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 527fbf28abf169..e307377d8d2c44 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -7232,7 +7232,7 @@ "isbinaryfile": "^5.0.4", "jsonc-parser": "^3.3.1", "lru-cache": "^11.1.0", - "markdown-it": "^14.1.1", + "markdown-it": "^14.2.0", "minimatch": "^10.2.1", "undici": "^7.24.1", "vscode-tas-client": "^0.1.84", From 2fd704bc6883d4dba4de1390225fe2c3953a5408 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:05:37 +0000 Subject: [PATCH 069/589] build(deps): bump markdown-it from 12.3.2 to 14.2.0 in /extensions/markdown-language-features (#321532) build(deps): bump markdown-it in /extensions/markdown-language-features Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 12.3.2 to 14.2.0. - [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) - [Commits](https://github.com/markdown-it/markdown-it/compare/12.3.2...14.2.0) --- updated-dependencies: - dependency-name: markdown-it dependency-version: 14.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> --- .../package-lock.json | 80 ++++++++++++------- .../markdown-language-features/package.json | 2 +- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/extensions/markdown-language-features/package-lock.json b/extensions/markdown-language-features/package-lock.json index 60e0349caf9edf..de3037536a9999 100644 --- a/extensions/markdown-language-features/package-lock.json +++ b/extensions/markdown-language-features/package-lock.json @@ -14,7 +14,7 @@ "dompurify": "^3.4.10", "highlight.js": "^11.8.0", "katex": "^0.16.33", - "markdown-it": "^12.3.2", + "markdown-it": "^14.2.0", "mermaid": "^11.15.0", "morphdom": "^2.7.7", "picomatch": "^2.3.2", @@ -1474,11 +1474,22 @@ "license": "MIT" }, "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", "dependencies": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, "node_modules/lodash-es": { @@ -1505,26 +1516,30 @@ } }, "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.1", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, - "node_modules/markdown-it/node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/marked": { @@ -1540,9 +1555,10 @@ } }, "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" }, "node_modules/mermaid": { "version": "11.15.0", @@ -2227,6 +2243,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -2296,9 +2321,10 @@ } }, "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" }, "node_modules/undici-types": { "version": "7.16.0", diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index e6812f69107c83..45c08f7dc56044 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -903,7 +903,7 @@ "dompurify": "^3.4.10", "highlight.js": "^11.8.0", "katex": "^0.16.33", - "markdown-it": "^12.3.2", + "markdown-it": "^14.2.0", "mermaid": "^11.15.0", "morphdom": "^2.7.7", "picomatch": "^2.3.2", From ba4447fbf145634fc161cb9542358b083b6f9f62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:08:45 +0000 Subject: [PATCH 070/589] build(deps): bump linkify-it from 5.0.0 to 5.0.1 in /extensions/copilot (#323367) Bumps [linkify-it](https://github.com/markdown-it/linkify-it) from 5.0.0 to 5.0.1. - [Changelog](https://github.com/markdown-it/linkify-it/blob/master/CHANGELOG.md) - [Commits](https://github.com/markdown-it/linkify-it/compare/5.0.0...5.0.1) --- updated-dependencies: - dependency-name: linkify-it dependency-version: 5.0.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> From b86f454a6ffb172244c45bab2ae12051477fb7f1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 29 Jun 2026 21:55:00 +0200 Subject: [PATCH 071/589] sessions: fix chat tab ordering in the Agents window (#323597) Chat tabs reordered for two reasons. (1) The renderer partitioned chats by status, pushing in-composer Untitled chats to the end, so committing a draft out of creation order made its tab jump. (2) restoreSession seeded peer chats in Promise.all resolution order rather than getChats() order, scrambling the catalog on reload. Render the provider's stable creation order as-is, and restore peer chats in getChats() order while still loading their data in parallel. Update the sessions skill pitfall to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/sessions/SKILL.md | 2 +- .../platform/agentHost/node/agentService.ts | 10 ++++-- .../agentHost/test/node/agentService.test.ts | 34 ++++++++++++++++++- .../browser/parts/chatCompositeBar.ts | 12 +------ 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index ff1bdfea16ee80..7e1c331b836bfa 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -50,7 +50,7 @@ Then read the relevant spec for the area you are changing (see table below). If - **Peer chats have no server `summary`, so dedup side-channel dispatch against the last value sent for that chat**: equality guards before dispatching `SessionModelChanged`/`SessionAgentChanged` compare against `summary.model`/`summary.agent`, which only exist for the session's default chat. For peer chats, track the last-dispatched model/agent on the `AgentHostChatSession` instance (auto-cleaned on dispose) and diff against that — otherwise every peer-chat turn redundantly re-dispatches (and re-resolves the agent), and an intentional "clear selection" (`undefined`) can't be detected. - **Scrollable transcript surfaces must use workbench scrollbars**: Don't make Agents/voice transcript regions scrollable with native `overflow-y: auto` on the content node. Wrap transcript content in `DomScrollableElement`/list widgets so scrollbars match VS Code theming and remain usable in narrow auxiliary-window layouts. - **Background-sending a multi-chat composer must reset the composer *before* dispatching the send, not concurrently**: in `NewChatInSessionWidget._send`, creating the replacement untitled chat (`openNewChatInSession({ forceNew: true })` → `provider.createNewChat`) and the fire-and-forget background `sendRequest` both reach into shared chat-session state (`acquireOrLoadSession` / `getOrCreateChatSession`) for chats in the **same group**. Running them concurrently (send first, reset second) raced and left the sent chat stuck spinning with its message never dispatched, plus a second empty "New Chat" tab. Fully `await` the composer reset first, then fire the background send so it runs on its own. -- **Chat tab order belongs in the renderer, not the providers**: providers report chats in unstable orders — the agent host re-sorts its `state.chats` catalog when a chat finishes a turn (moving the just-completed one last). Don't try to fix a "tab jumps to the end on completion" bug inside one provider (it won't cover the others). Instead keep the provider's order in the renderer's rebuild autorun (`chatCompositeBar.ts`) and only move in-composer `Untitled` chats to the end. +- **Chat tab order is the provider's stable creation order; don't reorder in the renderer**: the agent host delivers `state.chats` in stable creation order (append on add, replace-in-place on update — see `agentHostStateManager`/the session reducer), and a genuinely new chat is appended last. The renderer's rebuild autorun (`chatCompositeBar.ts`) must render that order as-is. Do **not** partition/move in-composer `Untitled` chats to the end: a draft is already last, and reordering by status makes a tab jump when a draft commits out of creation order (e.g. sending the 3rd of three drafts first moved it to the front). A chat's `Untitled` presentation (via `AdditionalChat._isNew`, needed so `sessionView.ts` shows the composer) is independent of tab order and must not drive it. Also note `_restorePeerChats` (`agentService.ts`) must seed restored chats in `getChats()` order, not in `Promise.all` resolution order, or the catalog scrambles on reload. - **A new chat must report `SessionStatus.Untitled` until its first request is sent, regardless of how the provider creates it**: `sessionView.ts` only shows the new-chat composer (which owns the Alt+Enter background-send handler) when `activeChat.status === Untitled`. The agent host commits a new peer chat *eagerly*, so its host status is `Completed` — surfacing the standard chat widget and breaking background send. Gate the chat's presented status on a provider-side `isNew` flag (`AdditionalChat.markNew`/`markSent`, set in `createNewChat` and cleared in `sendRequest`'s committed-chat branch), not on the host-reported status. - **Service operations should return a result or throw, not `undefined` for unsupported cases**: capability-gated operations like `forkChatInSession` must throw when the provider/session cannot perform them. Keep fallback decisions in the caller before invoking the service instead of encoding fallback as an `undefined` service result. - **Drop a fork when its turn point is unknown, don't forward it empty**: in `AgentService.createChat`/`createSession`, if the requested fork `turnId`/`turnIndex` resolves to no source turns, set `fork: undefined` and fall through to a fresh create. Forwarding the fork with an empty turn slice makes the Copilot provider call `sessions.fork` with no `toEventId`, inheriting the entire backend conversation while the new chat UI is seeded with zero turns — an inconsistent hidden-history chat. diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 508a9e6eec063f..ebd1f29200c7fe 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -1722,7 +1722,10 @@ export class AgentService extends Disposable implements IAgentService { if (chats.length === 0) { return; } - await Promise.all(chats.map(async (chatUri) => { + // Load each chat's data in parallel but restore in the order `getChats` + // returned (creation order), so the catalog never reorders by which + // chat's history/title happened to resolve first. + const restored = await Promise.all(chats.map(async (chatUri) => { let turns: readonly Turn[] = []; try { turns = await agent.getSessionMessages(chatUri); @@ -1733,8 +1736,11 @@ export class AgentService extends Disposable implements IAgentService { this._readPersistedChatTitle(session, chatUri), this._getChatDraft(session, chatUri), ]); - this._stateManager.restoreChat(session.toString(), chatUri.toString(), { title, turns: [...turns], draft }); + return { chatUri, title, turns: [...turns], draft }; })); + for (const { chatUri, title, turns, draft } of restored) { + this._stateManager.restoreChat(session.toString(), chatUri.toString(), { title, turns, draft }); + } } /** Reads a peer chat's persisted custom title, if any. */ diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index 41884cd379d0e3..3fc344226479c2 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -26,7 +26,7 @@ import { AgentSession, GITHUB_COPILOT_PROTECTED_RESOURCE, IRestoredSubagentSessi import { ISessionDatabase, ISessionDataService } from '../../common/sessionDataService.js'; import { SessionDatabase } from '../../node/sessionDatabase.js'; import { ActionType, ActionEnvelope } from '../../common/state/sessionActions.js'; -import { ChangesetStatus, CustomizationType, MessageAttachmentKind, MessageKind, SessionActiveClient, ResponsePartKind, ROOT_STATE_URI, SessionLifecycle, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, buildChatUri, buildDefaultChatUri, buildSubagentSessionUri, customizationId, isSubagentSession, parseSubagentSessionUri, type ChangesetState, type MarkdownResponsePart, type ToolCallCompletedState, type ToolCallResponsePart, type Turn } from '../../common/state/sessionState.js'; +import { ChangesetStatus, CustomizationType, MessageAttachmentKind, MessageKind, SessionActiveClient, ResponsePartKind, ROOT_STATE_URI, SessionLifecycle, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, buildChatUri, buildDefaultChatUri, buildSubagentSessionUri, customizationId, isSubagentSession, parseChatUri, parseSubagentSessionUri, type ChangesetState, type MarkdownResponsePart, type ToolCallCompletedState, type ToolCallResponsePart, type Turn } from '../../common/state/sessionState.js'; import { type MessageResourceAttachment } from '../../common/state/protocol/state.js'; import { IProductService } from '../../../product/common/productService.js'; import { AgentService } from '../../node/agentService.js'; @@ -2501,6 +2501,38 @@ suite('AgentService (node dispatcher)', () => { }); }); + test('restoreSession preserves peer chat catalog order regardless of load timing', async () => { + class MultiChatAgent extends MockAgent { + async createChat(_session: URI, _chat: URI): Promise { } + async getChats(session: URI): Promise { + return [ + URI.parse(buildChatUri(session, 'peer-a')), + URI.parse(buildChatUri(session, 'peer-b')), + URI.parse(buildChatUri(session, 'peer-c')), + ]; + } + override async getSessionMessages(session: URI): Promise { + // Resolve in the reverse of catalog order so a resolution-order + // append would scramble the catalog; the restore must keep a,b,c. + const delays: Record = { 'peer-a': 30, 'peer-b': 15, 'peer-c': 0 }; + await timeout(delays[parseChatUri(session)?.chatId ?? ''] ?? 0); + return []; + } + } + const agent = disposables.add(new MultiChatAgent('copilot')); + service.registerProvider(agent); + const { session } = await agent.createSession(); + service.stateManager.deleteSession(session.toString()); + + await service.restoreSession(session); + + const state = service.stateManager.getSessionState(session.toString()); + const peerChatIds = (state?.chats ?? []) + .map(c => parseChatUri(c.resource)?.chatId) + .filter((id): id is string => !!id && id.startsWith('peer-')); + assert.deepStrictEqual(peerChatIds, ['peer-a', 'peer-b', 'peer-c']); + }); + test('fork seeds the new chat with remapped source turns and forwards fork to the provider', async () => { let receivedFork: IAgentCreateChatForkSource | undefined; class MultiChatAgent extends MockAgent { diff --git a/src/vs/sessions/browser/parts/chatCompositeBar.ts b/src/vs/sessions/browser/parts/chatCompositeBar.ts index cbcf7ea0fb1a7e..662ad3c1b12c44 100644 --- a/src/vs/sessions/browser/parts/chatCompositeBar.ts +++ b/src/vs/sessions/browser/parts/chatCompositeBar.ts @@ -208,17 +208,7 @@ export class ChatCompositeBar extends Disposable { const activeChatUri = session.activeChat.read(reader)?.resource.toString() ?? ''; const mainChatUri = mainChat.resource.toString(); const visibleOpenChats = openChats.filter(chat => chat.origin?.kind !== ChatOriginKind.Tool); - // Keep the provider's order, but move untitled (in-composer) chats - // to the end so a just-completed background chat never jumps last. - // Partition so each chat's status is read exactly once (tracked) and - // relative order is preserved by construction. - const committedOpen: IChat[] = []; - const untitledOpen: IChat[] = []; - for (const chat of visibleOpenChats) { - (chat.status.read(reader) === SessionStatus.Untitled ? untitledOpen : committedOpen).push(chat); - } - const orderedChats = untitledOpen.length === 0 ? visibleOpenChats : [...committedOpen, ...untitledOpen]; - this._rebuildTabs(orderedChats, activeChatUri, mainChatUri); + this._rebuildTabs(visibleOpenChats, activeChatUri, mainChatUri); // Archived sessions are read-only, so disable the trailing New Chat // action (mirrors the header action's SessionIsArchivedContext gating). From 2ec88ac9a4251dece85f78a4a215a7cc60d2d86b Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 29 Jun 2026 21:55:18 +0200 Subject: [PATCH 072/589] Customization UI with agent host: Improve instructions page (#323582) * Customization UI with agent host: Improve instructions page * update * add badges --- src/vs/base/common/yaml.ts | 8 +++- src/vs/base/test/common/yaml.test.ts | 11 ++++++ .../agentCustomizationItemProvider.ts | 37 ++++++++++++++++--- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/vs/base/common/yaml.ts b/src/vs/base/common/yaml.ts index af3a2802125a71..64679352573b69 100644 --- a/src/vs/base/common/yaml.ts +++ b/src/vs/base/common/yaml.ts @@ -65,8 +65,12 @@ export class MarkdownNode { const property = this.header.properties.find(p => p.key.value === name); if (property && property.value.type === 'sequence') { return property.value.items.filter(item => item.type === 'scalar').map(item => item.value); - } else if (property && property.value.type === 'scalar' && property.value.format === 'none') { - return parseCommaSeparatedList(property.value.value, 0).map(item => item.value); + } else if (property && property.value.type === 'scalar') { + if (property.value.format === 'none') { + return parseCommaSeparatedList(property.value.value, 0).map(item => item.value); + } else { + return [property.value.value]; + } } } return undefined; diff --git a/src/vs/base/test/common/yaml.test.ts b/src/vs/base/test/common/yaml.test.ts index 933dfca3e754c7..3d5257f6ca5fbd 100644 --- a/src/vs/base/test/common/yaml.test.ts +++ b/src/vs/base/test/common/yaml.test.ts @@ -1259,6 +1259,17 @@ suite('YAML Parser', () => { assert.deepStrictEqual(result.getStringArrayValue('tags'), ['foo', 'bar', 'baz']); }); + test('getStringArrayValue wraps quoted scalars in a single-element array', () => { + const input = [ + '---', + 'tags: "foo, bar"', + '---', + ].join('\n'); + const result = parseFrontMatter(input); + assert.ok(result); + assert.deepStrictEqual(result.getStringArrayValue('tags'), ['foo, bar']); + }); + }); suite('parseCommaSeparatedList', () => { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts index 905de11c728735..6f21d2b131f300 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts @@ -21,6 +21,7 @@ import { AgentCustomizationContentExpander } from './agentCustomizationContentEx import { IAgentHostCustomizationService } from './agentHostCustomizationService.js'; import { IAgentSource, ICustomAgent, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; +import { localize } from '../../../../../../nls.js'; const REMOTE_HOST_GROUP = 'remote-host'; @@ -101,10 +102,10 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto }; } - private toDirectoryItems(customization: DirectoryCustomization, source: AICustomizationSource, groupKey: string | undefined): ICustomizationItem[] { + private toDirectoryItems(customization: DirectoryCustomization, source: AICustomizationSource, isRemote: boolean): ICustomizationItem[] { const items: ICustomizationItem[] = []; for (const child of customization.children ?? []) { - const item = this.toDirectoryChildItem(child, source, groupKey); + const item = this.toDirectoryChildItem(child, source, isRemote); if (item) { items.push(item); } @@ -112,7 +113,7 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto return items; } - private toDirectoryChildItem(child: ChildCustomization, source: AICustomizationSource, groupKey: string | undefined): ICustomizationItem | undefined { + private toDirectoryChildItem(child: ChildCustomization, source: AICustomizationSource, isRemote: boolean): ICustomizationItem | undefined { const type = toPromptsType(child.type); if (!type) { return undefined; @@ -121,6 +122,25 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto if (child.type === CustomizationType.Agent) { userInvocable = readAgentCustomizationMeta(child).userInvocable !== false; } + let groupKey = isRemote ? REMOTE_CLIENT_GROUP : undefined; + let badge: string | undefined = undefined; + let badgeTooltip: string | undefined = undefined; + if (!groupKey && child.type === CustomizationType.Rule) { + const pattern = child.globs?.[0]; + if (child.globs && child.globs.length > 0) { + groupKey = 'context-instructions'; + badge = pattern === '**' + ? localize('alwaysAdded', 'always added') + : pattern; + badgeTooltip = pattern === '**' + ? localize('alwaysIncluded', 'This instruction is automatically included in every interaction.') + : localize('contextInstructions', 'This instruction is automatically included when files matching \'{0}\' are in context.', pattern); + } else if (child.alwaysApply) { + groupKey = 'agent-instructions'; + } else { + groupKey = 'on-demand-instructions'; + } + } return { itemKey: child.id, @@ -130,6 +150,8 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto description: getChildDescription(child), source, groupKey, + badge, + badgeTooltip, extensionId: undefined, pluginUri: undefined, userInvocable, @@ -259,9 +281,9 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto const workingDirectory = this._customAgentsService.getWorkingDirectory(sessionResource); for (const sessionCustomization of directoryCustomizations) { - const source = workingDirectory && sessionCustomization.uri.startsWith(workingDirectory + '/') ? AICustomizationSources.local : AICustomizationSources.user; - const groupKey = sessionCustomization.clientId ? REMOTE_CLIENT_GROUP : undefined; - for (const child of this.toDirectoryItems(sessionCustomization, source, groupKey)) { + const source = workingDirectory && isParentOrEqual(workingDirectory, sessionCustomization.uri) ? AICustomizationSources.local : AICustomizationSources.user; + const isRemote = sessionCustomization.clientId !== undefined; + for (const child of this.toDirectoryItems(sessionCustomization, source, isRemote)) { items.set(child.itemKey ?? child.uri.toString(), { ...child, status: toStatusString(sessionCustomization.load), @@ -288,6 +310,9 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto return children; } } +function isParentOrEqual(folderURI: string, childURI: string): boolean { + return childURI === folderURI || childURI.startsWith(folderURI + '/'); +} function toStatusString(load: CustomizationLoadState | undefined): 'loading' | 'loaded' | 'degraded' | 'error' | undefined { return load?.kind; From 2d5c0cb7bee7882f27fcb91509428ce498d45ab5 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Mon, 29 Jun 2026 15:55:47 -0400 Subject: [PATCH 073/589] Fix pricing hover for Codex + Claude (#323599) * Fix pricing context hover * Fix comments --- .../agentHost/common/agentModelPricing.ts | 80 +++++++++++++++++-- .../agentHost/node/claude/claudeAgent.ts | 8 +- .../agentHost/node/codex/codexAgent.ts | 8 +- .../agentHost/test/node/copilotAgent.test.ts | 1 + 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentModelPricing.ts b/src/vs/platform/agentHost/common/agentModelPricing.ts index 2014c2ec70b09a..a6460d781d9622 100644 --- a/src/vs/platform/agentHost/common/agentModelPricing.ts +++ b/src/vs/platform/agentHost/common/agentModelPricing.ts @@ -84,6 +84,62 @@ export function createAgentModelPricingMeta(pricing: IAgentModelPricingMeta): Re return entries.length > 0 ? Object.fromEntries(entries) : undefined; } +/** + * Normalizes a raw CAPI billing payload (which uses snake_case field names like `token_prices`, + * `input_price`) into the camelCase {@link ICAPIModelBilling} shape that {@link createPricingMetaFromBilling} + * expects. Also handles the case where the billing object already uses camelCase (e.g. from the + * Copilot SDK's `ModelInfo`). Returns `undefined` when `raw` is nullish. + */ +export function normalizeCAPIBilling(raw: unknown): ICAPIModelBilling | undefined { + if (!raw || typeof raw !== 'object') { + return undefined; + } + const billing = raw as Record; + const multiplier = typeof billing.multiplier === 'number' ? billing.multiplier : undefined; + const priceCategory = typeof billing.priceCategory === 'string' ? billing.priceCategory + : typeof (billing as Record).price_category === 'string' ? (billing as Record).price_category as string + : undefined; + const discountPercent = typeof billing.discountPercent === 'number' ? billing.discountPercent + : typeof (billing as Record).discount_percent === 'number' ? (billing as Record).discount_percent as number + : undefined; + + // Resolve token prices: prefer camelCase `tokenPrices`, fall back to snake_case `token_prices`. + const rawTokenPrices = (billing.tokenPrices ?? billing.token_prices) as Record | undefined; + let tokenPrices: ICAPIModelBilling['tokenPrices'] = undefined; + if (rawTokenPrices && typeof rawTokenPrices === 'object') { + // The CAPI snake_case format nests prices under `default` / `long_context` tiers; + // the camelCase format flattens them at the top level of `tokenPrices`. + const defaultTier = rawTokenPrices.default as Record | undefined; + const hasDefault = defaultTier && typeof defaultTier === 'object'; + + const inputPrice = asNumber(rawTokenPrices.inputPrice) ?? asNumber(hasDefault ? defaultTier.input_price : undefined); + const cachePrice = asNumber(rawTokenPrices.cachePrice) ?? asNumber(hasDefault ? defaultTier.cache_price : undefined); + const cacheWritePrice = asNumber(rawTokenPrices.cacheWritePrice) ?? asNumber(hasDefault ? defaultTier.cache_write_price : undefined); + const outputPrice = asNumber(rawTokenPrices.outputPrice) ?? asNumber(hasDefault ? defaultTier.output_price : undefined); + const contextMax = asNumber(rawTokenPrices.contextMax) ?? asNumber(hasDefault ? defaultTier.context_max : undefined); + + const rawLong = (rawTokenPrices.longContext ?? rawTokenPrices.long_context) as Record | undefined; + let longContext: { readonly contextMax?: number; readonly inputPrice?: number; readonly cachePrice?: number; readonly cacheWritePrice?: number; readonly outputPrice?: number } | undefined; + if (rawLong && typeof rawLong === 'object') { + longContext = { + inputPrice: asNumber(rawLong.inputPrice) ?? asNumber(rawLong.input_price), + cachePrice: asNumber(rawLong.cachePrice) ?? asNumber(rawLong.cache_price), + cacheWritePrice: asNumber(rawLong.cacheWritePrice) ?? asNumber(rawLong.cache_write_price), + outputPrice: asNumber(rawLong.outputPrice) ?? asNumber(rawLong.output_price), + contextMax: asNumber(rawLong.contextMax) ?? asNumber(rawLong.context_max), + }; + } + + tokenPrices = { inputPrice, cachePrice, cacheWritePrice, outputPrice, contextMax, longContext }; + } + + return { multiplier, priceCategory, discountPercent, tokenPrices }; +} + +function asNumber(v: unknown): number | undefined { + return typeof v === 'number' ? v : undefined; +} + /** * Runtime shape of the CAPI model billing payload. The published SDK types (`CCAModelBilling`, `ModelBilling`) don't * yet declare `tokenPrices`, `priceCategory`, or `discountPercent`, but the `/models` endpoint already carries them. @@ -115,7 +171,9 @@ export interface ICAPIModelBilling { /** * Converts a CAPI model's billing payload into an {@link IAgentModelPricingMeta} `_meta` bag. Long-context costs are - * only emitted when they differ from the default tier so the model picker can tell them apart. + * only emitted when there is an actual surcharge (at least one long-context price differs from the default tier). + * When emitting, any missing long-context field falls back to the default-tier value so the hover table renders + * complete rows. See {@link hasLongContextSurcharge} for the surcharge detection logic. * * @param billing - The model's billing info, narrowed through {@link ICAPIModelBilling}. * @param priceCategory - An optional override for the price category (e.g. from `modelPickerPriceCategory` on the @@ -125,8 +183,16 @@ export function createPricingMetaFromBilling(billing: ICAPIModelBilling | undefi const tokenPrices = billing?.tokenPrices; const longContext = tokenPrices?.longContext; - const differsFromDefault = (longValue: number | undefined, defaultValue: number | undefined): number | undefined => - longValue !== undefined && longValue !== defaultValue ? longValue : undefined; + // Only emit long-context costs when there is an actual surcharge (at least + // one price differs from default). When emitting, fall back to the default- + // tier value for any field the long-context tier does not specify so the + // hover table renders complete rows without gaps. + const showLongContext = longContext !== undefined && ( + (longContext.inputPrice !== undefined && longContext.inputPrice !== tokenPrices?.inputPrice) || + (longContext.outputPrice !== undefined && longContext.outputPrice !== tokenPrices?.outputPrice) || + (longContext.cachePrice !== undefined && longContext.cachePrice !== tokenPrices?.cachePrice) || + (longContext.cacheWritePrice !== undefined && longContext.cacheWritePrice !== tokenPrices?.cacheWritePrice) + ); return createAgentModelPricingMeta({ multiplierNumeric: typeof billing?.multiplier === 'number' ? billing.multiplier : undefined, @@ -134,10 +200,10 @@ export function createPricingMetaFromBilling(billing: ICAPIModelBilling | undefi cacheCost: tokenPrices?.cachePrice, cacheWriteCost: tokenPrices?.cacheWritePrice, outputCost: tokenPrices?.outputPrice, - longContextInputCost: differsFromDefault(longContext?.inputPrice, tokenPrices?.inputPrice), - longContextCacheCost: differsFromDefault(longContext?.cachePrice, tokenPrices?.cachePrice), - longContextCacheWriteCost: differsFromDefault(longContext?.cacheWritePrice, tokenPrices?.cacheWritePrice), - longContextOutputCost: differsFromDefault(longContext?.outputPrice, tokenPrices?.outputPrice), + longContextInputCost: showLongContext ? (longContext.inputPrice ?? tokenPrices?.inputPrice) : undefined, + longContextCacheCost: showLongContext ? (longContext.cachePrice ?? tokenPrices?.cachePrice) : undefined, + longContextCacheWriteCost: showLongContext ? (longContext.cacheWritePrice ?? tokenPrices?.cacheWritePrice) : undefined, + longContextOutputCost: showLongContext ? (longContext.outputPrice ?? tokenPrices?.outputPrice) : undefined, priceCategory: priceCategory ?? (typeof billing?.priceCategory === 'string' ? billing.priceCategory : undefined), discountPercent: typeof billing?.discountPercent === 'number' ? billing.discountPercent : undefined, }); diff --git a/src/vs/platform/agentHost/node/claude/claudeAgent.ts b/src/vs/platform/agentHost/node/claude/claudeAgent.ts index a4a344525bc3e8..144c9d80a52856 100644 --- a/src/vs/platform/agentHost/node/claude/claudeAgent.ts +++ b/src/vs/platform/agentHost/node/claude/claudeAgent.ts @@ -42,7 +42,7 @@ import { getSubagentTranscript } from './claudeSubagentResolver.js'; import { ClaudeAgentSession } from './claudeAgentSession.js'; import { handleCanUseTool } from './claudeCanUseTool.js'; import type { IAgentServerToolHost } from '../../common/agentServerTools.js'; -import { createPricingMetaFromBilling, type ICAPIModelBilling } from '../../common/agentModelPricing.js'; +import { createPricingMetaFromBilling, normalizeCAPIBilling } from '../../common/agentModelPricing.js'; import { tryParseClaudeModelId } from './claudeModelId.js'; import { resolvePromptToContentBlocks } from './claudePromptResolver.js'; import { IClaudeProxyHandle, IClaudeProxyService, type ClaudeTransport } from './claudeProxyService.js'; @@ -96,10 +96,10 @@ function toAgentModelInfo(m: CCAModel, provider: AgentProvider): IAgentModelInfo const supportedEfforts = ((supports as IClaudeModelSupports | undefined)?.reasoning_effort ?? []).filter(isClaudeEffortLevel); const configSchema = createClaudeThinkingLevelSchema(supportedEfforts); const policyState = m.policy?.state as PolicyState | undefined; - const billing = m.billing as ICAPIModelBilling | undefined; + const billing = normalizeCAPIBilling(m.billing); // priceCategory may appear as a top-level model field depending on the CAPI version. - const priceCategory = typeof (m as { modelPickerPriceCategory?: string }).modelPickerPriceCategory === 'string' - ? (m as { modelPickerPriceCategory?: string }).modelPickerPriceCategory + const priceCategory = typeof m.model_picker_price_category === 'string' + ? m.model_picker_price_category : undefined; return { provider, diff --git a/src/vs/platform/agentHost/node/codex/codexAgent.ts b/src/vs/platform/agentHost/node/codex/codexAgent.ts index 1ca2521e4a8288..b6363c1b02e623 100644 --- a/src/vs/platform/agentHost/node/codex/codexAgent.ts +++ b/src/vs/platform/agentHost/node/codex/codexAgent.ts @@ -17,7 +17,7 @@ import { localize } from '../../../../nls.js'; import { ILogService } from '../../../log/common/log.js'; import { IProductService } from '../../../product/common/productService.js'; import { createSchema, platformSessionSchema, schemaProperty, type SessionMode } from '../../common/agentHostSchema.js'; -import { createPricingMetaFromBilling, type ICAPIModelBilling } from '../../common/agentModelPricing.js'; +import { createPricingMetaFromBilling, normalizeCAPIBilling } from '../../common/agentModelPricing.js'; import { getReasoningEffortDescription, getReasoningEffortLabel } from '../../common/reasoningEffort.js'; import { AgentHostCodexAgentBinaryArgsEnvVar, AgentHostCodexAgentCodexHomeEnvVar, AgentHostCodexAgentSdkRootEnvVar, AgentSession, AgentSignal, GITHUB_COPILOT_PROTECTED_RESOURCE, GITHUB_REPO_PROTECTED_RESOURCE, IActiveClient, IAgent, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentMaterializeSessionEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IMcpNotification, type AgentProvider } from '../../common/agentService.js'; import { SessionConfigKey } from '../../common/sessionConfigKeys.js'; @@ -788,9 +788,9 @@ export class CodexAgent extends Disposable implements IAgent { configSchema, policyState: m.policy?.state as PolicyState | undefined, _meta: createPricingMetaFromBilling( - m.billing as ICAPIModelBilling | undefined, - typeof (m as { modelPickerPriceCategory?: string }).modelPickerPriceCategory === 'string' - ? (m as { modelPickerPriceCategory?: string }).modelPickerPriceCategory + normalizeCAPIBilling(m.billing), + typeof m.model_picker_price_category === 'string' + ? m.model_picker_price_category : undefined, ), })); diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index 07163832399eca..0190d47a55174f 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -1060,6 +1060,7 @@ suite('CopilotAgent', () => { cacheCost: 1, outputCost: 15, longContextInputCost: 6, + longContextCacheCost: 1, longContextOutputCost: 22.5, priceCategory: 'medium', }); From 700f92779f39bb25445026c5c5f7065c62ef5b68 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Mon, 29 Jun 2026 13:25:30 -0700 Subject: [PATCH 074/589] Disable semantic_search on BYOK/custom endpoints (#323605) chat: disable semantic_search on BYOK/custom endpoints The semantic_search (codebase) tool relies on embeddings that require a Copilot token source. On BYOK / custom (non-CAPI) endpoints those embeddings are unavailable, and when the GitHub authentication provider is disabled the embeddings lookup times out waiting for the provider and aborts the entire chat turn instead of degrading gracefully. Gate semantic_search off for non-CAPI endpoints alongside the specialized subagents so it is never offered (even if manually enabled via the tool picker) when it cannot work. Add tests covering the CAPI and BYOK cases. Fixes #322525 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/extension/intents/node/agentIntent.ts | 8 ++++++-- .../node/test/searchSubagentGating.spec.ts | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index 335bce91c74e03..5a5d51eed70402 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -245,12 +245,16 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode. allowTools[ToolName.CoreRunTest] = await testService.hasAnyTests(); allowTools[ToolName.CoreRunTask] = tasksService.getTasks().length > 0; - // The specialized subagents must only run when - // the main agent is on CAPI. + // The specialized subagents and semantic search only work when the main + // agent is on CAPI. semantic_search relies on embeddings that require a + // Copilot token source, so on BYOK / custom endpoints it can abort the chat + // turn (e.g. when the GitHub auth provider is unavailable). Keep it off + // there. See https://github.com/microsoft/vscode/issues/322525. if (!isCAPIEndpoint(model)) { allowTools[ToolName.SearchSubagent] = false; allowTools[ToolName.ExploreSubagent] = false; allowTools[ToolName.ExecutionSubagent] = false; + allowTools[ToolName.Codebase] = false; } else { const searchSubagentEnabled = configurationService.getExperimentBasedConfig(ConfigKey.Advanced.SearchSubagentToolEnabled, experimentationService); const exploreAgentEnabled = configurationService.getExperimentBasedConfig(ConfigKey.ExploreAgentEnabled, experimentationService); diff --git a/extensions/copilot/src/extension/intents/node/test/searchSubagentGating.spec.ts b/extensions/copilot/src/extension/intents/node/test/searchSubagentGating.spec.ts index 79d5667e7bb715..8fa3ddb0fbd0a3 100644 --- a/extensions/copilot/src/extension/intents/node/test/searchSubagentGating.spec.ts +++ b/extensions/copilot/src/extension/intents/node/test/searchSubagentGating.spec.ts @@ -109,4 +109,19 @@ describe('getAgentTools search subagent gating', () => { expect(hasTool(tools, ToolName.SearchSubagent)).toBe(false); expect(hasTool(tools, ToolName.ExploreSubagent)).toBe(false); }); + + test('exposes semantic_search when the model is a CAPI endpoint', async () => { + const request = new TestChatRequest('how does foo work'); + const tools = await instantiationService.invokeFunction(getAgentTools, request, userEndpoint); + expect(hasTool(tools, ToolName.Codebase)).toBe(true); + }); + + test('hides semantic_search when the model is a BYOK / custom endpoint', async () => { + // A BYOK / custom endpoint is identified by a string URL rather than CAPI request metadata. + const byokEndpoint = instantiationService.createInstance(MockEndpoint, 'gpt-5'); + (byokEndpoint as { urlOrRequestMetadata: string }).urlOrRequestMetadata = 'https://localhost:8080/v1/chat/completions'; + const request = new TestChatRequest('how does foo work'); + const tools = await instantiationService.invokeFunction(getAgentTools, request, byokEndpoint); + expect(hasTool(tools, ToolName.Codebase)).toBe(false); + }); }); From b7e0a87054cea55f10f924cdd254beb61ad93964 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:31:39 -0700 Subject: [PATCH 075/589] chore: run npm audit fix (#323165) --- build/package-lock.json | 172 +++++------ build/rspack/package-lock.json | 32 +-- .../package-lock.json | 6 +- .../package-lock.json | 6 +- .../notebook-renderers/package-lock.json | 6 +- extensions/npm/package-lock.json | 7 +- package-lock.json | 266 +++++++++--------- remote/package-lock.json | 18 +- 8 files changed, 253 insertions(+), 260 deletions(-) diff --git a/build/package-lock.json b/build/package-lock.json index 12162d6054f94c..4271a5f06a58b1 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -1320,28 +1320,28 @@ } }, "node_modules/@textlint/ast-node-types": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.2.tgz", - "integrity": "sha512-9ByYNzWV8tpz6BFaRzeRzIov8dkbSZu9q7IWqEIfmRuLWb2qbI/5gTvKcoWT1HYs4XM7IZ8TKSXcuPvMb6eorA==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.2.2.tgz", - "integrity": "sha512-oMVaMJ3exFvXhCj3AqmCbLaeYrTNLqaJnLJMIlmnRM3/kZdxvku4OYdaDzgtlI194cVxamOY5AbHBBVnY79kEg==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", "dev": true, "license": "MIT", "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "15.2.2", - "@textlint/resolver": "15.2.2", - "@textlint/types": "15.2.2", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", "chalk": "^4.1.2", - "debug": "^4.4.1", - "js-yaml": "^3.14.1", - "lodash": "^4.17.21", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", @@ -1456,27 +1456,27 @@ } }, "node_modules/@textlint/module-interop": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.2.2.tgz", - "integrity": "sha512-2rmNcWrcqhuR84Iio1WRzlc4tEoOMHd6T7urjtKNNefpTt1owrTJ9WuOe60yD3FrTW0J/R0ux5wxUbP/eaeFOA==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.2.2.tgz", - "integrity": "sha512-4hGWjmHt0y+5NAkoYZ8FvEkj8Mez9TqfbTm3BPjoV32cIfEixl2poTOgapn1rfm73905GSO3P1jiWjmgvii13Q==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.2.2.tgz", - "integrity": "sha512-X2BHGAR3yXJsCAjwYEDBIk9qUDWcH4pW61ISfmtejau+tVqKtnbbvEZnMTb6mWgKU1BvTmftd5DmB1XVDUtY3g==", + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "15.2.2" + "@textlint/ast-node-types": "15.7.1" } }, "node_modules/@types/ansi-colors": { @@ -2408,21 +2408,11 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/argparse/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "BSD-3-Clause" + "license": "Python-2.0" }, "node_modules/arr-diff": { "version": "4.0.0", @@ -3403,20 +3393,6 @@ "node": ">=0.8.0" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/events": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", @@ -3642,17 +3618,17 @@ "dev": true }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -4004,9 +3980,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -4358,14 +4334,23 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -4509,10 +4494,20 @@ } }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -4609,15 +4604,25 @@ } }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -4626,13 +4631,6 @@ "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/markdown-it/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -5488,26 +5486,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/rc-config-loader/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/rc-config-loader/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", diff --git a/build/rspack/package-lock.json b/build/rspack/package-lock.json index c125386c34cf6f..29d6a8e71997c6 100644 --- a/build/rspack/package-lock.json +++ b/build/rspack/package-lock.json @@ -794,9 +794,9 @@ ] }, "node_modules/@rspack/cli": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.7.11.tgz", - "integrity": "sha512-vUnflkq4F654wTEpCd+L4RYVbet8L2lNqLMmAGIZvoZddlXm4Duvg+eqcFE9iF8plAjFflRcU7DhB7WZa76pwg==", + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.7.12.tgz", + "integrity": "sha512-aiVj5hm12/rhkH/KTvs9BugpRuLEutIw5o3KmQDvSMd6EqEIASuCCUI3/97uaNP58hqZSl7mAotsLcwn/OCbJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2625,9 +2625,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2849,14 +2849,14 @@ "license": "MIT" }, "node_modules/launch-editor": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", - "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.4" } }, "node_modules/loader-runner": { @@ -4298,9 +4298,9 @@ } }, "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "dev": true, "license": "MIT", "engines": { @@ -4461,9 +4461,9 @@ } }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { diff --git a/extensions/markdown-language-features/package-lock.json b/extensions/markdown-language-features/package-lock.json index de3037536a9999..13bdb2e042cc41 100644 --- a/extensions/markdown-language-features/package-lock.json +++ b/extensions/markdown-language-features/package-lock.json @@ -1348,9 +1348,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz", - "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==", + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" diff --git a/extensions/mermaid-markdown-features/package-lock.json b/extensions/mermaid-markdown-features/package-lock.json index b88d22950afd86..3ffd72c4c3f65e 100644 --- a/extensions/mermaid-markdown-features/package-lock.json +++ b/extensions/mermaid-markdown-features/package-lock.json @@ -1845,9 +1845,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz", - "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==", + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" diff --git a/extensions/notebook-renderers/package-lock.json b/extensions/notebook-renderers/package-lock.json index e5c39d11af174f..98aa3ee4360a6f 100644 --- a/extensions/notebook-renderers/package-lock.json +++ b/extensions/notebook-renderers/package-lock.json @@ -605,9 +605,9 @@ } }, "node_modules/undici": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", - "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": { diff --git a/extensions/npm/package-lock.json b/extensions/npm/package-lock.json index ca856d1c5ada5d..3f94141535afc0 100644 --- a/extensions/npm/package-lock.json +++ b/extensions/npm/package-lock.json @@ -332,9 +332,10 @@ } }, "node_modules/which-pm": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.1.1.tgz", - "integrity": "sha512-xzzxNw2wMaoCWXiGE8IJ9wuPMU+EYhFksjHxrRT8kMT5SnocBPRg69YAMtyV4D12fP582RA+k3P8H9J5EMdIxQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.2.0.tgz", + "integrity": "sha512-MOiaDbA5ZZgUjkeMWM5EkJp4loW5ZRoa5bc3/aeMox/PJelMhE6t7S/mLuiY43DBupyxH+S0U1bTui9kWUlmsw==", + "license": "MIT", "dependencies": { "load-yaml-file": "^0.2.0", "path-exists": "^4.0.0" diff --git a/package-lock.json b/package-lock.json index 338c7eff02e978..0ddd17820265cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -625,13 +625,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -640,9 +640,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -650,21 +650,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -698,14 +698,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -715,14 +715,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -759,9 +759,9 @@ "license": "ISC" }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -769,29 +769,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -801,9 +801,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -811,9 +811,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -821,9 +821,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -831,27 +831,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -870,33 +870,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -904,14 +904,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1814,13 +1814,13 @@ "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, "node_modules/@microsoft/dev-tunnels-connections": { - "version": "1.3.48", - "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-connections/-/dev-tunnels-connections-1.3.48.tgz", - "integrity": "sha512-PD7bKg5mCtVMTydKH++Xm0mf5uFPncPhLTmN/LYvoOmA06GLGkR1KLiPkilfibtu3Vyq3vqxfwp2kXoWZ6fEOQ==", + "version": "1.3.50", + "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-connections/-/dev-tunnels-connections-1.3.50.tgz", + "integrity": "sha512-L3vUE7jiW4tzx1D+sEsuCW5UKK3CjYFIxOtjZF/MN8ZCM2a2MVtIijctjU+/Y6Gi+ohvrOZKoqSZFRD12bpAgA==", "license": "MIT", "dependencies": { - "@microsoft/dev-tunnels-contracts": "1.3.48", - "@microsoft/dev-tunnels-management": "1.3.48", + "@microsoft/dev-tunnels-contracts": "1.3.50", + "@microsoft/dev-tunnels-management": "1.3.50", "await-semaphore": "^0.1.3", "buffer": "^5.2.1", "debug": "^4.1.1", @@ -1844,9 +1844,9 @@ } }, "node_modules/@microsoft/dev-tunnels-contracts": { - "version": "1.3.48", - "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-contracts/-/dev-tunnels-contracts-1.3.48.tgz", - "integrity": "sha512-PYj+MuKy03BDtjnM1I3qMAU2MO9tnBPAcj/uP75ZJHJefLcLBeK8TssKmmYjQywD5llp9Y6TDt0wGwsC2XvM0Q==", + "version": "1.3.50", + "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-contracts/-/dev-tunnels-contracts-1.3.50.tgz", + "integrity": "sha512-R4G/h939dL3UOui/69cKmRNZVf+3IpO6bMWxOgt4NYjF2IvWaSWBoc+u2AdZuZi+0ZHatWXvjoCDLNrV4pUyLQ==", "license": "MIT", "dependencies": { "buffer": "^5.2.1", @@ -1864,12 +1864,12 @@ } }, "node_modules/@microsoft/dev-tunnels-management": { - "version": "1.3.48", - "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-management/-/dev-tunnels-management-1.3.48.tgz", - "integrity": "sha512-6XrYQViWiv6xxueZYDcScj/zYw7dvpyO4mk8q/uSkBWBrSExNZs/rTLTR7hzxUDJFaiq/VybeZNJe/5LKlveMg==", + "version": "1.3.50", + "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-management/-/dev-tunnels-management-1.3.50.tgz", + "integrity": "sha512-sWK0CrBcmiNyeb3HztocR+Gd5ROfKjyixhxfSJ+TlGIj9Y4i3DggBDFZeaKGql5LbJN5+v13tOggF2vhn8OEfA==", "license": "MIT", "dependencies": { - "@microsoft/dev-tunnels-contracts": "1.3.48", + "@microsoft/dev-tunnels-contracts": "1.3.50", "axios": "^1.8.4", "buffer": "^5.2.1", "debug": "^4.1.1", @@ -3664,9 +3664,9 @@ } }, "node_modules/@vscode/component-explorer-cli": { - "version": "0.2.1-59", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.2.1-59.tgz", - "integrity": "sha512-7ewdfQ2Fre1GR7Ck9hvWVmuFx4Z2JDAtoRLqI4LFJgtxVXIjgqx+2ylkIX4h19/rgyN9AVFYketroOdtTlzsGQ==", + "version": "0.2.1-63", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.2.1-63.tgz", + "integrity": "sha512-DzPPaUXU/xB4zN2Urx9CP07TG/hItWVEaeWKp9j52BMTev6fWkVdh3YBVQGAxIEvoAVAuQSUlPUGdf8e9IkePw==", "dev": true, "license": "MIT", "dependencies": { @@ -5233,9 +5233,9 @@ ] }, "node_modules/baseline-browser-mapping": { - "version": "2.10.27", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", - "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5388,9 +5388,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "dev": true, "funding": [ { @@ -5408,11 +5408,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -5703,9 +5703,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001768", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", - "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ { @@ -5814,9 +5814,10 @@ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" }, "node_modules/chrome-remote-interface/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -7140,9 +7141,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.379", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz", + "integrity": "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==", "dev": true, "license": "ISC" }, @@ -12463,10 +12464,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -14148,11 +14159,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nopt": { "version": "4.0.1", @@ -19702,9 +19716,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/remote/package-lock.json b/remote/package-lock.json index 46f4e699f5e672..2d5ba37426945b 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -1634,9 +1634,9 @@ } }, "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "version": "7.5.17", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.17.tgz", + "integrity": "sha512-wPEBwzapC+2PaTYPH6e2L+cNOEE227S47wUYFqlegcs8zlLLmeb9Fcff1HVZY4Fwku/1Eyv38n7GYwB2aaS71g==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -1721,9 +1721,9 @@ "license": "Unlicense" }, "node_modules/undici": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", - "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -1792,9 +1792,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" From 78e714090e5ee8aa2de726a6ddc433bbeae15d3b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:36:54 +0000 Subject: [PATCH 076/589] Agents - reduce flickering of the change list + actions (#323593) * Agents - reduce flickering of the change list + actions * Fix tests --- .../node/agentHostChangesetCoordinator.ts | 34 ++++++----- .../contrib/changes/browser/changesView.ts | 5 ++ .../changes/browser/changesViewService.ts | 6 +- .../browser/agentHostChangesetConstants.ts | 12 ---- .../browser/agentHostSessionChangesets.ts | 21 ++----- .../browser/baseAgentHostSessionsProvider.ts | 16 ++---- .../localAgentHostSessionsProvider.test.ts | 56 ------------------- 7 files changed, 40 insertions(+), 110 deletions(-) delete mode 100644 src/vs/sessions/contrib/providers/agentHost/browser/agentHostChangesetConstants.ts diff --git a/src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts b/src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts index 6a63b1395f74de..108529b738247d 100644 --- a/src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts +++ b/src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts @@ -14,7 +14,7 @@ import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChang import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js'; import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; -import { readSessionGitState } from '../common/state/sessionState.js'; +import { isAhpChatChannel, readSessionGitState } from '../common/state/sessionState.js'; /** * Raw metadata blob values for the session DB, batch-read by the caller. @@ -141,24 +141,43 @@ export class AgentHostChangesetCoordinator extends Disposable { const resourceStr = resource.toString(); const parsed = parseChangesetUri(resourceStr); + if (!parsed && !isAhpChatChannel(resourceStr) && this._stateManager.getSessionState(resourceStr)) { + // Plain session-URI subscription (Agents Window list / detail + // observing the session). Track the session URI itself as a + // subscription marker so a later git-state change / + // materialization recompute (driven from the exposed + // subscription list) re-refreshes the static changesets, then + // refresh both now so the catalogue chip doesn't show a stale + // value just because no turn has run since process start. + this._addSubscription(resourceStr, resourceStr); + this._changesets.refreshBranchChangeset(resourceStr); + this._changesets.refreshSessionChangeset(resourceStr); + this._changesetFileMonitor.trackSessionChanges(resourceStr, resourceStr); + + return; + } + if (parsed?.kind === ChangesetKind.Branch) { this._addSubscription(parsed.sessionUri, resourceStr); this._changesets.refreshBranchChangeset(parsed.sessionUri); this._changesetFileMonitor.trackSessionChanges(resourceStr, parsed.sessionUri); return; } + if (parsed?.kind === ChangesetKind.Uncommitted) { this._addSubscription(parsed.sessionUri, resourceStr); void this._changesets.computeUncommittedChangeset(parsed.sessionUri); this._changesetFileMonitor.trackSessionChanges(resourceStr, parsed.sessionUri); return; } + if (parsed?.kind === ChangesetKind.Session) { this._addSubscription(parsed.sessionUri, resourceStr); this._changesets.refreshSessionChangeset(parsed.sessionUri); this._changesetFileMonitor.trackSessionChanges(resourceStr, parsed.sessionUri); return; } + if (parsed?.kind === ChangesetKind.Turn && parsed.turnId !== undefined) { // Track the new subscriber so the service's per-turn recompute // gating starts including this turn. The initial snapshot is @@ -168,19 +187,6 @@ export class AgentHostChangesetCoordinator extends Disposable { this._addSubscription(parsed.sessionUri, resourceStr); return; } - if (!parsed && this._stateManager.getSessionState(resourceStr)) { - // Plain session-URI subscription (Agents Window list / detail - // observing the session). Track the session URI itself as a - // subscription marker so a later git-state change / - // materialization recompute (driven from the exposed - // subscription list) re-refreshes the static changesets, then - // refresh both now so the catalogue chip doesn't show a stale - // value just because no turn has run since process start. - this._addSubscription(resourceStr, resourceStr); - this._changesets.refreshBranchChangeset(resourceStr); - this._changesets.refreshSessionChangeset(resourceStr); - this._changesetFileMonitor.trackSessionChanges(resourceStr, resourceStr); - } } /** diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 0c92558cc1fac5..c761ffb4975696 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -310,6 +310,11 @@ class ChangesWorkbenchButtonBarWidget extends Disposable { }); this._register(autorun(reader => { + const isLoading = changesViewService.activeSessionIsLoadingObs.read(reader); + if (isLoading) { + return; + } + const operationActionGroups = operationActionGroupsObs.read(reader); const menuActions = menuActionsObs.read(reader); diff --git a/src/vs/sessions/contrib/changes/browser/changesViewService.ts b/src/vs/sessions/contrib/changes/browser/changesViewService.ts index 63c1aa91656fbc..032811d7c3ae9d 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewService.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewService.ts @@ -143,7 +143,11 @@ export class ChangesViewService extends Disposable implements IChangesViewServic private _getActiveSessionState(): { isLoading: IObservable; state: IObservable } { const isLoadingObs = derived(reader => { const changeset = this.activeSessionChangesetObs.read(reader); - return changeset?.isLoadingChanges.read(reader) ?? false; + if (!changeset) { + return true; + } + + return changeset.isLoadingChanges.read(reader); }); const activeSessionStateObs = derivedObservableWithCache(this, (reader, lastValue) => { diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/agentHostChangesetConstants.ts b/src/vs/sessions/contrib/providers/agentHost/browser/agentHostChangesetConstants.ts deleted file mode 100644 index 68d697002a120e..00000000000000 --- a/src/vs/sessions/contrib/providers/agentHost/browser/agentHostChangesetConstants.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Minimum interval between changeset-driven UI recomputes. While an agent - * edits, the host streams many changeset envelopes per second; coalescing them - * to ~10 updates/second keeps the Changes view responsive without perceptible - * lag, and stops every envelope from forcing a full list relayout. - */ -export const CHANGESET_UPDATE_THROTTLE_MS = 100; diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/agentHostSessionChangesets.ts b/src/vs/sessions/contrib/providers/agentHost/browser/agentHostSessionChangesets.ts index f03e8b9298e835..266a9883127bf4 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/agentHostSessionChangesets.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/agentHostSessionChangesets.ts @@ -5,7 +5,7 @@ import { arrayEqualsC, structuralEquals } from '../../../../../base/common/equals.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { constObservable, derived, derivedObservableWithCache, derivedOpts, IObservable, mapObservableArrayCached, observableFromEvent, observableValue, throttledObservable } from '../../../../../base/common/observable.js'; +import { constObservable, derived, derivedObservableWithCache, derivedOpts, IObservable, mapObservableArrayCached, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../../base/common/resources.js'; import { format } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -17,7 +17,6 @@ import { ChangesetOperation, ChangesetOperationScope, type ChangesetFile, Change import { buildDefaultChatUri, ChangesetStatus, Changeset, StateComponents, type ChangesetState, type ChatState, type ChatSummary, type SessionState } from '../../../../../platform/agentHost/common/state/sessionState.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { ISessionChangeset, ISessionChangesetOperation, ISessionChangesetOperationTarget, ISessionFileChange, SessionChangesetOperationScope, SessionChangesetOperationStatus, sessionFileChangesEqual } from '../../../../services/sessions/common/session.js'; -import { CHANGESET_UPDATE_THROTTLE_MS } from './agentHostChangesetConstants.js'; import { changesetFileToChange } from './agentHostDiffs.js'; import { IAgentHostAdapterOptions } from './baseAgentHostSessionsProvider.js'; @@ -159,18 +158,8 @@ abstract class AbstractAgentHostChangeset implements ISessionChangeset { private readonly _options: IAgentHostAdapterOptions, private readonly _dialogService: IDialogService, ) { - // Throttle only the changes path; `isLoadingChanges` and `operations` - // keep reading `this.changesetStateObs` directly so spinners/buttons stay - // responsive. Throttle (not debounce) so a continuous stream keeps - // updating instead of starving until edits stop. The `derived` wrapper - // defers reading the abstract `changesetStateObs` until the observable is - // actually observed (it isn't assigned yet during base construction). - const throttledChangesetStateObs = throttledObservable( - derived(reader => this.changesetStateObs.read(reader).read(reader)), - CHANGESET_UPDATE_THROTTLE_MS); - this.isLoadingChanges = derived(reader => { - const changesetState = throttledChangesetStateObs.read(reader); + const changesetState = this.changesetStateObs.read(reader).read(reader); // If the changeset state is `undefined`, it means that the first snapshot // has not yet arrived, so in order to avoid any flickering in the Changes @@ -187,7 +176,7 @@ abstract class AbstractAgentHostChangeset implements ISessionChangeset { // For static changesets, that are persisted to the database, the // cached state will be sent over the wire while the changeset is // being computed. - return changesetState.status === ChangesetStatus.Computing && changesetState.files.length === 0; + return changesetState.status === ChangesetStatus.Computing; }); const mapDiffUri = this._options.mapDiffUri; @@ -196,7 +185,7 @@ abstract class AbstractAgentHostChangeset implements ISessionChangeset { // files keep their reference across reducer updates, enabling the // per-file cache below to skip rebuilding them. const changesetFilesObs = derivedObservableWithCache(this, (reader, lastValue) => { - const changesetState = throttledChangesetStateObs.read(reader); + const changesetState = this.changesetStateObs.read(reader).read(reader); if (changesetState === null || changesetState instanceof Error) { return []; } @@ -231,7 +220,7 @@ abstract class AbstractAgentHostChangeset implements ISessionChangeset { }); const operationsObs = derivedObservableWithCache(this, (reader, lastValue) => { - const changesetState = throttledChangesetStateObs.read(reader); + const changesetState = this.changesetStateObs.read(reader).read(reader); if (changesetState === null || changesetState instanceof Error) { return []; } diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts index 006af65c025adb..e0d7c9b6bbb4ab 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -11,7 +11,7 @@ import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, IReference, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { equals } from '../../../../../base/common/objects.js'; -import { constObservable, derived, derivedObservableWithCache, derivedOpts, IObservable, ISettableObservable, mapObservableArrayCached, observableFromEvent, observableValue, observableValueOpts, throttledObservable, transaction, waitForState } from '../../../../../base/common/observable.js'; +import { constObservable, derived, derivedObservableWithCache, derivedOpts, IObservable, ISettableObservable, mapObservableArrayCached, observableFromEvent, observableValue, observableValueOpts, transaction, waitForState } from '../../../../../base/common/observable.js'; import { isEqual, isEqualOrParent, relativePath } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isDefined } from '../../../../../base/common/types.js'; @@ -51,7 +51,6 @@ import { IDeleteChatOptions, ISendRequestOptions, ISessionChangeEvent, ISessionM import { IGitHubService } from '../../../github/browser/githubService.js'; import { computeLivePullRequestIcon } from '../../../github/browser/pullRequestIconStatus.js'; import { IPullRequestIconCache } from '../../../github/browser/pullRequestIconCache.js'; -import { CHANGESET_UPDATE_THROTTLE_MS } from './agentHostChangesetConstants.js'; import { changesetFileToChange, mapProtocolStatus } from './agentHostDiffs.js'; import { createChangesets } from './agentHostSessionChangesets.js'; @@ -817,13 +816,6 @@ export class AgentHostSessionAdapter extends Disposable implements ISession { const mapDiffUri = this._options.mapDiffUri; - // Coalesce the per-envelope changeset stream. `sessionChangesetStateObs` - // is a nested observable (which-subscription → value); flatten it to the - // value stream, then throttle. Throttle (not debounce) so a continuous - // stream keeps updating ~10x/s instead of starving until edits stop; the - // trailing read always delivers the final state. - const throttledChangesetValueObs = throttledObservable(sessionChangesetStateObs.flatten(), CHANGESET_UPDATE_THROTTLE_MS); - // Hold the raw `ChangesetFile[]` (with last-value semantics) rather than // the mapped changes. The changeset reducer preserves the reference of // every file that didn't change, so keeping the raw list lets the @@ -834,7 +826,7 @@ export class AgentHostSessionAdapter extends Disposable implements ISession { return lastValue; } - const branchChangesState = throttledChangesetValueObs.read(reader); + const branchChangesState = sessionChangesetStateObs.read(reader).read(reader); if (!branchChangesState || branchChangesState instanceof Error || branchChangesState.status !== 'ready') { return lastValue; } @@ -846,7 +838,9 @@ export class AgentHostSessionAdapter extends Disposable implements ISession { // `ChangesetFile` reference is unchanged. Only the file(s) that actually // changed get re-parsed and re-mapped, turning the previous O(all files) // URI work per update into O(changed files). - const mappedChangesObs = mapObservableArrayCached(this, changesetFilesObs.map(files => files ?? []), file => changesetFileToChange(file, mapDiffUri)); + const mappedChangesObs = mapObservableArrayCached(this, + changesetFilesObs.map(files => files ?? []), + file => changesetFileToChange(file, mapDiffUri)); const changesetChangesObs = derived(this, reader => { const files = changesetFilesObs.read(reader); diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts index 96e2de694e15cc..4c29e5408bd6fe 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts @@ -39,7 +39,6 @@ import { ISessionsService } from '../../../../../services/sessions/browser/sessi import { IAgentHostActiveClientService } from '../../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.js'; import { LocalAgentHostSessionsProvider } from '../../browser/localAgentHostSessionsProvider.js'; import { AgentHostSessionAdapter } from '../../browser/baseAgentHostSessionsProvider.js'; -import { CHANGESET_UPDATE_THROTTLE_MS } from '../../browser/agentHostChangesetConstants.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; import { IGitHubService } from '../../../../github/browser/githubService.js'; @@ -3230,7 +3229,6 @@ suite('LocalAgentHostSessionsProvider - active-session branch changeset subscrip }, }], }); - await timeout(CHANGESET_UPDATE_THROTTLE_MS); // let the changeset throttle flush const changes = session.changes.get(); assert.deepStrictEqual(changes.map(change => { @@ -3324,7 +3322,6 @@ suite('LocalAgentHostSessionsProvider - active-session branch changeset subscrip files.push(makeChangesetFile(i, 0)); } agentHost.setChangesetState(key, { status: ChangesetStatus.Ready, files: [...files] }); - await timeout(CHANGESET_UPDATE_THROTTLE_MS); // flush the changeset throttle let previous = session.changes.get(); assert.strictEqual(previous.length, FILE_COUNT, 'every file should surface as a change'); @@ -3333,7 +3330,6 @@ suite('LocalAgentHostSessionsProvider - active-session branch changeset subscrip const changedIndex = update % FILE_COUNT; files[changedIndex] = makeChangesetFile(changedIndex, update + 1); agentHost.setChangesetState(key, { status: ChangesetStatus.Ready, files: [...files] }); - await timeout(CHANGESET_UPDATE_THROTTLE_MS); // flush the changeset throttle const next = session.changes.get(); @@ -3367,7 +3363,6 @@ suite('LocalAgentHostSessionsProvider - active-session branch changeset subscrip files.push(makeChangesetFile(i, 0)); } agentHost.setChangesetState(key, { status: ChangesetStatus.Ready, files: [...files] }); - await timeout(CHANGESET_UPDATE_THROTTLE_MS); // flush the changeset throttle // Index 0 is never touched; only the last file "streams" updates. const untouchedChangeBefore = session.changes.get()[0]; @@ -3377,61 +3372,10 @@ suite('LocalAgentHostSessionsProvider - active-session branch changeset subscrip for (let update = 0; update < UPDATE_COUNT; update++) { files[lastIndex] = makeChangesetFile(lastIndex, update + 1); agentHost.setChangesetState(key, { status: ChangesetStatus.Ready, files: [...files] }); - await timeout(CHANGESET_UPDATE_THROTTLE_MS); // flush the changeset throttle session.changes.get(); // force the derived chain to recompute } const untouchedChangeAfter = session.changes.get()[0]; assert.strictEqual(untouchedChangeAfter, untouchedChangeBefore, 'an unchanged file must reuse its change object across all updates'); })); - - // Performance-regression guard for the changeset update throttle - // - // While an agent streams edits, the host emits many envelopes per second. Each - // envelope fires the subscription's `onDidChange`; without throttling, each one - // would drive a full recompute of the `changes` list (and a relayout). The - // throttle must collapse a burst that arrives within one window into a single - // recompute carrying the final state. - // - // Reverting the throttle makes the burst recompute the list ~BURST times, so - // the `recomputes === 0` assertion (before the window elapses) fails. - test('coalesces a burst of changeset envelopes into a single changes recompute', () => runWithFakedTimers({ useFakeTimers: true, maxTaskCount: 1_000 }, async () => { - const provider = createProvider(disposables, agentHost, undefined, { activeSession }); - const session = addAndObserve(provider, 'sess-A'); - activeSession.set(makeActive('sess-A'), undefined); - - const FILE_COUNT = 20; - const BURST = 50; - const key = branchChangesKeyFor('sess-A'); - - const files: ChangesetState['files'] = []; - for (let i = 0; i < FILE_COUNT; i++) { - files.push(makeChangesetFile(i, 0)); - } - agentHost.setChangesetState(key, { status: ChangesetStatus.Ready, files: [...files] }); - await timeout(CHANGESET_UPDATE_THROTTLE_MS); // settle the initial state - - let recomputes = 0; - disposables.add(autorun(reader => { - session.changes.read(reader); - recomputes++; - })); - recomputes = 0; // ignore the autorun's initial run - - // Fire a burst of single-file updates with NO time passing in between, so - // they all land within one throttle window. - for (let i = 0; i < BURST; i++) { - files[0] = makeChangesetFile(0, i + 1); - agentHost.setChangesetState(key, { status: ChangesetStatus.Ready, files: [...files] }); - } - assert.strictEqual(recomputes, 0, `a burst of ${BURST} envelopes within one window must not recompute the list yet`); - - await timeout(CHANGESET_UPDATE_THROTTLE_MS); // flush the throttle - assert.strictEqual(recomputes, 1, 'the burst should collapse into exactly one recompute with the final state'); - - // And that single recompute carries the latest state. - const change0 = session.changes.get()[0]; - assert.ok(change0 && isIChatSessionFileChange2(change0)); - assert.strictEqual(change0.insertions, BURST, 'the coalesced update must reflect the final envelope'); - })); }); From 7a1df4f080af75f91d99b1b165b9875e062b610f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 29 Jun 2026 22:37:55 +0200 Subject: [PATCH 077/589] sessions: fix sessions picker closing immediately on Ctrl+R release (#323606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting picker.quickNavigate on the initial open caused the platform's built-in KEY_UP listener to accept the focused item as soon as the modifier was released after the first Ctrl+R — making the picker appear to not open at all. The correct pattern (matching Cmd+P): quickNavigate must only be set when the chord is pressed a SECOND time while the picker is already open. The navigate-next/previous keybinding rules already delegate to getQuickNavigateHandler which calls quickInputService.navigate() and sets quickNavigate at that point, so the initial open must not set it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/sessions/browser/sessionsActions.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts b/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts index db945c9ae68987..ad50d0da58c09b 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts @@ -16,7 +16,6 @@ import { Action2, MenuRegistry, MenuId, registerAction2, MenuItemAction } from ' import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; @@ -60,7 +59,6 @@ registerAction2(class ShowSessionsPickerAction extends Action2 { const quickInputService = accessor.get(IQuickInputService); const sessionsPartService = accessor.get(ISessionsPartService); const sessionsListModelService = accessor.get(ISessionsListModelService); - const keybindingService = accessor.get(IKeybindingService); const contextKeyService = accessor.get(IContextKeyService); const { recent, other } = sessionsService.getRecentlyOpenedSessions(); @@ -145,16 +143,6 @@ registerAction2(class ShowSessionsPickerAction extends Action2 { // Match on the detail row too so sessions can be found by their folder. picker.matchOnDetail = true; - // Enable quick navigation: when invoked via keybinding the user can keep - // the modifier held and press the trigger key again to cycle through - // sessions, then release the modifier to open the focused one (mirroring - // the editor switcher). The keybindings of this command drive which - // modifier release accepts the active item. - const keybindings = keybindingService.lookupKeybindings(SHOW_SESSIONS_PICKER_COMMAND_ID); - if (keybindings.length > 0) { - picker.quickNavigate = { keybindings }; - } - // Default to the currently active session so it is selected on open. if (activeItem) { picker.activeItems = [activeItem]; From 8b2d651ac97b8e8ef542398f7b9bf972b64b2cf6 Mon Sep 17 00:00:00 2001 From: Vijay Upadya <41652029+vijayupadya@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:39:05 -0700 Subject: [PATCH 078/589] Add telemetry to measure TAS (exp) call latency (#323601) * Telemetry to measure TAS latency * feedback updates --- .../assignment/common/assignmentService.ts | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts index 9fc26ab4cabef6..8290f4ad89f385 100644 --- a/src/vs/workbench/services/assignment/common/assignmentService.ts +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -20,6 +20,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { importAMDNodeModule } from '../../../../amdX.js'; import { timeout } from '../../../../base/common/async.js'; +import { StopWatch } from '../../../../base/common/stopwatch.js'; import { CopilotAssignmentFilterProvider } from './assignmentFilters.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../../base/common/event.js'; @@ -270,7 +271,15 @@ export class WorkbenchAssignmentService extends Disposable implements IAssignmen this.tasSetupDisposables.add(extensionsFilterProvider.onDidChangeFilters(() => this.refetchAssignments())); const tasConfig = this.productService.tasConfig!; - const tasClient = new (await importAMDNodeModule('tas-client', 'dist/tas-client.min.js')).ExperimentationService({ + + const tasClientModule = await importAMDNodeModule('tas-client', 'dist/tas-client.min.js'); + + // Measure the client-side latency of the first network call to the + // Treatment Assignment Service. The fetch is triggered by constructing + // the client, so start timing right before construction to exclude + // module loading time from the measurement. + const fetchStopWatch = StopWatch.create(); + const tasClient = new tasClientModule.ExperimentationService({ filterProviders: [filterProvider, extensionsFilterProvider], telemetry: this.telemetry, storageKey: ASSIGNMENT_STORAGE_KEY, @@ -284,11 +293,31 @@ export class WorkbenchAssignmentService extends Disposable implements IAssignmen await tasClient.initializePromise; tasClient.initialFetch.then(() => { this.networkInitialized = true; - }); + this.logFetchLatency('initial', fetchStopWatch.elapsed()); + }).catch(() => undefined); return tasClient; } + private logFetchLatency(fetchType: 'initial' | 'refetch', durationMs: number): void { + type TASClientFetchLatencyData = { + fetchType: string; + durationMs: number; + }; + + type TASClientFetchLatencyClassification = { + owner: 'sbatten'; + comment: 'Measures the client-side latency of fetching treatment assignments from the experiment service (TAS)'; + fetchType: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether this was the initial fetch or a refetch' }; + durationMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds the fetch took to complete' }; + }; + + this.telemetryService.publicLog2('tasClientFetchLatency', { + fetchType, + durationMs + }); + } + private async refetchAssignments(): Promise { if (!this.tasClient) { return; // Setup has not started, assignments will use latest filters @@ -298,8 +327,10 @@ export class WorkbenchAssignmentService extends Disposable implements IAssignmen const tasClient = await this.tasClient; await tasClient.initialFetch; - // Refresh the assignments + // Refresh the assignments and measure the network latency of the refetch. + const refetchStopWatch = StopWatch.create(); await tasClient.getTreatmentVariableAsync('vscode', 'refresh', false); + this.logFetchLatency('refetch', refetchStopWatch.elapsed()); } async getCurrentExperiments(): Promise { From cd7702cd644bb3de65f8686aa037ca966293d7cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:54:29 +0000 Subject: [PATCH 079/589] build(deps): bump js-yaml from 3.14.2 to 3.15.0 in /extensions/npm (#323612) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.2 to 3.15.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.2...3.15.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 3.15.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/npm/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/npm/package-lock.json b/extensions/npm/package-lock.json index 3f94141535afc0..1b77aeac60dd45 100644 --- a/extensions/npm/package-lock.json +++ b/extensions/npm/package-lock.json @@ -150,9 +150,9 @@ } }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", From e0f1be709824045e3d6296cf493edd9fa411827f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:03:04 -0700 Subject: [PATCH 080/589] build(deps): bump protobufjs, @opentelemetry/exporter-logs-otlp-grpc, @opentelemetry/exporter-logs-otlp-http, @opentelemetry/exporter-logs-otlp-proto, @opentelemetry/exporter-metrics-otlp-grpc, @opentelemetry/exporter-metrics-otlp-http, @opentelemetry/exporter-metrics-otlp-proto, @opentelemetry/exporter-trace-otlp-grpc, @opentelemetry/exporter-trace-otlp-http and @opentelemetry/exporter-trace-otlp-proto in /extensions/copilot (#323447) build(deps): bump protobufjs, @opentelemetry/exporter-logs-otlp-grpc, @opentelemetry/exporter-logs-otlp-http, @opentelemetry/exporter-logs-otlp-proto, @opentelemetry/exporter-metrics-otlp-grpc, @opentelemetry/exporter-metrics-otlp-http, @opentelemetry/exporter-metrics-otlp-proto, @opentelemetry/exporter-trace-otlp-grpc, @opentelemetry/exporter-trace-otlp-http and @opentelemetry/exporter-trace-otlp-proto Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) to 7.6.4 and updates ancestor dependencies [protobufjs](https://github.com/protobufjs/protobuf.js), [@opentelemetry/exporter-logs-otlp-grpc](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/exporter-logs-otlp-http](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/exporter-logs-otlp-proto](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/exporter-metrics-otlp-grpc](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/exporter-metrics-otlp-http](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/exporter-metrics-otlp-proto](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/exporter-trace-otlp-grpc](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/exporter-trace-otlp-http](https://github.com/open-telemetry/opentelemetry-js) and [@opentelemetry/exporter-trace-otlp-proto](https://github.com/open-telemetry/opentelemetry-js). These dependencies need to be updated together. Updates `protobufjs` from 7.5.8 to 7.6.4 - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.6.4/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.8...protobufjs-v7.6.4) Updates `@opentelemetry/exporter-logs-otlp-grpc` from 0.214.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.214.0...experimental/v0.219.0) Updates `@opentelemetry/exporter-logs-otlp-http` from 0.214.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.214.0...experimental/v0.219.0) Updates `@opentelemetry/exporter-logs-otlp-proto` from 0.214.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.214.0...experimental/v0.219.0) Updates `@opentelemetry/exporter-metrics-otlp-grpc` from 0.214.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.214.0...experimental/v0.219.0) Updates `@opentelemetry/exporter-metrics-otlp-http` from 0.214.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.214.0...experimental/v0.219.0) Updates `@opentelemetry/exporter-metrics-otlp-proto` from 0.214.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.214.0...experimental/v0.219.0) Updates `@opentelemetry/exporter-trace-otlp-grpc` from 0.214.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.214.0...experimental/v0.219.0) Updates `@opentelemetry/exporter-trace-otlp-http` from 0.214.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.214.0...experimental/v0.219.0) Updates `@opentelemetry/exporter-trace-otlp-proto` from 0.214.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.214.0...experimental/v0.219.0) --- updated-dependencies: - dependency-name: "@opentelemetry/exporter-logs-otlp-grpc" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/exporter-logs-otlp-http" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/exporter-logs-otlp-proto" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/exporter-metrics-otlp-grpc" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/exporter-metrics-otlp-http" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/exporter-metrics-otlp-proto" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/exporter-trace-otlp-grpc" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/exporter-trace-otlp-http" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/exporter-trace-otlp-proto" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: protobufjs dependency-version: 7.6.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/copilot/package-lock.json | 513 +++++++++++++-------------- extensions/copilot/package.json | 18 +- 2 files changed, 265 insertions(+), 266 deletions(-) diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index 722f833e9306d9..49404274782846 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -20,15 +20,15 @@ "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.212.0", - "@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", - "@opentelemetry/exporter-logs-otlp-proto": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", + "@opentelemetry/exporter-logs-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.219.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.219.0", "@opentelemetry/resources": "^2.5.1", "@opentelemetry/sdk-logs": "^0.212.0", "@opentelemetry/sdk-metrics": "^2.5.1", @@ -4070,17 +4070,17 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.214.0.tgz", - "integrity": "sha512-SwmFRwO8mi6nndzbsjPgSFg7qy1WeNHRFD+s6uCsdiUDUt3+yzI2qiHE3/ub2f37+/CbeGcG+Ugc8Gwr6nu2Aw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.219.0.tgz", + "integrity": "sha512-7SvzDCIclHWAcCwZ1MTOLcwn4BVNPGI3QxS/DJraPNe1TTL+4TvUBq5zeQV8tsnYvtDN7wKW2qocVmaCP2l7sQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/sdk-logs": "0.214.0" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/sdk-logs": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4090,9 +4090,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", - "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -4102,9 +4102,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4117,12 +4117,12 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4133,14 +4133,14 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", - "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4151,16 +4151,16 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.214.0.tgz", - "integrity": "sha512-9qv2Tl/Hq6qc5pJCbzFJnzA0uvlb9DgM70yGJPYf3bA5LlLkRCpcn81i4JbcIH4grlQIWY6A+W7YG0LLvS1BAw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.219.0.tgz", + "integrity": "sha512-mhl2HL6GmZI8b8PwPfqMws/5ovJfbRTxwc9Y5agVVHiQ+e5SL1btsFr/kJDgt7YCexDtsUn5HAreHQO9szFS0A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/sdk-logs": "0.214.0" + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/sdk-logs": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4170,9 +4170,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/api-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", - "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -4182,9 +4182,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4197,12 +4197,12 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4213,14 +4213,14 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", - "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4231,18 +4231,18 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.214.0.tgz", - "integrity": "sha512-IWAVvCO1TlpotRjFmhQFz9RSfQy5BsLtDRBtptSrXZRwfyRPpuql/RMe5zdmu0Gxl3ERDFwOzOqkf3bwy7Jzcw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.219.0.tgz", + "integrity": "sha512-Ayw4Gf71PS9jhBVaYywa4WsajnqfDehMkTdVH3TSAVHqPcsAv/AhH/wTNRYNt99szeYr6Gbd/D6RjZD77wAxHg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-logs": "0.214.0", - "@opentelemetry/sdk-trace-base": "2.6.1" + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4252,9 +4252,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", - "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -4264,9 +4264,9 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4279,12 +4279,12 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4295,14 +4295,14 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", - "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4313,13 +4313,13 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", - "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4330,19 +4330,19 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.214.0.tgz", - "integrity": "sha512-0NGxWHVYHgbp51SEzmsP+Hdups81eRs229STcSWHo3WO0aqY6RpJ9csxfyEtFgaNrBDv6UfOh0je4ss/ROS6XA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.219.0.tgz", + "integrity": "sha512-6LaaSrPxK5L55bXevWajvOMxGOpNm0n12tG53TeZaUeNzXwLPg6d2KCC1zAlGsojan+xRG71mA4Qqs9K2VVrKQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-metrics": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4352,9 +4352,9 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4367,12 +4367,12 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4383,16 +4383,16 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.214.0.tgz", - "integrity": "sha512-Tx/59RmjBgkXJ3qnsD04rpDrVWL53LU/czpgLJh+Ab98nAroe91I7vZ3uGN9mxwPS0jsZEnmqmHygVwB2vRMlA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.219.0.tgz", + "integrity": "sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-metrics": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4402,9 +4402,9 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4417,12 +4417,12 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4433,17 +4433,17 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.214.0.tgz", - "integrity": "sha512-pJIcghFGhx3VSCgP5U+yZx+OMNj0t+ttnhC8IjL5Wza7vWIczctF6t3AGcVQffi2dEqX+ZHANoBwoPR8y6RMKA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.219.0.tgz", + "integrity": "sha512-DUS7XyIiEnoeccQUvuKy0G2/YqeKhpN8FVIrGbrLNIVMj10yeIFLRzRv0tibCI2kXXvlTTABVexGAk78wHk2ug==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-metrics": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4453,9 +4453,9 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4468,12 +4468,12 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4484,18 +4484,18 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.214.0.tgz", - "integrity": "sha512-FWRZ7AWoTryYhthralHkfXUuyO3l7cRsnr49WcDio1orl2a7KxT8aDZdwQtV1adzoUvZ9Gfo+IstElghCS4zfw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.219.0.tgz", + "integrity": "sha512-BkDNv1UD6BscW19MxbAxVmSYSSFuyeqR6buV2/HTYqA7GrR0EbTFzqG6h86T3PtXmpdbsWjMGLDdjG2rikG27Q==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-trace-base": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4505,9 +4505,9 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4520,12 +4520,12 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4536,13 +4536,13 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", - "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4553,16 +4553,16 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.214.0.tgz", - "integrity": "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.219.0.tgz", + "integrity": "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-trace-base": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4572,9 +4572,9 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4587,12 +4587,12 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4603,13 +4603,13 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", - "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4620,16 +4620,16 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.214.0.tgz", - "integrity": "sha512-ON0spYWb2yAdQ9b+ItNyK0c6qdtcs+0eVR4YFJkhJL7agfT8sHFg0e5EesauSRiTHPZHiDobI92k77q0lwAmqg==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.219.0.tgz", + "integrity": "sha512-lF/LUBfhOFmxJa+SQsLN7ziV4MHa2pyKgOM6JNehSOfU+npjM4gwm9oIKEJrzrWcexMcqydiyoFy0XCb1Ql3wQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-trace-base": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4639,9 +4639,9 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4654,12 +4654,12 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4670,13 +4670,13 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", - "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4705,13 +4705,13 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.214.0.tgz", - "integrity": "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz", + "integrity": "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-transformer": "0.214.0" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-transformer": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4721,9 +4721,9 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4736,15 +4736,15 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.214.0.tgz", - "integrity": "sha512-IDP6zcyA24RhNZ289MP6eToIZcinlmirHjX8v3zKCQ2ZhPpt5cGwkN91tCth337lqHIgWcTy90uKRiX/SzALDw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.219.0.tgz", + "integrity": "sha512-iIk/s8QQu39zpTrRRmsW/Eg3SE2+Hg8tLWepr2FLRgmwUpNd0IpCTLJEHJ77hpt4hgIS8MAh44UYI4xQPZwWlw==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/otlp-exporter-base": "0.214.0", - "@opentelemetry/otlp-transformer": "0.214.0" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4754,9 +4754,9 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4769,18 +4769,17 @@ } }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.214.0.tgz", - "integrity": "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz", + "integrity": "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", - "@opentelemetry/sdk-logs": "0.214.0", - "@opentelemetry/sdk-metrics": "2.6.1", - "@opentelemetry/sdk-trace-base": "2.6.1", - "protobufjs": "^7.0.0" + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-metrics": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4790,9 +4789,9 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", - "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -4802,9 +4801,9 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4817,12 +4816,12 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4833,14 +4832,14 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", - "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4851,13 +4850,13 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", - "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4931,13 +4930,13 @@ } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", - "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", + "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4947,9 +4946,9 @@ } }, "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4962,12 +4961,12 @@ } }, "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index e307377d8d2c44..2fb215472cd0c3 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -7201,15 +7201,15 @@ "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.212.0", - "@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", - "@opentelemetry/exporter-logs-otlp-proto": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", + "@opentelemetry/exporter-logs-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.219.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.219.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.219.0", "@opentelemetry/resources": "^2.5.1", "@opentelemetry/sdk-logs": "^0.212.0", "@opentelemetry/sdk-metrics": "^2.5.1", From 90b3b82a5cb3ad9f714560bed8cff693fc3d5f83 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:04:04 -0700 Subject: [PATCH 081/589] Agents mobile: native bottom-sheet confirmation for archive-all (#323607) On phone layout the desktop confirmation dialog for the destructive "Mark All as Done" bulk action rendered with the safe (Cancel) button pushed off-screen. Rather than patch the dialog with CSS, route the confirmation through the existing showMobileContentSheet primitive (matching the other Agents-window mobile surfaces) with full-width destructive + Cancel buttons and 44px tap targets. Desktop/tablet keep the existing dialog. The sheet builder lives in its own file under the feature's mobile/ folder (mobileArchiveConfirmSheet.ts), mirroring the other mobile sheet builders. It moves focus into the sheet (defaulting to the safe Cancel action) and restores focus on close, like IDialogService.confirm. The sheet intentionally omits the "Do not ask me again" opt-out: the persisted skip flag is shared across layouts, so a single mis-tap on the cramped phone dialog could otherwise permanently disable confirmation for a destructive bulk action. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../parts/mobile/media/mobilePickerSheet.css | 33 +++++++ .../mobile/mobileArchiveConfirmSheet.ts | 89 +++++++++++++++++++ .../browser/views/sessionsViewActions.ts | 42 ++++++--- 3 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 src/vs/sessions/contrib/sessions/browser/mobile/mobileArchiveConfirmSheet.ts diff --git a/src/vs/sessions/browser/parts/mobile/media/mobilePickerSheet.css b/src/vs/sessions/browser/parts/mobile/media/mobilePickerSheet.css index 7c66f51eb94628..fd35aadaa511ac 100644 --- a/src/vs/sessions/browser/parts/mobile/media/mobilePickerSheet.css +++ b/src/vs/sessions/browser/parts/mobile/media/mobilePickerSheet.css @@ -251,6 +251,39 @@ touch-action: pan-y; } +/* Generic building blocks for `showMobileContentSheet` bodies that present a + * short confirmation: a bold message, an optional muted detail line, and a + * vertical stack of full-width action buttons with comfortable (44pt) tap + * targets. Kept caller-agnostic so any confirm-style sheet can share them. */ +.mobile-content-sheet-message { + padding: 4px 16px 0; + font-size: 15px; + font-weight: 600; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.mobile-content-sheet-detail { + padding: 6px 16px 0; + font-size: 13px; + color: var(--vscode-descriptionForeground); + line-height: 1.35; + overflow-wrap: anywhere; +} + +.mobile-content-sheet-actions { + display: flex; + flex-direction: column; + gap: 8px; + padding: 16px; +} + +.mobile-content-sheet-actions .monaco-button { + min-height: 44px; + font-size: 16px; + padding: 0 12px; +} + /* iOS grouped-list section header. The HIG uses a regular-case * footnote-sized label in secondary color — not the uppercased small * caps that older Settings rows used. */ diff --git a/src/vs/sessions/contrib/sessions/browser/mobile/mobileArchiveConfirmSheet.ts b/src/vs/sessions/contrib/sessions/browser/mobile/mobileArchiveConfirmSheet.ts new file mode 100644 index 00000000000000..221e04ccf4c15b --- /dev/null +++ b/src/vs/sessions/contrib/sessions/browser/mobile/mobileArchiveConfirmSheet.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, append, getActiveElement, isHTMLElement } from '../../../../../base/browser/dom.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../nls.js'; +import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { IWorkbenchLayoutService } from '../../../../../workbench/services/layout/browser/layoutService.js'; +import { showMobileContentSheet } from '../../../../browser/parts/mobile/mobilePickerSheet.js'; + +export interface IMobileArchiveConfirmOptions { + /** Sheet header title (single line). */ + readonly title: string; + /** Bold message describing the scope of the action (wraps). */ + readonly message: string; + /** Muted detail line beneath the message (wraps). */ + readonly detail: string; + /** Label for the primary, destructive action button. */ + readonly primaryLabel: string; +} + +/** + * Phone-only confirmation for a destructive "Mark All as Done" bulk action. + * Presents a native bottom sheet (matching the other Agents-window mobile + * surfaces) with an explicit, full-width destructive action and Cancel, + * instead of squeezing the desktop modal dialog onto a phone. + * + * Resolves `true` only when the user taps the destructive action; any other + * dismissal (Cancel, backdrop tap, Escape) resolves `false`. + * + * Unlike the desktop dialog, the sheet intentionally omits the "Do not ask + * me again" opt-out: the persisted skip flag is shared across layouts, and + * letting a mis-tap permanently disable confirmation for a destructive bulk + * action is exactly the footgun this mobile treatment avoids. + */ +export async function confirmArchiveOnPhone(layoutService: IWorkbenchLayoutService, options: IMobileArchiveConfirmOptions): Promise { + let confirmed = false; + + // Remember what had focus so we can restore it when the sheet closes, + // mirroring `IDialogService.confirm`'s focus handling. + const previouslyFocused = getActiveElement(); + + await showMobileContentSheet( + layoutService.mainContainer, + options.title, + (body, api) => { + const store = new DisposableStore(); + + const message = append(body, $('.mobile-content-sheet-message')); + message.textContent = options.message; + + const detail = append(body, $('.mobile-content-sheet-detail')); + detail.textContent = options.detail; + + const actions = append(body, $('.mobile-content-sheet-actions')); + + const archiveButton = store.add(new Button(actions, { ...defaultButtonStyles })); + archiveButton.label = options.primaryLabel; + store.add(archiveButton.onDidClick(() => { + confirmed = true; + api.close(); + })); + + const cancelButton = store.add(new Button(actions, { ...defaultButtonStyles, secondary: true })); + cancelButton.label = localize('mobileArchiveConfirm.cancel', "Cancel"); + store.add(cancelButton.onDidClick(() => { + confirmed = false; + api.close(); + })); + + // Move focus into the modal sheet for keyboard / screen reader + // users, defaulting to the safe action so an accidental Enter + // cancels rather than performing the destructive action. + cancelButton.focus(); + + return store; + }, + { hideDoneButton: true }, + ); + + if (isHTMLElement(previouslyFocused) && previouslyFocused.isConnected) { + previouslyFocused.focus(); + } + + return confirmed; +} diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts index 21ccc7bf938c48..1a68cd04b91f5f 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts @@ -33,6 +33,9 @@ import { ActiveSessionContextKeys } from '../../../changes/common/changes.js'; import { hasActiveSessionFailedCIChecks } from '../../../changes/browser/checksActions.js'; import { ISessionsPartService } from '../../../../services/sessions/browser/sessionsPartService.js'; import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; +import { IWorkbenchLayoutService } from '../../../../../workbench/services/layout/browser/layoutService.js'; +import { isPhoneLayout } from '../../../../browser/parts/mobile/mobileLayout.js'; +import { confirmArchiveOnPhone } from '../mobile/mobileArchiveConfirmSheet.js'; const CLOSE_SESSION_COMMAND_ID = 'sessionsViewPane.closeSession'; registerAction2(class CloseSessionAction extends Action2 { @@ -512,24 +515,37 @@ registerAction2(class ArchiveSectionAction extends Action2 { const sessionsManagementService = accessor.get(ISessionsManagementService); const dialogService = accessor.get(IDialogService); const storageService = accessor.get(IStorageService); + const layoutService = accessor.get(IWorkbenchLayoutService); const skipConfirmation = storageService.getBoolean(ConfirmArchiveStorageKey, StorageScope.PROFILE, false); if (!skipConfirmation) { - const confirmed = await dialogService.confirm({ - message: getArchiveSectionConfirmationMessage(context), - detail: localize('archiveSectionSessions.detail', "You can restore sessions later if needed from the sessions view."), - primaryButton: localize('archiveSectionSessions.archive', "Mark All as Done"), - checkbox: { - label: localize('doNotAskAgain', "Do not ask me again") + if (isPhoneLayout(layoutService)) { + const confirmed = await confirmArchiveOnPhone(layoutService, { + title: localize('archiveSectionSheet.title', "Mark All as Done"), + message: getArchiveSectionConfirmationMessage(context), + detail: localize('archiveSectionSessions.detail', "You can restore sessions later if needed from the sessions view."), + primaryLabel: localize('archiveSectionSessions.archive', "Mark All as Done"), + }); + if (!confirmed) { + return; + } + } else { + const confirmed = await dialogService.confirm({ + message: getArchiveSectionConfirmationMessage(context), + detail: localize('archiveSectionSessions.detail', "You can restore sessions later if needed from the sessions view."), + primaryButton: localize('archiveSectionSessions.archive', "Mark All as Done"), + checkbox: { + label: localize('doNotAskAgain', "Do not ask me again") + } + }); + + if (!confirmed.confirmed) { + return; } - }); - - if (!confirmed.confirmed) { - return; - } - if (confirmed.checkboxChecked) { - storageService.store(ConfirmArchiveStorageKey, true, StorageScope.PROFILE, StorageTarget.USER); + if (confirmed.checkboxChecked) { + storageService.store(ConfirmArchiveStorageKey, true, StorageScope.PROFILE, StorageTarget.USER); + } } } From ed0f5b6084a226b715c335f3f38b0873a808f11c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:36:18 +0200 Subject: [PATCH 082/589] Simplify session time buckets to Recent and Older (#323618) * Simplify session time buckets to Recent and Older Replace the today/yesterday/last-7-days/older date buckets in the sessions list with two buckets: Recent (up to 10 sessions from the last 7 days) and Older (the rest). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review: make RECENT_SESSIONS_LIMIT private, add groupByDate tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/SESSIONS_LIST.md | 6 +-- .../sessions/browser/views/sessionsList.ts | 29 +++++----- .../test/browser/sessionsList.test.ts | 54 ++++++++++++++++++- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/src/vs/sessions/SESSIONS_LIST.md b/src/vs/sessions/SESSIONS_LIST.md index ddc94a4b14506d..78575ae385e56d 100644 --- a/src/vs/sessions/SESSIONS_LIST.md +++ b/src/vs/sessions/SESSIONS_LIST.md @@ -48,7 +48,7 @@ Sessions are organized into sections with fixed priority: Two grouping modes (user-switchable): - **By Workspace** (default) — user groups and one section per workspace label share a single, freely-reorderable user-managed order below Pinned. By default groups come first and workspaces are alphabetical ("Unknown" workspace last) until the user drags them. -- **By Date** — user groups form a contiguous, user-ordered block directly below Pinned; the non-grouped sessions follow in the fixed date sections (Today, Yesterday, Last 7 Days, Older). Groups never mix into the date sections. +- **By Date** — user groups form a contiguous, user-ordered block directly below Pinned; the non-grouped sessions follow in the fixed date sections (Recent, Older), where Recent holds up to 10 sessions from the last 7 days and Older holds the rest. Groups never mix into the date sections. User groups are **fully user-managed**: their order is owned by `ISessionSectionOrderService`, defaults to newest-first, and is shared across both grouping modes (it no longer derives from the recency of a group's member sessions). @@ -101,7 +101,7 @@ Regular sessions can be reordered by dragging them up or down within the list. P - **Storage** — reordering stores a synthetic numeric *sort key* per session in `ISessionsListModelService` (persisted locally, not synced). It is used **only** for sorting; the provider's real `createdAt`/`updatedAt` are never modified. A separate override map is kept for each sort mode (Created vs Updated). - **Sort key** — on drop, the new key is the midpoint between the effective keys of the sessions immediately above and below the drop point. Dropping above the first session uses the current time (so it sorts to the top). Dropping below the last session steps below the last key. - **Dropping the fake value** — if a session's natural timestamp already sorts it into the dropped slot (e.g. after dragging it down and back), the stored override is removed so the list falls back to natural ordering. -- **Grouping by Date** — the regular list is one continuous sequence, so dragging can move a session across date buckets (e.g. to the top makes it "Today"). +- **Grouping by Date** — the regular list is one continuous sequence, so dragging can move a session across date buckets (e.g. to the top makes it "Recent"). - **Grouping by Workspace** — reordering is restricted to within the same workspace group; drops onto another workspace are rejected. - **Pinned** — dropping a non-archived session on the Pinned header pins it and lets it sort naturally. Dropping it on a pinned session shows an insertion line, pins it, and stores the sort key needed to place it at that location. - **User groups** — dropping a non-archived session on a group header adds or moves it into the group and lets it sort naturally. Dropping it on a session inside the group shows an insertion line for the exact slot and highlights only the group header to indicate the receiving group. @@ -220,7 +220,7 @@ Context keys available for `when` clauses when contributing to session list menu | Key | Type | Description | |-----|------|-------------| -| `sessionSection.type` | string | `'pinned'`, `'archived'`, `'workspace: