From a97aec123f168ca9a8ba889675b6f24b69dd0da4 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Wed, 29 Jul 2026 17:50:46 -0700 Subject: [PATCH 01/86] agentHost: use Responses API for BYOK models Preserve structured reasoning, tool calls, continuation metadata, and usage across the Agent Host renderer bridge while replacing the Chat Completions proxy contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../vscode-node/languageModelAccess.ts | 9 +- .../vscode-node/languageModelAccessPrompt.tsx | 8 +- .../agentHost/common/agentHostByokLm.ts | 115 ++-- .../node/copilot/byokLmProxyService.ts | 69 +-- .../node/copilot/byokOpenAiTranslation.ts | 262 ---------- .../node/copilot/byokResponsesTranslation.ts | 489 ++++++++++++++++++ .../node/copilot/copilotSessionLauncher.ts | 4 +- .../node/agentHostClientByokLmChannel.test.ts | 42 +- .../test/node/byokLmBridgeRegistry.test.ts | 2 +- .../test/node/byokLmProxyService.test.ts | 118 ++--- .../test/node/byokOpenAiTranslation.test.ts | 150 ------ .../node/byokResponsesTranslation.test.ts | 197 +++++++ .../test/node/copilotSessionLauncher.test.ts | 21 +- .../copilotByokResponses.integrationTest.ts | 129 +++++ .../agentHost/agentHostByokLmHandler.ts | 257 +++++++-- .../agentHostByokLmHandler.test.ts | 117 +++-- 16 files changed, 1340 insertions(+), 649 deletions(-) delete mode 100644 src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts create mode 100644 src/vs/platform/agentHost/node/copilot/byokResponsesTranslation.ts delete mode 100644 src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts create mode 100644 src/vs/platform/agentHost/test/node/byokResponsesTranslation.test.ts create mode 100644 src/vs/platform/agentHost/test/node/providerIntegration/copilotByokResponses.integrationTest.ts diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts index 02e314bc338..4160085627a 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -840,8 +840,13 @@ export class CopilotLanguageModelWrapper extends Disposable { let thinkingActive = false; const finishCallback: FinishedCallback = async (_text, index, delta): Promise => { if (delta.thinking) { - // Show thinking progress for unencrypted thinking deltas - if (!isEncryptedThinkingDelta(delta.thinking)) { + if (isEncryptedThinkingDelta(delta.thinking)) { + progress.report(new vscode.LanguageModelThinkingPart( + delta.thinking.text ?? '', + delta.thinking.id, + { encrypted_content: delta.thinking.encrypted } + )); + } else { const text = delta.thinking.text ?? ''; progress.report(new vscode.LanguageModelThinkingPart(text, delta.thinking.id, delta.thinking.metadata)); thinkingActive = true; diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx index 5d2720bf68f..e62f6a0e39b 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx @@ -41,10 +41,14 @@ export class LanguageModelAccessPrompt extends PromptElement { // There should only be one string part per message const content = filteredContent.find(part => part instanceof LanguageModelTextPart); const toolCalls = filteredContent.filter(part => part instanceof vscode.LanguageModelToolCallPart); - const thinking = filteredContent.find(part => part instanceof vscode.LanguageModelThinkingPart); + const thinkingParts = filteredContent.filter(part => part instanceof vscode.LanguageModelThinkingPart); + const thinking = thinkingParts.find(part => typeof part.metadata?.encrypted_content === 'string') ?? thinkingParts.at(-1); + const thinkingText = thinkingParts.flatMap(part => Array.isArray(part.value) ? part.value : [part.value]); + const thinkingMetadata = Object.assign({}, ...thinkingParts.map(part => part.metadata)); const statefulMarkerElement = statefulMarker && ; - const thinkingElement = thinking && thinking.id && ; + const encrypted = typeof thinkingMetadata.encrypted_content === 'string' ? thinkingMetadata.encrypted_content : undefined; + const thinkingElement = thinking && thinking.id && ; chatMessages.push( ({ id: tc.callId, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.input) } }))}>{statefulMarkerElement}{content?.value}{thinkingElement}); } else if (message.role === vscode.LanguageModelChatMessageRole.User) { for (const part of message.content) { diff --git a/src/vs/platform/agentHost/common/agentHostByokLm.ts b/src/vs/platform/agentHost/common/agentHostByokLm.ts index dec18d3be10..772b6e5db88 100644 --- a/src/vs/platform/agentHost/common/agentHostByokLm.ts +++ b/src/vs/platform/agentHost/common/agentHostByokLm.ts @@ -15,64 +15,109 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; * 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 + * translation step. The node side converts OpenAI Responses 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. */ +export interface IByokLmTextPart { + readonly type: 'text'; + readonly text: string; +} + +export interface IByokLmMessageItem { + readonly type: 'message'; + readonly role: 'system' | 'developer' | 'user' | 'assistant'; + readonly content: IByokLmTextPart[]; +} + +export interface IByokLmReasoningItem { + readonly type: 'reasoning'; + readonly id?: string; + readonly summary: string[]; + readonly encryptedContent?: string; + readonly metadata?: Record; +} + +export interface IByokLmFunctionCallItem { + readonly type: 'function_call'; + readonly callId: string; readonly name: string; - /** JSON-encoded tool input, which may be an object or a freeform string. */ readonly argumentsJson: string; } -/** A tool/function the model may call. */ -export interface IByokLmTool { +export interface IByokLmFunctionCallOutputItem { + readonly type: 'function_call_output'; + readonly callId: string; + readonly output: string; +} + +export interface IByokLmCustomToolCallItem { + readonly type: 'custom_tool_call'; + readonly callId: string; + readonly name: string; + readonly input: string; +} + +export interface IByokLmCustomToolCallOutputItem { + readonly type: 'custom_tool_call_output'; + readonly callId: string; + readonly output: string; +} + +export type IByokLmInputItem = + IByokLmMessageItem | + IByokLmReasoningItem | + IByokLmFunctionCallItem | + IByokLmFunctionCallOutputItem | + IByokLmCustomToolCallItem | + IByokLmCustomToolCallOutputItem; + +export interface IByokLmFunctionTool { + readonly type: 'function'; 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; +export interface IByokLmCustomTool { + readonly type: 'custom'; + readonly name: string; + readonly description?: string; } -/** A chat request forwarded from the proxy to the renderer LM API. */ +export type IByokLmTool = IByokLmFunctionTool | IByokLmCustomTool; + 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 instructions?: string; + readonly input: IByokLmInputItem[]; readonly tools?: IByokLmTool[]; - /** Opaque per-request model options forwarded to the LM provider. */ + readonly previousResponseId?: string; + readonly reasoningEffort?: string; readonly modelOptions?: Record; } -/** The (buffered) completion produced by the renderer LM API. */ +export interface IByokLmOutputMessageItem { + readonly type: 'message'; + readonly content: IByokLmTextPart[]; +} + +export type IByokLmOutputItem = + IByokLmOutputMessageItem | + IByokLmReasoningItem | + IByokLmFunctionCallItem | + IByokLmCustomToolCallItem; + 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 output: IByokLmOutputItem[]; + readonly responseId?: string; readonly usage?: { - readonly promptTokens?: number; - readonly completionTokens?: number; + readonly inputTokens?: number; + readonly outputTokens?: number; + readonly reasoningTokens?: number; }; - /** Set when the LM call failed; `content` is then empty. */ readonly error?: string; } @@ -119,7 +164,7 @@ export interface IAgentHostByokLmHandler { readonly onDidChangeModels?: Event; /** - * Run a BYOK chat completion against the extension-registered model that + * Run a BYOK Responses request against the extension-registered model that * matches `request.vendor` + `request.modelId`. Rejects (or resolves with * {@link IByokLmChatResult.error}) when no such model is available. */ diff --git a/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts b/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts index f101cd10c64..9928fd2782a 100644 --- a/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts +++ b/src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts @@ -16,12 +16,13 @@ import { readProxyRequestBody, } from '../shared/loopbackProxyServer.js'; import { - IOpenAiChatRequest, - OpenAiTranslationError, - bridgeResultToSseFrames, - openAiErrorBody, - openAiRequestToBridge, -} from './byokOpenAiTranslation.js'; + bridgeResultToResponsesBody, + bridgeResultToResponsesSseFrames, + IResponsesRequest, + responsesErrorBody, + responsesRequestToBridge, + ResponsesTranslationError, +} from './byokResponsesTranslation.js'; // #region Public types @@ -45,7 +46,7 @@ export interface IByokLmProxyHandle extends ILoopbackProxyHandle { /** * 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. + * runtime appends `/responses` to this URL. */ providerBaseUrl(vendor: string): string; } @@ -69,7 +70,7 @@ export interface IByokLmProxyService { const PROXY_USER_FACING_NAME = 'ByokLmProxyService'; const VENDOR_PATH_PREFIX = '/v/'; -const CHAT_COMPLETIONS_SUFFIX = '/chat/completions'; +const RESPONSES_SUFFIX = '/responses'; /** * The BYOK proxy keeps no per-bind mutable state: the active renderer bridge is @@ -81,11 +82,11 @@ 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, + * `type: 'openai'`, `wireApi: 'responses'` provider whose `baseUrl` points + * here; inbound `POST /v//responses` 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. + * as OpenAI Responses SSE. * * The server lifecycle — lazy bind on `127.0.0.1`, nonce minting, refcounted * handles, in-flight tracking, and teardown — is inherited from @@ -149,9 +150,9 @@ export class ByokLmProxyService extends LoopbackProxyServer im return; } - const vendor = this._parseVendorFromChatPath(pathname); + const vendor = this._parseVendorFromResponsesPath(pathname); if (method === 'POST' && vendor !== undefined) { - await this._handleChatCompletions(req, res, runtime, vendor); + await this._handleResponses(req, res, runtime, vendor); return; } @@ -159,14 +160,13 @@ export class ByokLmProxyService extends LoopbackProxyServer im } /** - * Extract the vendor from a `/v//chat/completions` path, or return - * `undefined` when the path is not a chat-completions route. + * Extract the vendor from a `/v//responses` path. */ - private _parseVendorFromChatPath(pathname: string): string | undefined { - if (!pathname.startsWith(VENDOR_PATH_PREFIX) || !pathname.endsWith(CHAT_COMPLETIONS_SUFFIX)) { + private _parseVendorFromResponsesPath(pathname: string): string | undefined { + if (!pathname.startsWith(VENDOR_PATH_PREFIX) || !pathname.endsWith(RESPONSES_SUFFIX)) { return undefined; } - const vendorSegment = pathname.slice(VENDOR_PATH_PREFIX.length, pathname.length - CHAT_COMPLETIONS_SUFFIX.length); + const vendorSegment = pathname.slice(VENDOR_PATH_PREFIX.length, pathname.length - RESPONSES_SUFFIX.length); if (!vendorSegment) { return undefined; } @@ -185,11 +185,11 @@ export class ByokLmProxyService extends LoopbackProxyServer im return vendor; } - private async _handleChatCompletions(req: http.IncomingMessage, res: http.ServerResponse, runtime: ILoopbackProxyRuntime, vendor: string): Promise { - let body: IOpenAiChatRequest; + private async _handleResponses(req: http.IncomingMessage, res: http.ServerResponse, runtime: ILoopbackProxyRuntime, vendor: string): Promise { + let body: IResponsesRequest; try { const raw = await readProxyRequestBody(req); - body = JSON.parse(raw) as IOpenAiChatRequest; + body = JSON.parse(raw) as IResponsesRequest; } catch (err) { this._writeJsonError(res, 400, `Invalid request body: ${err instanceof Error ? err.message : String(err)}`, 'invalid_request_error'); return; @@ -197,9 +197,9 @@ export class ByokLmProxyService extends LoopbackProxyServer im let bridgeRequest; try { - bridgeRequest = openAiRequestToBridge(vendor, body); + bridgeRequest = responsesRequestToBridge(vendor, body); } catch (err) { - const message = err instanceof OpenAiTranslationError ? err.message : String(err); + const message = err instanceof ResponsesTranslationError ? err.message : String(err); this._writeJsonError(res, 400, message, 'invalid_request_error'); return; } @@ -231,15 +231,20 @@ export class ByokLmProxyService extends LoopbackProxyServer im 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); + if (body.stream === true) { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }); + for (const frame of bridgeResultToResponsesSseFrames(result, bridgeRequest.modelId)) { + res.write(frame); + } + res.end(); + } else { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(bridgeResultToResponsesBody(result, bridgeRequest.modelId)); } - res.end(); } catch (err) { if (entry.ac.signal.aborted || res.writableEnded) { return; @@ -261,7 +266,7 @@ export class ByokLmProxyService extends LoopbackProxyServer im return; } res.writeHead(status, { 'Content-Type': 'application/json' }); - res.end(openAiErrorBody(message, type)); + res.end(responsesErrorBody(message, type)); } } diff --git a/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts b/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts deleted file mode 100644 index 2adb7944a8d..00000000000 --- a/src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts +++ /dev/null @@ -1,262 +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 { - 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 IOpenAiFunctionToolCall { - readonly id?: string; - readonly type?: 'function'; - readonly function?: { - readonly name?: string; - readonly arguments?: string; - }; -} - -interface IOpenAiCustomToolCall { - readonly id?: string; - readonly type: 'custom'; - readonly custom?: { - readonly name?: string; - readonly input?: string; - }; -} - -type IOpenAiToolCall = IOpenAiFunctionToolCall | IOpenAiCustomToolCall; - -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]; - if (call.type === 'custom') { - const name = call.custom?.name; - if (!name) { - throw new OpenAiTranslationError(`tool_calls[${i}].custom.name is required`); - } - mapped.push({ - id: call.id ?? `call_${i}`, - name, - argumentsJson: JSON.stringify({ input: call.custom?.input ?? '' }), - }); - continue; - } - - const name = call.function?.name; - if (!name) { - throw new OpenAiTranslationError(`tool_calls[${i}].function.name is required`); - } - mapped.push({ - id: call.id ?? `call_${i}`, - 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/node/copilot/byokResponsesTranslation.ts b/src/vs/platform/agentHost/node/copilot/byokResponsesTranslation.ts new file mode 100644 index 00000000000..5c28e59bec6 --- /dev/null +++ b/src/vs/platform/agentHost/node/copilot/byokResponsesTranslation.ts @@ -0,0 +1,489 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + IByokLmChatRequest, + IByokLmChatResult, + IByokLmInputItem, + IByokLmOutputItem, + IByokLmTool, +} from '../../common/agentHostByokLm.js'; + +interface IResponsesContentPart { + readonly type?: string; + readonly text?: string; +} + +interface IResponsesSummaryPart { + readonly type?: string; + readonly text?: string; +} + +interface IResponsesInputItem { + readonly type?: string; + readonly role?: string; + readonly content?: string | IResponsesContentPart[]; + readonly id?: string; + readonly summary?: IResponsesSummaryPart[]; + readonly encrypted_content?: string | null; + readonly call_id?: string; + readonly name?: string; + readonly arguments?: string; + readonly input?: string; + readonly output?: string; +} + +interface IResponsesTool { + readonly type?: string; + readonly name?: string; + readonly description?: string; + readonly parameters?: object; +} + +export interface IResponsesRequest { + readonly model?: string; + readonly instructions?: string; + readonly input?: string | IResponsesInputItem[]; + readonly tools?: IResponsesTool[]; + readonly previous_response_id?: string; + readonly reasoning?: { + readonly effort?: string; + }; + readonly temperature?: number; + readonly top_p?: number; + readonly max_output_tokens?: number; + readonly [key: string]: unknown; +} + +export class ResponsesTranslationError extends Error { } + +function toBridgeRole(role: string | undefined): 'system' | 'developer' | 'user' | 'assistant' { + switch (role) { + case 'system': + case 'developer': + case 'assistant': + case 'user': + return role; + default: + throw new ResponsesTranslationError(`Unsupported message role '${role ?? ''}'`); + } +} + +function toTextParts(content: string | IResponsesContentPart[] | undefined, itemIndex: number): Array<{ type: 'text'; text: string }> { + if (typeof content === 'string') { + return content ? [{ type: 'text', text: content }] : []; + } + if (!Array.isArray(content)) { + return []; + } + return content.map((part, contentIndex) => { + if ((part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') && typeof part.text === 'string') { + return { type: 'text' as const, text: part.text }; + } + throw new ResponsesTranslationError(`Unsupported input[${itemIndex}].content[${contentIndex}] type '${part.type ?? ''}'`); + }); +} + +function requiredString(value: string | undefined, path: string): string { + if (!value) { + throw new ResponsesTranslationError(`${path} is required`); + } + return value; +} + +function toBridgeInputItem(item: IResponsesInputItem, index: number): IByokLmInputItem { + switch (item.type) { + case 'message': + return { + type: 'message', + role: toBridgeRole(item.role), + content: toTextParts(item.content, index), + }; + case 'reasoning': + return { + type: 'reasoning', + id: item.id, + summary: (item.summary ?? []).map((part, summaryIndex) => { + if (part.type !== 'summary_text' || typeof part.text !== 'string') { + throw new ResponsesTranslationError(`Unsupported input[${index}].summary[${summaryIndex}]`); + } + return part.text; + }), + encryptedContent: item.encrypted_content ?? undefined, + }; + case 'function_call': + return { + type: 'function_call', + callId: requiredString(item.call_id, `input[${index}].call_id`), + name: requiredString(item.name, `input[${index}].name`), + argumentsJson: item.arguments ?? '{}', + }; + case 'function_call_output': + return { + type: 'function_call_output', + callId: requiredString(item.call_id, `input[${index}].call_id`), + output: item.output ?? '', + }; + case 'custom_tool_call': + return { + type: 'custom_tool_call', + callId: requiredString(item.call_id, `input[${index}].call_id`), + name: requiredString(item.name, `input[${index}].name`), + input: item.input ?? '', + }; + case 'custom_tool_call_output': + return { + type: 'custom_tool_call_output', + callId: requiredString(item.call_id, `input[${index}].call_id`), + output: item.output ?? '', + }; + default: + throw new ResponsesTranslationError(`Unsupported input[${index}] type '${item.type ?? ''}'`); + } +} + +function toBridgeTools(tools: IResponsesTool[] | undefined): IByokLmTool[] | undefined { + if (!tools?.length) { + return undefined; + } + return tools.map((tool, index) => { + switch (tool.type) { + case 'function': + return { + type: 'function', + name: requiredString(tool.name, `tools[${index}].name`), + description: tool.description, + parametersSchema: tool.parameters, + }; + case 'custom': + return { + type: 'custom', + name: requiredString(tool.name, `tools[${index}].name`), + description: tool.description, + }; + default: + throw new ResponsesTranslationError(`Unsupported tools[${index}] type '${tool.type ?? ''}'`); + } + }); +} + +export function responsesRequestToBridge(vendor: string, body: IResponsesRequest): IByokLmChatRequest { + const modelId = requiredString(body.model, 'model'); + let input: IByokLmInputItem[]; + if (typeof body.input === 'string') { + input = [{ type: 'message', role: 'user', content: [{ type: 'text', text: body.input }] }]; + } else if (Array.isArray(body.input)) { + input = body.input.map(toBridgeInputItem); + } else { + input = []; + } + + 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_output_tokens === 'number') { + modelOptions.max_tokens = body.max_output_tokens; + } + + return { + vendor, + modelId, + instructions: body.instructions, + input, + tools: toBridgeTools(body.tools), + previousResponseId: body.previous_response_id, + reasoningEffort: body.reasoning?.effort, + modelOptions: Object.keys(modelOptions).length ? modelOptions : undefined, + }; +} + +let responseCounter = 0; + +function nextId(prefix: string): string { + responseCounter = (responseCounter + 1) % Number.MAX_SAFE_INTEGER; + return `${prefix}_byok_${Date.now().toString(36)}_${responseCounter.toString(36)}`; +} + +function sseEvent(eventName: string, data: unknown): string { + return `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`; +} + +type ResponsesOutputItem = + | { readonly id: string; readonly type: 'message'; readonly role: 'assistant'; readonly status: 'completed'; readonly content: Array<{ readonly type: 'output_text'; readonly text: string; readonly annotations: unknown[]; readonly logprobs: unknown[] }> } + | { readonly id: string; readonly type: 'reasoning'; readonly status: 'completed'; readonly summary: Array<{ readonly type: 'summary_text'; readonly text: string }>; readonly encrypted_content: string | null } + | { readonly id: string; readonly type: 'function_call'; readonly status: 'completed'; readonly call_id: string; readonly name: string; readonly arguments: string } + | { readonly id: string; readonly type: 'custom_tool_call'; readonly status: 'completed'; readonly call_id: string; readonly name: string; readonly input: string }; + +function toInProgressOutputItem(item: ResponsesOutputItem): object { + switch (item.type) { + case 'message': + return { ...item, status: 'in_progress', content: [] }; + case 'reasoning': + return { ...item, status: 'in_progress', summary: [], encrypted_content: null }; + case 'function_call': + return { ...item, status: 'in_progress', arguments: '' }; + case 'custom_tool_call': + return { ...item, status: 'in_progress', input: '' }; + } +} + +function toResponsesOutputItem(item: IByokLmOutputItem): ResponsesOutputItem { + switch (item.type) { + case 'message': + return { + id: nextId('msg'), + type: 'message', + role: 'assistant', + status: 'completed', + content: item.content.map(part => ({ type: 'output_text', text: part.text, annotations: [], logprobs: [] })), + }; + case 'reasoning': + return { + id: item.id?.startsWith('rs') ? item.id : nextId('rs'), + type: 'reasoning', + status: 'completed', + summary: item.summary.map(text => ({ type: 'summary_text', text })), + encrypted_content: item.id?.startsWith('rs') ? item.encryptedContent ?? null : null, + }; + case 'function_call': + return { + id: nextId('fc'), + type: 'function_call', + status: 'completed', + call_id: item.callId, + name: item.name, + arguments: item.argumentsJson, + }; + case 'custom_tool_call': + return { + id: nextId('ctc'), + type: 'custom_tool_call', + status: 'completed', + call_id: item.callId, + name: item.name, + input: item.input, + }; + } +} + +function outputText(items: readonly ResponsesOutputItem[]): string { + return items + .filter((item): item is Extract => item.type === 'message') + .flatMap(item => item.content) + .map(part => part.text) + .join(''); +} + +function responseEnvelope(responseId: string, model: string, status: 'in_progress' | 'completed', output: readonly ResponsesOutputItem[], usage: unknown) { + return { + id: responseId, + object: 'response', + created_at: Math.floor(Date.now() / 1000), + status, + error: null, + incomplete_details: null, + instructions: null, + model, + output, + output_text: outputText(output), + parallel_tool_calls: true, + temperature: 1, + tool_choice: 'auto', + tools: [], + top_p: 1, + usage, + }; +} + +function prepareResponse(result: IByokLmChatResult, model: string) { + const responseId = result.responseId ?? nextId('resp'); + const output = result.output.map(toResponsesOutputItem); + const inputTokens = result.usage?.inputTokens ?? 0; + const outputTokens = result.usage?.outputTokens ?? 0; + const usage = { + input_tokens: inputTokens, + input_tokens_details: { cached_tokens: 0 }, + output_tokens: outputTokens, + output_tokens_details: { reasoning_tokens: result.usage?.reasoningTokens ?? 0 }, + total_tokens: inputTokens + outputTokens, + }; + return { + responseId, + output, + completed: responseEnvelope(responseId, model, 'completed', output, usage), + }; +} + +export function bridgeResultToResponsesBody(result: IByokLmChatResult, model: string): string { + return JSON.stringify(prepareResponse(result, model).completed); +} + +function reasoningFrames(item: Extract, outputIndex: number, sequence: { value: number }): string[] { + const frames: string[] = []; + item.summary.forEach((part, summaryIndex) => { + frames.push(sseEvent('response.reasoning_summary_part.added', { + type: 'response.reasoning_summary_part.added', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + summary_index: summaryIndex, + part: { type: 'summary_text', text: '' }, + })); + frames.push(sseEvent('response.reasoning_summary_text.delta', { + type: 'response.reasoning_summary_text.delta', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + summary_index: summaryIndex, + delta: part.text, + })); + frames.push(sseEvent('response.reasoning_summary_text.done', { + type: 'response.reasoning_summary_text.done', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + summary_index: summaryIndex, + text: part.text, + })); + frames.push(sseEvent('response.reasoning_summary_part.done', { + type: 'response.reasoning_summary_part.done', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + summary_index: summaryIndex, + part, + })); + }); + return frames; +} + +function messageFrames(item: Extract, outputIndex: number, sequence: { value: number }): string[] { + const frames: string[] = []; + item.content.forEach((part, contentIndex) => { + frames.push(sseEvent('response.content_part.added', { + type: 'response.content_part.added', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + content_index: contentIndex, + part: { type: 'output_text', text: '', annotations: [], logprobs: [] }, + })); + frames.push(sseEvent('response.output_text.delta', { + type: 'response.output_text.delta', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + content_index: contentIndex, + delta: part.text, + logprobs: [], + })); + frames.push(sseEvent('response.output_text.done', { + type: 'response.output_text.done', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + content_index: contentIndex, + text: part.text, + logprobs: [], + })); + frames.push(sseEvent('response.content_part.done', { + type: 'response.content_part.done', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + content_index: contentIndex, + part, + })); + }); + return frames; +} + +function callFrames(item: Extract, outputIndex: number, sequence: { value: number }): string[] { + if (item.type === 'function_call') { + return [ + sseEvent('response.function_call_arguments.delta', { + type: 'response.function_call_arguments.delta', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + delta: item.arguments, + }), + sseEvent('response.function_call_arguments.done', { + type: 'response.function_call_arguments.done', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + arguments: item.arguments, + }), + ]; + } + return [ + sseEvent('response.custom_tool_call_input.delta', { + type: 'response.custom_tool_call_input.delta', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + delta: item.input, + }), + sseEvent('response.custom_tool_call_input.done', { + type: 'response.custom_tool_call_input.done', + sequence_number: sequence.value++, + item_id: item.id, + output_index: outputIndex, + input: item.input, + }), + ]; +} + +export function bridgeResultToResponsesSseFrames(result: IByokLmChatResult, model: string): string[] { + const { responseId, output, completed } = prepareResponse(result, model); + const sequence = { value: 0 }; + const frames: string[] = []; + const skeleton = responseEnvelope(responseId, model, 'in_progress', [], undefined); + frames.push(sseEvent('response.created', { type: 'response.created', sequence_number: sequence.value++, response: skeleton })); + frames.push(sseEvent('response.in_progress', { type: 'response.in_progress', sequence_number: sequence.value++, response: skeleton })); + + output.forEach((item, outputIndex) => { + frames.push(sseEvent('response.output_item.added', { + type: 'response.output_item.added', + sequence_number: sequence.value++, + output_index: outputIndex, + item: toInProgressOutputItem(item), + })); + switch (item.type) { + case 'message': + frames.push(...messageFrames(item, outputIndex, sequence)); + break; + case 'reasoning': + frames.push(...reasoningFrames(item, outputIndex, sequence)); + break; + case 'function_call': + case 'custom_tool_call': + frames.push(...callFrames(item, outputIndex, sequence)); + break; + } + frames.push(sseEvent('response.output_item.done', { + type: 'response.output_item.done', + sequence_number: sequence.value++, + output_index: outputIndex, + item, + })); + }); + + frames.push(sseEvent('response.completed', { + type: 'response.completed', + sequence_number: sequence.value++, + response: completed, + })); + return frames; +} + +export function responsesErrorBody(message: string, type = 'api_error'): string { + return JSON.stringify({ error: { message, type } }); +} diff --git a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts index 442640397bf..ef7ab36fa3d 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts @@ -296,7 +296,7 @@ export function getCopilotContextTier(model: ModelSelection | undefined, longCon * 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 + * Each vendor maps to one `type: 'openai'` / `wireApi: 'responses'` 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 @@ -352,7 +352,7 @@ export async function resolveByokSessionConfig( const providers: NamedProviderConfig[] = [...new Set(byokModels.map(m => m.vendor))].map(vendor => ({ name: vendor, type: 'openai', - wireApi: 'completions', + wireApi: 'responses', baseUrl: handle.providerBaseUrl(vendor), bearerToken: `${handle.nonce}.${sessionId}`, })); diff --git a/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts b/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts index a941b5c90df..60d5aa846a3 100644 --- a/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostClientByokLmChannel.test.ts @@ -47,30 +47,52 @@ suite('agentHostClientByokLmChannel', () => { return createAgentHostClientByokLmConnection(channel); } - test('round-trips a chat request to the handler and back', async () => { + test('round-trips a Responses 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: '{}' }] }; + return { + responseId: 'resp_1', + output: [ + { type: 'reasoning', id: 'rs_1', summary: ['thinking'], encryptedContent: 'opaque' }, + { type: 'message', content: [{ type: 'text', text: 'pong' }] }, + { type: 'function_call', callId: 'c1', name: 'noop', argumentsJson: '{}' }, + ], + }; })); - const request: IByokLmChatRequest = { vendor: 'acme', modelId: 'm', messages: [{ role: 'user', content: 'ping' }] }; + const request: IByokLmChatRequest = { + vendor: 'acme', + modelId: 'm', + previousResponseId: 'resp_0', + input: [ + { type: 'reasoning', id: 'rs_0', summary: ['previous'], encryptedContent: 'previous-opaque' }, + { type: 'message', role: 'user', content: [{ type: 'text', text: 'ping' }] }, + ], + }; const result = await connection.chat(request); assert.deepStrictEqual(seen, request); - assert.deepStrictEqual(result, { content: 'pong', toolCalls: [{ id: 'c1', name: 'noop', argumentsJson: '{}' }] }); + assert.deepStrictEqual(result, { + responseId: 'resp_1', + output: [ + { type: 'reasoning', id: 'rs_1', summary: ['thinking'], encryptedContent: 'opaque' }, + { type: 'message', content: [{ type: 'text', text: 'pong' }] }, + { type: 'function_call', callId: '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: [] }); + const connection = bridge(handlerOf(async () => ({ output: [], error: 'no model' }))); + const result = await connection.chat({ vendor: 'v', modelId: 'm', input: [] }); assert.strictEqual(result.error, 'no model'); }); test('pushes the current model snapshot on subscribe and re-pushes on change', async () => { const onDidChange = store.add(new Emitter()); let models: IByokLmModelInfo[] = [{ vendor: 'acme', id: 'claude', name: 'Acme Claude', maxContextWindowTokens: 128000 }]; - const connection = bridge(handlerOf(async () => ({ content: '' }), async () => models, onDidChange.event)); + const connection = bridge(handlerOf(async () => ({ output: [] }), async () => models, onDidChange.event)); const pushed: IByokLmModelInfo[][] = []; const sub = connection.onDidChangeModels(snapshot => pushed.push(snapshot)); @@ -91,7 +113,7 @@ suite('agentHostClientByokLmChannel', () => { test('coalesces a burst of changes so the final snapshot reflects the latest models', async () => { const onDidChange = store.add(new Emitter()); let models: IByokLmModelInfo[] = [{ vendor: 'acme', id: 'v1' }]; - const connection = bridge(handlerOf(async () => ({ content: '' }), async () => models, onDidChange.event)); + const connection = bridge(handlerOf(async () => ({ output: [] }), async () => models, onDidChange.event)); const pushed: IByokLmModelInfo[][] = []; const sub = connection.onDidChangeModels(snapshot => pushed.push(snapshot)); @@ -111,12 +133,12 @@ suite('agentHostClientByokLmChannel', () => { }); test('rejects unknown channel commands', async () => { - const server = new AgentHostClientByokLmChannel(handlerOf(async () => ({ content: '' })), new NullLogService()); + const server = new AgentHostClientByokLmChannel(handlerOf(async () => ({ output: [] })), new NullLogService()); await assert.rejects(() => server.call(null, 'frobnicate'), /Unknown command/); }); test('exposes only the models event', () => { - const server = new AgentHostClientByokLmChannel(handlerOf(async () => ({ content: '' })), new NullLogService()); + const server = new AgentHostClientByokLmChannel(handlerOf(async () => ({ output: [] })), new NullLogService()); assert.throws(() => server.listen(null, 'anything'), /No event/); }); }); diff --git a/src/vs/platform/agentHost/test/node/byokLmBridgeRegistry.test.ts b/src/vs/platform/agentHost/test/node/byokLmBridgeRegistry.test.ts index 60b1edbc1b9..da7cec180fb 100644 --- a/src/vs/platform/agentHost/test/node/byokLmBridgeRegistry.test.ts +++ b/src/vs/platform/agentHost/test/node/byokLmBridgeRegistry.test.ts @@ -29,7 +29,7 @@ suite('ByokLmBridgeRegistry', () => { const emitter = store.add(new Emitter()); return { connection: { - chat: async (): Promise => ({ content: '' }), + chat: async (): Promise => ({ output: [] }), onDidChangeModels: emitter.event, }, push: models => emitter.fire(models), diff --git a/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts b/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts index a98902a349b..7f036f71355 100644 --- a/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts +++ b/src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts @@ -13,7 +13,7 @@ import { ByokLmProxyService, type IByokLmProxyHandle } from '../../node/copilot/ /** * Exercises the inference path end-to-end without the Copilot SDK runtime: - * the test plays the runtime's role by POSTing OpenAI Chat Completions + * the test plays the runtime's role by POSTing OpenAI Responses * 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, @@ -53,8 +53,8 @@ suite('ByokLmProxyService', () => { } } - function chatUrl(handle: IByokLmProxyHandle, vendor: string): string { - return `${handle.providerBaseUrl(vendor)}/chat/completions`; + function responsesUrl(handle: IByokLmProxyHandle, vendor: string): string { + return `${handle.providerBaseUrl(vendor)}/responses`; } function authHeaders(handle: IByokLmProxyHandle): Record { @@ -63,7 +63,7 @@ suite('ByokLmProxyService', () => { test('serves the unauthenticated health check', async () => { await withProxy( - async () => ({ content: 'unused' }), + async () => ({ output: [] }), async (handle) => { const response = await fetch(`${handle.baseUrl}/`); assert.strictEqual(response.status, 200); @@ -74,12 +74,12 @@ suite('ByokLmProxyService', () => { test('rejects requests without a valid bearer token', async () => { await withProxy( - async () => ({ content: 'unused' }), + async () => ({ output: [] }), async (handle) => { - const response = await fetch(chatUrl(handle, 'acme'), { + const response = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: 'm', messages: [] }), + body: JSON.stringify({ model: 'm', input: [] }), }); assert.strictEqual(response.status, 401); }, @@ -88,12 +88,12 @@ suite('ByokLmProxyService', () => { test('rejects a nonce-only bearer token (no session id)', async () => { await withProxy( - async () => ({ content: 'unused' }), + async () => ({ output: [] }), async (handle) => { - const response = await fetch(chatUrl(handle, 'acme'), { + const response = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${handle.nonce}` }, - body: JSON.stringify({ model: 'm', messages: [] }), + body: JSON.stringify({ model: 'm', input: [] }), }); assert.strictEqual(response.status, 401); }, @@ -102,9 +102,9 @@ suite('ByokLmProxyService', () => { test('returns 404 for an authenticated but unknown route', async () => { await withProxy( - async () => ({ content: 'unused' }), + async () => ({ output: [] }), async (handle) => { - const response = await fetch(`${handle.baseUrl}/v/acme/responses`, { + const response = await fetch(`${handle.baseUrl}/v/acme/chat/completions`, { method: 'POST', headers: authHeaders(handle), body: '{}', @@ -114,29 +114,28 @@ suite('ByokLmProxyService', () => { ); }); - test('forwards a chat request to the bridge and streams an SSE completion', async () => { + test('forwards a Responses request to the bridge and returns JSON by default', async () => { let captured: IByokLmChatRequest | undefined; await withProxy( async (request) => { captured = request; - return { content: 'hello from byok' }; + return { output: [{ type: 'message', content: [{ type: 'text', text: 'hello from byok' }] }] }; }, async (handle) => { - const response = await fetch(chatUrl(handle, 'acme'), { + const response = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: authHeaders(handle), - body: JSON.stringify({ model: 'claude', messages: [{ role: 'user', content: 'hi' }] }), + body: JSON.stringify({ model: 'claude', input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: '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(response.headers.get('content-type'), 'application/json'); + const body = await response.json() as { output: Array<{ content: Array<{ text: string }> }> }; + assert.strictEqual(body.output[0].content[0].text, 'hello from byok'); }, ); assert.strictEqual(captured?.vendor, 'acme'); assert.strictEqual(captured?.modelId, 'claude'); - assert.deepStrictEqual(captured?.messages, [{ role: 'user', content: 'hi', toolCalls: undefined, toolCallId: undefined }]); + assert.deepStrictEqual(captured?.input, [{ type: 'message', role: 'user', content: [{ type: 'text', text: 'hi' }] }]); }); test('forwards custom tool call history with freeform input', async () => { @@ -144,21 +143,22 @@ suite('ByokLmProxyService', () => { await withProxy( async (request) => { captured = request; - return { content: 'done' }; + return { output: [{ type: 'message', content: [{ type: 'text', text: 'done' }] }] }; }, async (handle) => { - const response = await fetch(chatUrl(handle, 'acme'), { + const response = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: authHeaders(handle), body: JSON.stringify({ model: 'm', - messages: [ + input: [ { - role: 'assistant', - content: '', - tool_calls: [{ id: 'call_1', type: 'custom', custom: { name: 'apply_patch', input: '*** Begin Patch\n*** End Patch' } }], + type: 'custom_tool_call', + call_id: 'call_1', + name: 'apply_patch', + input: '*** Begin Patch\n*** End Patch', }, - { role: 'tool', tool_call_id: 'call_1', content: 'Done!' }, + { type: 'custom_tool_call_output', call_id: 'call_1', output: 'Done!' }, ], }), }); @@ -166,26 +166,26 @@ suite('ByokLmProxyService', () => { await response.text(); }, ); - assert.deepStrictEqual(captured?.messages, [ + assert.deepStrictEqual(captured?.input, [ { - role: 'assistant', - content: '', - toolCalls: [{ id: 'call_1', name: 'apply_patch', argumentsJson: '{"input":"*** Begin Patch\\n*** End Patch"}' }], - toolCallId: undefined, + type: 'custom_tool_call', + callId: 'call_1', + name: 'apply_patch', + input: '*** Begin Patch\n*** End Patch', }, - { role: 'tool', content: 'Done!', toolCalls: undefined, toolCallId: 'call_1' }, + { type: 'custom_tool_call_output', callId: 'call_1', output: 'Done!' }, ]); }); test('decodes a url-encoded vendor path segment', async () => { let captured: IByokLmChatRequest | undefined; await withProxy( - async (request) => { captured = request; return { content: 'ok' }; }, + async (request) => { captured = request; return { output: [{ type: 'message', content: [{ type: 'text', text: 'ok' }] }] }; }, async (handle) => { - const response = await fetch(chatUrl(handle, 'acme corp'), { + const response = await fetch(responsesUrl(handle, 'acme corp'), { method: 'POST', headers: authHeaders(handle), - body: JSON.stringify({ model: 'm', messages: [] }), + body: JSON.stringify({ model: 'm', input: [] }), }); assert.strictEqual(response.status, 200); await response.text(); @@ -196,14 +196,14 @@ suite('ByokLmProxyService', () => { test('rejects a vendor that decodes to a multi-segment path (%2F)', async () => { await withProxy( - async () => ({ content: 'unused' }), + async () => ({ output: [] }), 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'), { + const response = await fetch(responsesUrl(handle, 'a/b'), { method: 'POST', headers: authHeaders(handle), - body: JSON.stringify({ model: 'm', messages: [] }), + body: JSON.stringify({ model: 'm', input: [] }), }); assert.strictEqual(response.status, 404); }, @@ -212,16 +212,16 @@ suite('ByokLmProxyService', () => { 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 () => ({ output: [{ type: 'function_call', callId: 'call_1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }] }), async (handle) => { - const response = await fetch(chatUrl(handle, 'acme'), { + const response = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: authHeaders(handle), - body: JSON.stringify({ model: 'm', messages: [{ role: 'user', content: 'weather?' }] }), + body: JSON.stringify({ model: 'm', input: 'weather?', stream: true }), }); 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('"type":"function_call"'), `expected function_call in SSE: ${text}`); + assert.ok(text.includes('event: response.completed'), `expected completed response: ${text}`); assert.ok(text.includes('getWeather')); }, ); @@ -229,12 +229,12 @@ suite('ByokLmProxyService', () => { test('returns a 502 when the bridge reports an error', async () => { await withProxy( - async () => ({ content: '', error: 'model unavailable' }), + async () => ({ output: [], error: 'model unavailable' }), async (handle) => { - const response = await fetch(chatUrl(handle, 'acme'), { + const response = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: authHeaders(handle), - body: JSON.stringify({ model: 'm', messages: [] }), + body: JSON.stringify({ model: 'm', input: [] }), }); assert.strictEqual(response.status, 502); const body = await response.json() as { error?: { message?: string } }; @@ -247,10 +247,10 @@ suite('ByokLmProxyService', () => { await withProxy( async () => { throw new Error('bridge exploded'); }, async (handle) => { - const response = await fetch(chatUrl(handle, 'acme'), { + const response = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: authHeaders(handle), - body: JSON.stringify({ model: 'm', messages: [] }), + body: JSON.stringify({ model: 'm', input: [] }), }); assert.strictEqual(response.status, 502); const body = await response.json() as { error?: { message?: string } }; @@ -261,9 +261,9 @@ suite('ByokLmProxyService', () => { test('rejects a malformed JSON body with 400', async () => { await withProxy( - async () => ({ content: 'unused' }), + async () => ({ output: [] }), async (handle) => { - const response = await fetch(chatUrl(handle, 'acme'), { + const response = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: authHeaders(handle), body: 'not json', @@ -278,10 +278,10 @@ suite('ByokLmProxyService', () => { const service = new ByokLmProxyService(new NullLogService(), registry); const handle = await service.start(); try { - const response = await fetch(chatUrl(handle, 'acme'), { + const response = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: authHeaders(handle), - body: JSON.stringify({ model: 'm', messages: [] }), + body: JSON.stringify({ model: 'm', input: [] }), }); assert.strictEqual(response.status, 503); } finally { @@ -295,21 +295,21 @@ suite('ByokLmProxyService', () => { const calls: string[] = []; // The serving window (editor): pushes models and answers chat. const regServing = registry.register('editor', servingConnection( - async () => { calls.push('serving'); return { content: 'from serving' }; }, + async () => { calls.push('serving'); return { output: [{ type: 'message', content: [{ type: 'text', text: 'from serving' }] }] }; }, [{ vendor: 'acme', id: 'claude' }], )); // A non-serving window (connected without a BYOK handler): it never pushes // a snapshot, so it must never be picked for routing even though connected. const regNonServing = registry.register('no-handler', { - chat: async () => { calls.push('no-handler'); return { content: 'from non-serving' }; }, + chat: async () => { calls.push('no-handler'); return { output: [{ type: 'message', content: [{ type: 'text', text: 'from non-serving' }] }] }; }, onDidChangeModels: Event.None, }); const service = new ByokLmProxyService(new NullLogService(), registry); const handle = await service.start(); try { - const res = await fetch(chatUrl(handle, 'acme'), { + const res = await fetch(responsesUrl(handle, 'acme'), { method: 'POST', headers: authHeaders(handle), - body: JSON.stringify({ model: 'claude', messages: [] }), + body: JSON.stringify({ model: 'claude', input: [] }), }); assert.deepStrictEqual({ routedToServing: (await res.text()).includes('from serving'), @@ -325,7 +325,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', servingConnection(async () => ({ content: 'ok' }))); + const registration = registry.register('client-1', servingConnection(async () => ({ output: [{ type: 'message', content: [{ type: 'text', text: 'ok' }] }] }))); const service = new ByokLmProxyService(new NullLogService(), registry); const first = await service.start(); const firstNonce = first.nonce; diff --git a/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts b/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts deleted file mode 100644 index 48ed0781ea1..00000000000 --- a/src/vs/platform/agentHost/test/node/byokOpenAiTranslation.test.ts +++ /dev/null @@ -1,150 +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 { 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('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('maps a custom assistant tool call with freeform input', () => { - const result = openAiRequestToBridge('acme', { - model: 'm', - messages: [ - { - role: 'assistant', - content: '', - tool_calls: [{ id: 'call_1', type: 'custom', custom: { name: 'apply_patch', input: '*** Begin Patch\n*** End Patch' } }], - }, - ], - }); - - assert.deepStrictEqual(result.messages, [{ - role: 'assistant', - content: '', - toolCalls: [{ id: 'call_1', name: 'apply_patch', argumentsJson: '{"input":"*** Begin Patch\\n*** End Patch"}' }], - toolCallId: undefined, - }]); - }); - - test('throws when a custom assistant tool call is missing its name', () => { - assert.throws(() => openAiRequestToBridge('acme', { - model: 'm', - messages: [{ role: 'assistant', content: '', tool_calls: [{ id: 'call_1', type: 'custom', custom: { input: 'patch' } }] }], - }), OpenAiTranslationError); - }); - - test('treats an omitted tool call type as a function call', () => { - const result = openAiRequestToBridge('acme', { - model: 'm', - messages: [{ role: 'assistant', content: '', tool_calls: [{ id: 'call_1', function: { name: 'getWeather', arguments: '{}' } }] }], - }); - - assert.deepStrictEqual(result.messages[0].toolCalls, [ - { id: 'call_1', name: 'getWeather', argumentsJson: '{}' }, - ]); - }); - - 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'); - }); - }); -}); diff --git a/src/vs/platform/agentHost/test/node/byokResponsesTranslation.test.ts b/src/vs/platform/agentHost/test/node/byokResponsesTranslation.test.ts new file mode 100644 index 00000000000..5f625d131ee --- /dev/null +++ b/src/vs/platform/agentHost/test/node/byokResponsesTranslation.test.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + bridgeResultToResponsesBody, + bridgeResultToResponsesSseFrames, + IResponsesRequest, + responsesRequestToBridge, + ResponsesTranslationError, +} from '../../node/copilot/byokResponsesTranslation.js'; + +suite('byokResponsesTranslation', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('maps ordered Responses input, tools, continuation, reasoning and options', () => { + const body: IResponsesRequest = { + model: 'gpt-5', + instructions: 'be helpful', + previous_response_id: 'resp_previous', + reasoning: { effort: 'high' }, + temperature: 0.5, + top_p: 0.9, + max_output_tokens: 256, + input: [ + { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'hello' }] }, + { type: 'reasoning', id: 'rs_1', summary: [{ type: 'summary_text', text: 'considered it' }], encrypted_content: 'encrypted' }, + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'checking' }] }, + { type: 'function_call', call_id: 'call_1', name: 'getWeather', arguments: '{"city":"NYC"}' }, + { type: 'function_call_output', call_id: 'call_1', output: 'sunny' }, + { type: 'custom_tool_call', call_id: 'call_2', name: 'apply_patch', input: '*** Begin Patch' }, + { type: 'custom_tool_call_output', call_id: 'call_2', output: 'Done!' }, + ], + tools: [ + { type: 'function', name: 'getWeather', description: 'weather', parameters: { type: 'object' } }, + { type: 'custom', name: 'apply_patch', description: 'patch files' }, + ], + }; + + assert.deepStrictEqual(responsesRequestToBridge('acme', body), { + vendor: 'acme', + modelId: 'gpt-5', + instructions: 'be helpful', + input: [ + { type: 'message', role: 'user', content: [{ type: 'text', text: 'hello' }] }, + { type: 'reasoning', id: 'rs_1', summary: ['considered it'], encryptedContent: 'encrypted' }, + { type: 'message', role: 'assistant', content: [{ type: 'text', text: 'checking' }] }, + { type: 'function_call', callId: 'call_1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }, + { type: 'function_call_output', callId: 'call_1', output: 'sunny' }, + { type: 'custom_tool_call', callId: 'call_2', name: 'apply_patch', input: '*** Begin Patch' }, + { type: 'custom_tool_call_output', callId: 'call_2', output: 'Done!' }, + ], + tools: [ + { type: 'function', name: 'getWeather', description: 'weather', parametersSchema: { type: 'object' } }, + { type: 'custom', name: 'apply_patch', description: 'patch files' }, + ], + previousResponseId: 'resp_previous', + reasoningEffort: 'high', + modelOptions: { temperature: 0.5, top_p: 0.9, max_tokens: 256 }, + }); + }); + + test('maps string input to a user message', () => { + assert.deepStrictEqual(responsesRequestToBridge('acme', { model: 'm', input: 'hello' }).input, [ + { type: 'message', role: 'user', content: [{ type: 'text', text: 'hello' }] }, + ]); + }); + + test('rejects missing models and unsupported input items', () => { + assert.throws(() => responsesRequestToBridge('acme', { input: [] }), ResponsesTranslationError); + assert.throws(() => responsesRequestToBridge('acme', { + model: 'm', + input: [{ type: 'computer_call' }], + }), /Unsupported input\[0\]/); + }); + + test('emits ordered Responses SSE for reasoning, text and tool calls', () => { + const result: IByokLmChatResult = { + responseId: 'resp_provider', + output: [ + { type: 'reasoning', id: 'rs_1', summary: ['first', 'second'], encryptedContent: 'encrypted' }, + { type: 'message', content: [{ type: 'text', text: 'hello' }] }, + { type: 'function_call', callId: 'call_1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }, + { type: 'custom_tool_call', callId: 'call_2', name: 'apply_patch', input: 'patch' }, + ], + usage: { inputTokens: 10, outputTokens: 5, reasoningTokens: 2 }, + }; + + const events = bridgeResultToResponsesSseFrames(result, 'gpt-5').map(frame => { + const lines = frame.trim().split('\n'); + return { + event: lines[0].slice('event: '.length), + data: JSON.parse(lines[1].slice('data: '.length)) as Record, + }; + }); + const completed = events.at(-1)?.data.response as { id: string; output: Array<{ type: string }>; usage: unknown }; + + assert.deepStrictEqual({ + eventTypes: events.map(event => event.event), + addedStatuses: events + .filter(event => event.event === 'response.output_item.added') + .map(event => (event.data.item as { status: string }).status), + responseId: completed.id, + outputTypes: completed.output.map(item => item.type), + usage: completed.usage, + }, { + eventTypes: [ + 'response.created', + 'response.in_progress', + 'response.output_item.added', + 'response.reasoning_summary_part.added', + 'response.reasoning_summary_text.delta', + 'response.reasoning_summary_text.done', + 'response.reasoning_summary_part.done', + 'response.reasoning_summary_part.added', + 'response.reasoning_summary_text.delta', + 'response.reasoning_summary_text.done', + 'response.reasoning_summary_part.done', + 'response.output_item.done', + 'response.output_item.added', + 'response.content_part.added', + 'response.output_text.delta', + 'response.output_text.done', + 'response.content_part.done', + 'response.output_item.done', + 'response.output_item.added', + 'response.function_call_arguments.delta', + 'response.function_call_arguments.done', + 'response.output_item.done', + 'response.output_item.added', + 'response.custom_tool_call_input.delta', + 'response.custom_tool_call_input.done', + 'response.output_item.done', + 'response.completed', + ], + addedStatuses: ['in_progress', 'in_progress', 'in_progress', 'in_progress'], + responseId: 'resp_provider', + outputTypes: ['reasoning', 'message', 'function_call', 'custom_tool_call'], + usage: { + input_tokens: 10, + input_tokens_details: { cached_tokens: 0 }, + output_tokens: 5, + output_tokens_details: { reasoning_tokens: 2 }, + total_tokens: 15, + }, + }); + }); + + test('encodes a completed non-streaming Responses body', () => { + const body = JSON.parse(bridgeResultToResponsesBody({ + responseId: 'resp_provider', + output: [ + { type: 'reasoning', id: 'rs_1', summary: ['thought'], encryptedContent: 'opaque' }, + { type: 'message', content: [{ type: 'text', text: 'answer' }] }, + ], + usage: { inputTokens: 3, outputTokens: 2, reasoningTokens: 1 }, + }, 'gpt-5')) as { + id: string; + created_at: number; + status: string; + output: Array<{ type: string }>; + output_text: string; + usage: unknown; + }; + + assert.deepStrictEqual(body, { + id: 'resp_provider', + object: 'response', + created_at: body['created_at'], + status: 'completed', + error: null, + incomplete_details: null, + instructions: null, + model: 'gpt-5', + output: body.output, + output_text: 'answer', + parallel_tool_calls: true, + temperature: 1, + tool_choice: 'auto', + tools: [], + top_p: 1, + usage: { + input_tokens: 3, + input_tokens_details: { cached_tokens: 0 }, + output_tokens: 2, + output_tokens_details: { reasoning_tokens: 1 }, + total_tokens: 5, + }, + }); + assert.deepStrictEqual(body.output.map(item => item.type), ['reasoning', 'message']); + }); +}); diff --git a/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts b/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts index 680f9952c48..4a77ace093e 100644 --- a/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts @@ -74,7 +74,7 @@ suite('resolveByokSessionConfig', () => { * A bridge connection that pushes `models` as its snapshot synchronously when * the registry subscribes; `chat` is scripted (unused by most tests). */ - function connectionOf(models: IByokLmModelInfo[], chat: IByokLmBridgeConnection['chat'] = async () => ({ content: '' })): IByokLmBridgeConnection { + function connectionOf(models: IByokLmModelInfo[], chat: IByokLmBridgeConnection['chat'] = async () => ({ output: [] })): IByokLmBridgeConnection { const emitter = store.add(new Emitter({ onDidAddFirstListener: () => emitter.fire(models), })); @@ -122,7 +122,7 @@ suite('resolveByokSessionConfig', () => { const registry = new ByokLmBridgeRegistry(); // A window connected without a BYOK handler never pushes, so it stays // non-serving and contributes no models. - const registration = registry.register('client-1', { chat: async (): Promise => ({ content: '' }), onDidChangeModels: Event.None }); + const registration = registry.register('client-1', { chat: async (): Promise => ({ output: [] }), onDidChangeModels: Event.None }); const proxy = countingProxy(); const config = await resolveByokSessionConfig(sessionId, registry, proxy.startProxy, log); @@ -147,8 +147,8 @@ suite('resolveByokSessionConfig', () => { 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' }, + { name: 'acme', type: 'openai', wireApi: 'responses', baseUrl: 'http://127.0.0.1:1/v/acme', bearerToken: 'NONCE.sess-1' }, + { name: 'globex', type: 'openai', wireApi: 'responses', baseUrl: 'http://127.0.0.1:1/v/globex', bearerToken: 'NONCE.sess-1' }, ], models: [ { id: 'claude', provider: 'acme', name: 'Acme Claude', maxContextWindowTokens: 200000 }, @@ -163,7 +163,10 @@ suite('resolveByokSessionConfig', () => { let captured: IByokLmChatRequest | undefined; const registration = registry.register('client-1', connectionOf( [{ vendor: 'acme', id: 'claude' }], - async (request) => { captured = request; return { content: 'hello from byok' }; }, + async (request) => { + captured = request; + return { output: [{ type: 'message', content: [{ type: 'text', text: 'hello from byok' }] }] }; + }, )); const service = new ByokLmProxyService(log, registry); let handle: IByokLmProxyHandle | undefined; @@ -172,10 +175,10 @@ suite('resolveByokSessionConfig', () => { const provider = config.providers![0]; const model = config.models![0]; try { - const response = await fetch(`${provider.baseUrl}/chat/completions`, { + const response = await fetch(`${provider.baseUrl}/responses`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${provider.bearerToken}` }, - body: JSON.stringify({ model: model.id, messages: [{ role: 'user', content: 'hi' }] }), + body: JSON.stringify({ model: model.id, input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'hi' }] }] }), }); assert.strictEqual(response.status, 200); const text = await response.text(); @@ -193,7 +196,7 @@ suite('resolveByokSessionConfig', () => { const registry = new ByokLmBridgeRegistry(); const emitter = store.add(new Emitter()); const registration = registry.register('client-1', { - chat: async (): Promise => ({ content: '' }), + chat: async (): Promise => ({ output: [] }), onDidChangeModels: emitter.event, }); const proxy = countingProxy(); @@ -231,7 +234,7 @@ suite('CopilotSessionLauncher BYOK proxy lifecycle', () => { const emitter = store.add(new Emitter({ onDidAddFirstListener: () => emitter.fire(models), })); - return { chat: async (): Promise => ({ content: '' }), onDidChangeModels: emitter.event }; + return { chat: async (): Promise => ({ output: [] }), onDidChangeModels: emitter.event }; } /** A fake proxy service whose handles carry a unique nonce per `start()`. */ diff --git a/src/vs/platform/agentHost/test/node/providerIntegration/copilotByokResponses.integrationTest.ts b/src/vs/platform/agentHost/test/node/providerIntegration/copilotByokResponses.integrationTest.ts new file mode 100644 index 00000000000..80e86f407d9 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/providerIntegration/copilotByokResponses.integrationTest.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { mkdtemp, rm } from 'fs/promises'; +import { tmpdir } from 'os'; +import { CopilotClient } from '@github/copilot-sdk'; +import { Emitter } from '../../../../../base/common/event.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../../log/common/log.js'; +import type { IByokLmChatRequest, IByokLmModelInfo } from '../../../common/agentHostByokLm.js'; +import { ByokLmBridgeRegistry } from '../../../node/byokLmBridgeRegistry.js'; +import { ByokLmProxyService } from '../../../node/copilot/byokLmProxyService.js'; + +suite('Agent Host Provider Integration - Copilot BYOK Responses', function () { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('real SDK consumes structured reasoning and text from the proxy', async function () { + this.timeout(120_000); + + const sessionId = 'byok-responses-integration'; + const baseDirectory = await mkdtemp(`${tmpdir()}/byok-responses-sdk-`); + const models = store.add(new Emitter()); + const registry = new ByokLmBridgeRegistry(); + const captured: IByokLmChatRequest[] = []; + const registration = registry.register('client', { + chat: async request => { + captured.push(request); + if (captured.length > 1) { + return { + responseId: 'resp_provider_2', + output: [{ type: 'message', content: [{ type: 'text', text: 'second' }] }], + }; + } + return { + responseId: 'resp_provider', + output: [ + { type: 'reasoning', id: 'rs_provider', summary: ['considered options'], encryptedContent: 'opaque' }, + { type: 'message', content: [{ type: 'text', text: 'hello' }] }, + ], + usage: { inputTokens: 1, outputTokens: 2, reasoningTokens: 1 }, + }; + }, + onDidChangeModels: models.event, + }); + models.fire([{ vendor: 'acme', id: 'test-model' }]); + + const proxy = new ByokLmProxyService(new NullLogService(), registry); + const handle = await proxy.start(); + const client = new CopilotClient({ + mode: 'empty', + baseDirectory, + useLoggedInUser: false, + logLevel: 'error', + }); + let session: Awaited> | undefined; + let clientStarted = false; + + try { + await client.start(); + clientStarted = true; + session = await client.createSession({ + sessionId, + model: 'test-model', + availableTools: [], + provider: { + type: 'openai', + wireApi: 'responses', + baseUrl: handle.providerBaseUrl('acme'), + bearerToken: `${handle.nonce}.${sessionId}`, + }, + }); + const reasoning: string[] = []; + session.on('assistant.reasoning', event => reasoning.push(event.data.content)); + + const result = await session.sendAndWait({ prompt: 'Reply exactly hello.' }, 30_000); + const secondResult = await session.sendAndWait({ prompt: 'Reply exactly second.' }, 30_000); + const replayedReasoning = captured[1]?.input.find(item => item.type === 'reasoning'); + + assert.deepStrictEqual({ + result: result?.type === 'assistant.message' ? result.data.content : undefined, + secondResult: secondResult?.type === 'assistant.message' ? secondResult.data.content : undefined, + reasoning, + firstRequest: { + vendor: captured[0]?.vendor, + modelId: captured[0]?.modelId, + inputTypes: captured[0]?.input.map(item => item.type), + reasoningEffort: captured[0]?.reasoningEffort, + }, + replayedReasoning, + }, { + result: 'hello', + secondResult: 'second', + reasoning: ['considered options'], + firstRequest: { + vendor: 'acme', + modelId: 'test-model', + inputTypes: ['message'], + reasoningEffort: 'medium', + }, + replayedReasoning: { + type: 'reasoning', + id: 'rs_provider', + summary: ['considered options'], + encryptedContent: 'opaque', + }, + }); + + } finally { + try { + await session?.disconnect(); + } finally { + try { + if (clientStarted) { + await client.stop(); + } + } finally { + handle.dispose(); + registration.dispose(); + proxy.dispose(); + await rm(baseDirectory, { recursive: true, force: true }); + } + } + } + }); +}); 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 bd2a7b9defe..d1b8d6cd892 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -6,13 +6,15 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; import { IAgentHostByokLmHandler, - IByokLmChatMessage, IByokLmChatRequest, IByokLmChatResult, + IByokLmInputItem, IByokLmModelInfo, - IByokLmToolCall, + IByokLmOutputItem, + IByokLmReasoningItem, } from '../../../../../../platform/agentHost/common/agentHostByokLm.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { @@ -23,6 +25,9 @@ import { ILanguageModelsService, } from '../../../common/languageModels.js'; +const STATEFUL_MARKER_MIME_TYPE = 'stateful_marker'; +const USAGE_MIME_TYPE = 'usage'; + /** * Renderer-side {@link IAgentHostByokLmHandler}. Services BYOK chat requests * forwarded by the node agent host's OpenAI proxy by calling the VS Code LM @@ -55,50 +60,71 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok 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}` }; + return { output: [], error: `No BYOK model found for ${request.vendor}/${request.modelId}` }; } - const messages = request.messages.map(message => this._toChatMessage(message)); + const messages = this._toChatMessages(request); const tools = request.tools?.length ? request.tools.map(tool => ({ name: tool.name, description: tool.description ?? '', - inputSchema: tool.parametersSchema, + inputSchema: tool.type === 'function' + ? tool.parametersSchema + : { type: 'object', properties: { input: { type: 'string' } }, required: ['input'] }, })) : undefined; const options: ILanguageModelChatRequestOptions = { modelOptions: request.modelOptions, + ...(request.reasoningEffort ? { configuration: { reasoningEffort: request.reasoningEffort } } : {}), ...(tools ? { tools } : {}), }; try { const response = await this._languageModelsService.sendChatRequest(modelIdentifier, undefined, messages, options, token); - let content = ''; - const toolCalls: IByokLmToolCall[] = []; + const output: IByokLmOutputItem[] = []; + const customToolNames = new Set(request.tools?.filter(tool => tool.type === 'custom').map(tool => tool.name)); + let responseId: string | undefined; + let usage: IByokLmChatResult['usage']; 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; + this._appendTextOutput(output, p.value); + } else if (p.type === 'thinking') { + this._appendReasoningOutput(output, p); } else if (p.type === 'tool_use') { - toolCalls.push({ - id: p.toolCallId, - name: p.name, - argumentsJson: JSON.stringify(p.parameters ?? {}), - }); + if (customToolNames.has(p.name)) { + output.push({ + type: 'custom_tool_call', + callId: p.toolCallId, + name: p.name, + input: this._customToolInput(p.parameters), + }); + } else { + output.push({ + type: 'function_call', + callId: p.toolCallId, + name: p.name, + argumentsJson: JSON.stringify(p.parameters ?? {}), + }); + } + } else if (p.type === 'data' && p.mimeType === STATEFUL_MARKER_MIME_TYPE) { + responseId = this._decodeStatefulMarker(p.data, request.modelId); + } else if (p.type === 'data' && p.mimeType === USAGE_MIME_TYPE) { + usage = this._decodeUsage(p.data); } } } })(); await Promise.all([response.result, streaming]); - return { content, toolCalls: toolCalls.length ? toolCalls : undefined }; + return { output, responseId, usage }; } 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 }; + return { output: [], error: message }; } } @@ -136,45 +162,184 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok return undefined; } - 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 }] }], - }; + private _toChatMessages(request: IByokLmChatRequest): IChatMessage[] { + const messages: IChatMessage[] = []; + if (request.previousResponseId) { + messages.push({ + role: ChatMessageRole.Assistant, + content: [{ + type: 'data', + mimeType: STATEFUL_MARKER_MIME_TYPE, + data: VSBuffer.fromString(`${request.modelId}\\${request.previousResponseId}`), + }], + }); } - - const content: IChatMessagePart[] = []; - if (message.content) { - content.push({ type: 'text', value: message.content }); + if (request.instructions) { + messages.push({ + role: ChatMessageRole.System, + content: [{ type: 'text', value: request.instructions }], + }); } - - 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), - }); + for (const item of request.input) { + const message = this._toChatMessage(item); + const previous = messages.at(-1); + if (message.role === ChatMessageRole.Assistant && previous?.role === ChatMessageRole.Assistant) { + messages[messages.length - 1] = { + ...previous, + content: [...previous.content, ...message.content], + }; + } else { + messages.push(message); } } - - return { role: this._toChatRole(message.role), content }; + return messages; } - private _toChatRole(role: IByokLmChatMessage['role']): ChatMessageRole { + private _toChatMessage(item: IByokLmInputItem): IChatMessage { + switch (item.type) { + case 'message': + return { + role: this._toChatRole(item.role), + content: item.content.map(part => ({ type: 'text', value: part.text })), + }; + case 'reasoning': + return { + role: ChatMessageRole.Assistant, + content: [{ + type: 'thinking', + value: item.summary, + id: item.id, + metadata: { + ...item.metadata, + ...(item.encryptedContent ? { encrypted_content: item.encryptedContent } : {}), + }, + }], + }; + case 'function_call': + return { + role: ChatMessageRole.Assistant, + content: [{ + type: 'tool_use', + name: item.name, + toolCallId: item.callId, + parameters: this._safeParseJson(item.argumentsJson), + }], + }; + case 'custom_tool_call': + return { + role: ChatMessageRole.Assistant, + content: [{ + type: 'tool_use', + name: item.name, + toolCallId: item.callId, + parameters: { input: item.input }, + }], + }; + case 'function_call_output': + case 'custom_tool_call_output': + return { + role: ChatMessageRole.User, + content: [{ + type: 'tool_result', + toolCallId: item.callId, + value: [{ type: 'text', value: item.output }], + }], + }; + } + } + + private _appendTextOutput(output: IByokLmOutputItem[], value: string): void { + const previous = output.at(-1); + if (previous?.type === 'message') { + output[output.length - 1] = { + ...previous, + content: [...previous.content, { type: 'text', text: value }], + }; + } else { + output.push({ type: 'message', content: [{ type: 'text', text: value }] }); + } + } + + private _appendReasoningOutput(output: IByokLmOutputItem[], part: Extract): void { + if (part.metadata?.vscode_reasoning_done === true) { + return; + } + const summary = Array.isArray(part.value) ? part.value : [part.value]; + const encryptedContent = this._stringMetadata(part.metadata, 'encrypted_content') ?? this._stringMetadata(part.metadata, 'encrypted'); + const reasoning: IByokLmReasoningItem = { + type: 'reasoning', + id: part.id, + summary, + encryptedContent, + metadata: part.metadata, + }; + const previous = output.at(-1); + if (previous?.type === 'reasoning' && previous.id === reasoning.id) { + output[output.length - 1] = { + ...previous, + summary: [...previous.summary, ...reasoning.summary], + encryptedContent: reasoning.encryptedContent ?? previous.encryptedContent, + metadata: { ...previous.metadata, ...reasoning.metadata }, + }; + } else { + output.push(reasoning); + } + } + + private _customToolInput(parameters: unknown): string { + if (typeof parameters === 'object' && parameters !== null) { + const input = Object.getOwnPropertyDescriptor(parameters, 'input')?.value; + if (typeof input === 'string') { + return input; + } + } + return typeof parameters === 'string' ? parameters : JSON.stringify(parameters ?? {}); + } + + private _decodeStatefulMarker(data: VSBuffer, expectedModelId: string): string | undefined { + const decoded = data.toString(); + const separator = decoded.indexOf('\\'); + if (separator === -1 || decoded.slice(0, separator) !== expectedModelId) { + return undefined; + } + return decoded.slice(separator + 1) || undefined; + } + + private _decodeUsage(data: VSBuffer): IByokLmChatResult['usage'] { + try { + const value = JSON.parse(data.toString()) as Record; + const outputDetails = typeof value.completion_tokens_details === 'object' && value.completion_tokens_details !== null + ? value.completion_tokens_details as Record + : undefined; + return { + inputTokens: this._numberProperty(value, 'prompt_tokens'), + outputTokens: this._numberProperty(value, 'completion_tokens'), + reasoningTokens: outputDetails ? this._numberProperty(outputDetails, 'reasoning_tokens') : undefined, + }; + } catch { + return undefined; + } + } + + private _numberProperty(value: Record, key: string): number | undefined { + const property = value[key]; + return typeof property === 'number' ? property : undefined; + } + + private _stringMetadata(metadata: Readonly> | undefined, key: string): string | undefined { + const value = metadata?.[key]; + return typeof value === 'string' ? value : undefined; + } + + private _toChatRole(role: Extract['role']): ChatMessageRole { switch (role) { - case 'system': return ChatMessageRole.System; - case 'assistant': return ChatMessageRole.Assistant; + case 'system': + case 'developer': + return ChatMessageRole.System; + case 'assistant': + return ChatMessageRole.Assistant; case 'user': - case 'tool': - default: return ChatMessageRole.User; + return ChatMessageRole.User; } } 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 5952e1b4666..50417e1659b 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 @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Event } from '../../../../../../base/common/event.js'; import { mock } from '../../../../../../base/test/common/mock.js'; @@ -128,30 +129,49 @@ suite('AgentHostByokLmHandler', () => { ]); }); - test('resolves the BYOK model and buffers text + tool calls', async () => { + test('buffers ordered thinking, text, tool calls, continuation and usage', async () => { const service = new TestLanguageModelsService( new Map([['id-acme-claude', byokModel('acme', 'claude')]]), () => responseOf([ + { type: 'thinking', value: 'considered ', id: 'rs_1' }, + { type: 'thinking', value: ['options'], id: 'rs_1', metadata: { encrypted_content: 'opaque' } }, { type: 'text', value: 'hello ' }, { type: 'text', value: 'world' }, { type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }, + { type: 'tool_use', name: 'apply_patch', toolCallId: 't2', parameters: { input: 'patch' } }, + { type: 'data', mimeType: 'stateful_marker', data: VSBuffer.fromString('claude\\resp_provider') }, + { type: 'data', mimeType: 'usage', data: VSBuffer.fromString('{"prompt_tokens":10,"completion_tokens":5,"completion_tokens_details":{"reasoning_tokens":2}}') }, ]), ); const handler = createHandler(service); const result = await handler.chat( - { vendor: 'acme', modelId: 'claude', messages: [{ role: 'user', content: 'hi' }] }, + { + vendor: 'acme', + modelId: 'claude', + input: [{ type: 'message', role: 'user', content: [{ type: 'text', text: 'hi' }] }], + tools: [ + { type: 'function', name: 'getWeather' }, + { type: 'custom', name: 'apply_patch' }, + ], + }, CancellationToken.None, ); assert.strictEqual(service.captured?.modelId, 'id-acme-claude'); assert.deepStrictEqual(result, { - content: 'hello world', - toolCalls: [{ id: 't1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }], + output: [ + { type: 'reasoning', id: 'rs_1', summary: ['considered ', 'options'], encryptedContent: 'opaque', metadata: { encrypted_content: 'opaque' } }, + { type: 'message', content: [{ type: 'text', text: 'hello ' }, { type: 'text', text: 'world' }] }, + { type: 'function_call', callId: 't1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }, + { type: 'custom_tool_call', callId: 't2', name: 'apply_patch', input: 'patch' }, + ], + responseId: 'resp_provider', + usage: { inputTokens: 10, outputTokens: 5, reasoningTokens: 2 }, }); }); - test('maps bridge messages to LM API chat messages', async () => { + test('maps ordered Responses input and options to LM API chat messages', async () => { const service = new TestLanguageModelsService( new Map([['id', byokModel('acme', 'claude')]]), () => responseOf([{ type: 'text', value: 'ok' }]), @@ -162,41 +182,60 @@ suite('AgentHostByokLmHandler', () => { { 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' }, + instructions: 'be helpful', + previousResponseId: 'resp_previous', + reasoningEffort: 'high', + modelOptions: { temperature: 0.5 }, + tools: [ + { type: 'function', name: 'getWeather', parametersSchema: { type: 'object' } }, + { type: 'custom', name: 'apply_patch' }, + ], + input: [ + { type: 'reasoning', id: 'rs_1', summary: ['thought'], encryptedContent: 'opaque' }, + { type: 'message', role: 'assistant', content: [{ type: 'text', text: 'checking' }] }, + { type: 'function_call', callId: 't1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }, + { type: 'custom_tool_call', callId: 't2', name: 'apply_patch', input: 'patch' }, + { type: 'function_call_output', callId: 't1', output: 'sunny' }, + { type: 'custom_tool_call_output', callId: 't2', output: 'Done!' }, + { type: 'message', role: 'user', content: [{ type: 'text', text: 'hi' }] }, ], }, 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 (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' }] }, - ]); + const messages = service.captured?.messages.map(message => ({ + role: message.role, + content: message.content.map(part => part.type === 'data' ? { ...part, data: part.data.toString() } : part), + })); + assert.deepStrictEqual({ + messages, + options: service.captured?.options, + }, { + messages: [ + { role: ChatMessageRole.Assistant, content: [{ type: 'data', mimeType: 'stateful_marker', data: 'claude\\resp_previous' }] }, + { role: ChatMessageRole.System, content: [{ type: 'text', value: 'be helpful' }] }, + { + role: ChatMessageRole.Assistant, + content: [ + { type: 'thinking', value: ['thought'], id: 'rs_1', metadata: { encrypted_content: 'opaque' } }, + { type: 'text', value: 'checking' }, + { type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }, + { type: 'tool_use', name: 'apply_patch', toolCallId: 't2', parameters: { input: 'patch' } }, + ], + }, + { role: ChatMessageRole.User, content: [{ type: 'tool_result', toolCallId: 't1', value: [{ type: 'text', value: 'sunny' }] }] }, + { role: ChatMessageRole.User, content: [{ type: 'tool_result', toolCallId: 't2', value: [{ type: 'text', value: 'Done!' }] }] }, + { role: ChatMessageRole.User, content: [{ type: 'text', value: 'hi' }] }, + ], + options: { + modelOptions: { temperature: 0.5 }, + configuration: { reasoningEffort: 'high' }, + tools: [ + { name: 'getWeather', description: '', inputSchema: { type: 'object' } }, + { name: 'apply_patch', description: '', inputSchema: { type: 'object', properties: { input: { type: 'string' } }, required: ['input'] } }, + ], + }, + }); }); test('returns an error result when no BYOK model matches', async () => { @@ -204,11 +243,11 @@ suite('AgentHostByokLmHandler', () => { const handler = createHandler(service); const result = await handler.chat( - { vendor: 'acme', modelId: 'missing', messages: [] } satisfies IByokLmChatRequest, + { vendor: 'acme', modelId: 'missing', input: [] } satisfies IByokLmChatRequest, CancellationToken.None, ); - assert.strictEqual(result.content, ''); + assert.deepStrictEqual(result.output, []); assert.ok(result.error?.includes('acme/missing'), `expected error to name the model: ${result.error}`); }); @@ -220,10 +259,10 @@ suite('AgentHostByokLmHandler', () => { const handler = createHandler(service); const result = await handler.chat( - { vendor: 'acme', modelId: 'claude', messages: [{ role: 'user', content: 'hi' }] }, + { vendor: 'acme', modelId: 'claude', input: [{ type: 'message', role: 'user', content: [{ type: 'text', text: 'hi' }] }] }, CancellationToken.None, ); - assert.deepStrictEqual(result, { content: '', error: 'provider exploded' }); + assert.deepStrictEqual(result, { output: [], error: 'provider exploded' }); }); }); From bd2cb6d3ed90c13ae29b960329e3102506689fa5 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Wed, 29 Jul 2026 18:07:34 -0700 Subject: [PATCH 02/86] agentHost: gate BYOK encrypted reasoning bridge Carry encrypted reasoning through a private data part only for Agent Host BYOK requests, preserving existing vscode.lm behavior for other consumers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../vscode-node/languageModelAccess.ts | 15 ++-- .../vscode-node/languageModelAccessPrompt.tsx | 26 +++++-- .../agentHost/agentHostByokLmHandler.ts | 69 +++++++++++++------ .../agentHostByokLmHandler.test.ts | 16 +++-- 4 files changed, 86 insertions(+), 40 deletions(-) diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts index 4160085627a..d4a31944600 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -837,15 +837,20 @@ export class CopilotLanguageModelWrapper extends Disposable { } async provideLanguageModelResponse(endpoint: IChatEndpoint, messages: Array, options: vscode.ProvideLanguageModelChatResponseOptions, extensionId: string | undefined, progress: vscode.Progress, token: vscode.CancellationToken): Promise { + const preserveAgentHostByokReasoning = options.modelOptions?._vscodeAgentHostByokReasoningBridge === true; let thinkingActive = false; const finishCallback: FinishedCallback = async (_text, index, delta): Promise => { if (delta.thinking) { if (isEncryptedThinkingDelta(delta.thinking)) { - progress.report(new vscode.LanguageModelThinkingPart( - delta.thinking.text ?? '', - delta.thinking.id, - { encrypted_content: delta.thinking.encrypted } - )); + if (preserveAgentHostByokReasoning) { + progress.report(new vscode.LanguageModelDataPart( + new TextEncoder().encode(JSON.stringify({ + id: delta.thinking.id, + encryptedContent: delta.thinking.encrypted, + })), + 'application/vnd.code.agent-host-byok-reasoning+json' + )); + } } else { const text = delta.thinking.text ?? ''; progress.report(new vscode.LanguageModelThinkingPart(text, delta.thinking.id, delta.thinking.metadata)); diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx index e62f6a0e39b..90813e90921 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx @@ -15,6 +15,16 @@ import { EditorIntegrationRules } from '../../prompts/node/panel/editorIntegrati import { imageDataPartToTSX, ToolResult } from '../../prompts/node/panel/toolCalling'; import { isImageDataPart } from '../common/languageModelChatMessageHelpers'; +const AGENT_HOST_BYOK_REASONING_MIME_TYPE = 'application/vnd.code.agent-host-byok-reasoning+json'; + +function decodeAgentHostByokReasoning(data: Uint8Array): { id?: string; encryptedContent: string } { + const value = JSON.parse(new TextDecoder().decode(data)) as { id?: unknown; encryptedContent?: unknown }; + if ((value.id !== undefined && typeof value.id !== 'string') || typeof value.encryptedContent !== 'string') { + throw new Error('Invalid Agent Host BYOK reasoning data'); + } + return { id: value.id, encryptedContent: value.encryptedContent }; +} + export type Props = PromptElementProps<{ noSafety: boolean; messages: Array; @@ -37,18 +47,22 @@ export class LanguageModelAccessPrompt extends PromptElement { } else if (message.role === vscode.LanguageModelChatMessageRole.Assistant) { const statefulMarkerPart = message.content.find(part => part instanceof vscode.LanguageModelDataPart && part.mimeType === CustomDataPartMimeTypes.StatefulMarker) as vscode.LanguageModelDataPart | undefined; const statefulMarker = statefulMarkerPart && decodeStatefulMarker(statefulMarkerPart.data); + const reasoningDataPart = message.content.find(part => part instanceof vscode.LanguageModelDataPart && part.mimeType === AGENT_HOST_BYOK_REASONING_MIME_TYPE) as vscode.LanguageModelDataPart | undefined; + const reasoningData = reasoningDataPart && decodeAgentHostByokReasoning(reasoningDataPart.data); const filteredContent = message.content.filter(part => !(part instanceof vscode.LanguageModelDataPart)); // There should only be one string part per message const content = filteredContent.find(part => part instanceof LanguageModelTextPart); const toolCalls = filteredContent.filter(part => part instanceof vscode.LanguageModelToolCallPart); - const thinkingParts = filteredContent.filter(part => part instanceof vscode.LanguageModelThinkingPart); - const thinking = thinkingParts.find(part => typeof part.metadata?.encrypted_content === 'string') ?? thinkingParts.at(-1); - const thinkingText = thinkingParts.flatMap(part => Array.isArray(part.value) ? part.value : [part.value]); - const thinkingMetadata = Object.assign({}, ...thinkingParts.map(part => part.metadata)); + const thinking = filteredContent.find(part => part instanceof vscode.LanguageModelThinkingPart); const statefulMarkerElement = statefulMarker && ; - const encrypted = typeof thinkingMetadata.encrypted_content === 'string' ? thinkingMetadata.encrypted_content : undefined; - const thinkingElement = thinking && thinking.id && ; + const thinkingId = reasoningData?.id ?? thinking?.id; + const thinkingElement = thinkingId && ; chatMessages.push( ({ id: tc.callId, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.input) } }))}>{statefulMarkerElement}{content?.value}{thinkingElement}); } else if (message.role === vscode.LanguageModelChatMessageRole.User) { for (const part of message.content) { 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 d1b8d6cd892..99faae000b3 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -27,6 +27,8 @@ import { const STATEFUL_MARKER_MIME_TYPE = 'stateful_marker'; const USAGE_MIME_TYPE = 'usage'; +const AGENT_HOST_BYOK_REASONING_MIME_TYPE = 'application/vnd.code.agent-host-byok-reasoning+json'; +const AGENT_HOST_BYOK_REASONING_MODEL_OPTION = '_vscodeAgentHostByokReasoningBridge'; /** * Renderer-side {@link IAgentHostByokLmHandler}. Services BYOK chat requests @@ -74,7 +76,10 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok })) : undefined; const options: ILanguageModelChatRequestOptions = { - modelOptions: request.modelOptions, + modelOptions: { + ...request.modelOptions, + [AGENT_HOST_BYOK_REASONING_MODEL_OPTION]: true, + }, ...(request.reasoningEffort ? { configuration: { reasoningEffort: request.reasoningEffort } } : {}), ...(tools ? { tools } : {}), }; @@ -112,6 +117,8 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok } } else if (p.type === 'data' && p.mimeType === STATEFUL_MARKER_MIME_TYPE) { responseId = this._decodeStatefulMarker(p.data, request.modelId); + } else if (p.type === 'data' && p.mimeType === AGENT_HOST_BYOK_REASONING_MIME_TYPE) { + this._appendEncryptedReasoningOutput(output, p.data); } else if (p.type === 'data' && p.mimeType === USAGE_MIME_TYPE) { usage = this._decodeUsage(p.data); } @@ -181,16 +188,7 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok }); } for (const item of request.input) { - const message = this._toChatMessage(item); - const previous = messages.at(-1); - if (message.role === ChatMessageRole.Assistant && previous?.role === ChatMessageRole.Assistant) { - messages[messages.length - 1] = { - ...previous, - content: [...previous.content, ...message.content], - }; - } else { - messages.push(message); - } + messages.push(this._toChatMessage(item)); } return messages; } @@ -202,19 +200,28 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok role: this._toChatRole(item.role), content: item.content.map(part => ({ type: 'text', value: part.text })), }; - case 'reasoning': + case 'reasoning': { + const content: IChatMessagePart[] = [{ + type: 'thinking', + value: item.summary, + id: item.id, + metadata: item.metadata, + }]; + if (item.encryptedContent) { + content.push({ + type: 'data', + mimeType: AGENT_HOST_BYOK_REASONING_MIME_TYPE, + data: VSBuffer.fromString(JSON.stringify({ + id: item.id, + encryptedContent: item.encryptedContent, + })), + }); + } return { role: ChatMessageRole.Assistant, - content: [{ - type: 'thinking', - value: item.summary, - id: item.id, - metadata: { - ...item.metadata, - ...(item.encryptedContent ? { encrypted_content: item.encryptedContent } : {}), - }, - }], + content, }; + } case 'function_call': return { role: ChatMessageRole.Assistant, @@ -279,13 +286,31 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok ...previous, summary: [...previous.summary, ...reasoning.summary], encryptedContent: reasoning.encryptedContent ?? previous.encryptedContent, - metadata: { ...previous.metadata, ...reasoning.metadata }, + metadata: previous.metadata || reasoning.metadata ? { ...previous.metadata, ...reasoning.metadata } : undefined, }; } else { output.push(reasoning); } } + private _appendEncryptedReasoningOutput(output: IByokLmOutputItem[], data: VSBuffer): void { + const value = JSON.parse(data.toString()) as { id?: unknown; encryptedContent?: unknown }; + if ((value.id !== undefined && typeof value.id !== 'string') || typeof value.encryptedContent !== 'string') { + throw new Error('Invalid Agent Host BYOK reasoning data'); + } + const previous = output.at(-1); + if (previous?.type === 'reasoning' && previous.id === value.id) { + output[output.length - 1] = { ...previous, encryptedContent: value.encryptedContent }; + } else { + output.push({ + type: 'reasoning', + id: value.id, + summary: [], + encryptedContent: value.encryptedContent, + }); + } + } + private _customToolInput(parameters: unknown): string { if (typeof parameters === 'object' && parameters !== null) { const input = Object.getOwnPropertyDescriptor(parameters, 'input')?.value; 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 50417e1659b..3a54e5347f9 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 @@ -134,7 +134,8 @@ suite('AgentHostByokLmHandler', () => { new Map([['id-acme-claude', byokModel('acme', 'claude')]]), () => responseOf([ { type: 'thinking', value: 'considered ', id: 'rs_1' }, - { type: 'thinking', value: ['options'], id: 'rs_1', metadata: { encrypted_content: 'opaque' } }, + { type: 'thinking', value: ['options'], id: 'rs_1' }, + { type: 'data', mimeType: 'application/vnd.code.agent-host-byok-reasoning+json', data: VSBuffer.fromString('{"id":"rs_1","encryptedContent":"opaque"}') }, { type: 'text', value: 'hello ' }, { type: 'text', value: 'world' }, { type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }, @@ -161,7 +162,7 @@ suite('AgentHostByokLmHandler', () => { assert.strictEqual(service.captured?.modelId, 'id-acme-claude'); assert.deepStrictEqual(result, { output: [ - { type: 'reasoning', id: 'rs_1', summary: ['considered ', 'options'], encryptedContent: 'opaque', metadata: { encrypted_content: 'opaque' } }, + { type: 'reasoning', id: 'rs_1', summary: ['considered ', 'options'], encryptedContent: 'opaque', metadata: undefined }, { type: 'message', content: [{ type: 'text', text: 'hello ' }, { type: 'text', text: 'world' }] }, { type: 'function_call', callId: 't1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }, { type: 'custom_tool_call', callId: 't2', name: 'apply_patch', input: 'patch' }, @@ -217,18 +218,19 @@ suite('AgentHostByokLmHandler', () => { { role: ChatMessageRole.Assistant, content: [ - { type: 'thinking', value: ['thought'], id: 'rs_1', metadata: { encrypted_content: 'opaque' } }, - { type: 'text', value: 'checking' }, - { type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }, - { type: 'tool_use', name: 'apply_patch', toolCallId: 't2', parameters: { input: 'patch' } }, + { type: 'thinking', value: ['thought'], id: 'rs_1', metadata: undefined }, + { type: 'data', mimeType: 'application/vnd.code.agent-host-byok-reasoning+json', data: '{"id":"rs_1","encryptedContent":"opaque"}' }, ], }, + { role: ChatMessageRole.Assistant, content: [{ type: 'text', value: 'checking' }] }, + { role: ChatMessageRole.Assistant, content: [{ type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }] }, + { role: ChatMessageRole.Assistant, content: [{ type: 'tool_use', name: 'apply_patch', toolCallId: 't2', parameters: { input: 'patch' } }] }, { role: ChatMessageRole.User, content: [{ type: 'tool_result', toolCallId: 't1', value: [{ type: 'text', value: 'sunny' }] }] }, { role: ChatMessageRole.User, content: [{ type: 'tool_result', toolCallId: 't2', value: [{ type: 'text', value: 'Done!' }] }] }, { role: ChatMessageRole.User, content: [{ type: 'text', value: 'hi' }] }, ], options: { - modelOptions: { temperature: 0.5 }, + modelOptions: { temperature: 0.5, _vscodeAgentHostByokReasoningBridge: true }, configuration: { reasoningEffort: 'high' }, tools: [ { name: 'getWeather', description: '', inputSchema: { type: 'object' } }, From 967fb3410b6e712b55f55300577838d6e429ecaa Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 30 Jul 2026 16:28:29 +0200 Subject: [PATCH 03/86] Support custom views inside the agents window workbench --- src/vs/sessions/LAYOUT.md | 23 +- src/vs/sessions/browser/layoutActions.ts | 3 +- src/vs/sessions/browser/media/workbench.css | 16 +- src/vs/sessions/browser/menus.ts | 3 + .../sessions/browser/mobileNavigationStack.ts | 2 +- .../sessions/browser/parts/agentsPartCard.ts | 55 ++++ .../browser/parts/customViewGridPart.ts | 155 ++++++++++ .../browser/parts/customViewGridParts.ts | 41 +++ .../sessions/browser/parts/customViewNode.ts | 159 ++++++++++ .../parts/media/customViewGridPart.css | 107 +++++++ .../browser/parts/mobile/mobileChatShell.css | 4 +- .../parts/mobile/mobileSessionsPart.ts | 6 +- src/vs/sessions/browser/parts/sessionView.ts | 3 +- src/vs/sessions/browser/parts/sessionsPart.ts | 28 +- .../sessions/browser/singlePaneWorkbench.ts | 15 +- src/vs/sessions/browser/workbench.ts | 284 +++++++++++++++--- src/vs/sessions/common/contextkeys.ts | 6 + src/vs/sessions/common/sizes.ts | 7 + .../browser/customViewTest.contribution.ts | 133 ++++++++ .../browser/media/customViewTest.css | 9 + .../browser/baseSessionLayoutController.ts | 20 +- .../browser/sessionsTerminalContribution.ts | 4 +- .../services/customView/browser/customView.ts | 63 ++++ .../browser/customViewGridPartService.ts | 25 ++ .../customView/browser/customViewService.ts | 81 +++++ .../test/browser/customViewService.test.ts | 82 +++++ .../sessions/browser/sessionsService.ts | 6 + .../browser/sessionsManagementService.test.ts | 2 + src/vs/sessions/sessions.common.main.ts | 3 + .../sessions/test/browser/workbench.test.ts | 177 ++++++++++- .../services/layout/browser/layoutService.ts | 1 + .../sessions/customViewNode.fixture.ts | 93 ++++++ 32 files changed, 1529 insertions(+), 87 deletions(-) create mode 100644 src/vs/sessions/browser/parts/agentsPartCard.ts create mode 100644 src/vs/sessions/browser/parts/customViewGridPart.ts create mode 100644 src/vs/sessions/browser/parts/customViewGridParts.ts create mode 100644 src/vs/sessions/browser/parts/customViewNode.ts create mode 100644 src/vs/sessions/browser/parts/media/customViewGridPart.css create mode 100644 src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts create mode 100644 src/vs/sessions/contrib/customViewTest/browser/media/customViewTest.css create mode 100644 src/vs/sessions/services/customView/browser/customView.ts create mode 100644 src/vs/sessions/services/customView/browser/customViewGridPartService.ts create mode 100644 src/vs/sessions/services/customView/browser/customViewService.ts create mode 100644 src/vs/sessions/services/customView/test/browser/customViewService.test.ts create mode 100644 src/vs/workbench/test/browser/componentFixtures/sessions/customViewNode.fixture.ts diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index 5f8fe05ed39..437ada9e98a 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -37,6 +37,7 @@ Editors open as modal overlays via `ModalEditorPart`. The main editor part exist | Titlebar | Top, full width | Always visible | Session picker, toggle actions, account widget | | Sidebar | Left, below titlebar | Visible | Sessions list | | Sessions Part | Center of right section | Visible | Grid of one or more session views (each rendering the active chat of its session) | +| Custom View Grid | Same row as the Sessions Part | Hidden | Grid of custom views shown *instead of* the Sessions Part — see [§2.4](#24-custom-view-grid) | | Editor | In grid, beside Sessions Part | Hidden | Shown for explicit editor workflows | | Auxiliary Bar | Right side | Visible | Changes view, file tree | | Panel | Below Sessions Part + Aux Bar | Hidden | Terminal, debug output | @@ -52,7 +53,8 @@ Orientation: VERTICAL (root) ├── Top Right (HORIZONTAL) │ ├── Sessions Part (leaf, remaining width) │ ├── Editor (leaf, hidden by default) - │ └── Auxiliary Bar (leaf, 340px default) + │ ├── Auxiliary Bar (leaf, 340px default) + │ └── Custom View Grid (leaf, hidden by default) └── Panel (leaf, 300px default, hidden) ``` @@ -72,6 +74,7 @@ The workbench grid is built with `proportionalLayout: false` (see `createWorkben | Sessions Part | **`High`** | The single flexible view — grows/shrinks to absorb every horizontal delta. `minimumWidth` 300, `maximumWidth` ∞. | | Editor | `Normal` | Keeps its user-set width (`600` default); only resized via its own sash. | | Auxiliary Bar | `Low` | Keeps its user-set width (`340` default); only resized via its own sash. | +| Custom View Grid | **`High`** | Claims the whole row. Never visible at the same time as the Sessions Part, so the "exactly one `High` view" invariant below still holds. | In the single-pane detail-panel layout, first-run sidebar width is slightly narrower (280px) so a typical window keeps roughly balanced chat and third-pane widths when the pane is shown. Persisted `_savedPartSizes` always win over these defaults. @@ -79,6 +82,24 @@ In the single-pane detail-panel layout, first-run sidebar width is slightly narr > **Pitfall:** the `High` role must live on the Sessions Part, not the editor. It was previously on the editor, but that made the editor drift to its 300px minimum when the auxiliary bar was toggled across session switches. When moving the role, set the Sessions Part to `High` **and** the editor to `Normal` together — removing `High` from the editor without adding it to the Sessions Part leaves the chain with no `High` view and reintroduces the growing-sidebar bug. +### 2.4 Custom View Grid + +The Custom View Grid (`CustomViewGridPart` in [browser/parts/customViewGridPart.ts](src/vs/sessions/browser/parts/customViewGridPart.ts)) hosts full-surface views that replace the sessions grid — for example a management or dashboard surface that is not tied to a single session. + +**Contract — it is mutually exclusive with the sessions surface.** While a custom view is shown, the Sessions Part, the Editor part *in the grid*, the Auxiliary Bar (side panel) and the Panel (terminal) are all hidden, and vice versa. Only the titlebar and the primary sidebar remain. The *modal* editor part is not affected and may still open over the custom view. + +Which view is shown is owned by `ICustomViewService` ([services/customView/browser/customViewService.ts](src/vs/sessions/services/customView/browser/customViewService.ts)): contributions register an `ICustomViewDescriptor` (id, title, view constructor and optional header actions) and call `showCustomView(id)` / `hideCustomView()`. The workbench observes `activeCustomView` and applies the layout; it is not persisted, so a reload always starts on the sessions grid. + +**Desired vs. effective visibility.** The covered parts keep their *desired* visibility in `partVisibility` — showing a custom view only changes what the grid renders (`Workbench._effectiveVisible`). So a layout-controller change made while the custom view is shown (e.g. the user opened a different session in the background) is what gets restored when it is hidden, and `_savePartVisibility` never records the forced-hidden state. `IWorkbenchLayoutService.isVisible` reports the effective value and `onDidChangePartVisibility` fires for the parts whose effective visibility flips, so context keys stay truthful; the layout controller's per-session capture listeners skip those transitions (`_isCustomViewVisible`). + +> **Pitfall:** `SplitView` calls `Part.setVisible` when a view's grid visibility changes, and the workbench maps that event straight back onto the desired visibility (`setSessionsHidden`, `setPanelHidden`, …). The custom view's grid updates therefore run under `_applyingCustomViewGridVisibility`, which makes that listener bail — without it, hiding the parts for a custom view *overwrites* the state that is supposed to be restored, and hiding the custom view leaves neither grid visible. For the same reason, showing a custom view first exits a maximized editor (a maximized editor owns the row instead of the sessions grid) and the grid descriptor is built from the effective values. + +**Dismissal.** Opening a session (`SessionsService._startOpenSession`, which every explicit open gesture funnels through) hides the custom view. On phone layouts showing one pushes a `MobileNavigationStack` layer, so the Android back button dismisses it. Actions that operate on the hidden parts — Toggle Side Panel, Open Terminal, and the secondary side bar toggle — are disabled while it is shown (`CustomViewVisibleContext`). + +**Chrome.** Each grid leaf is a `CustomViewNode` ([browser/parts/customViewNode.ts](src/vs/sessions/browser/parts/customViewNode.ts)) that owns the shared header — title, optional description and the contributed actions rendered either as an icon toolbar or a button bar — above a scroll container that grows a bottom border on the header as soon as the content is scrolled. The header band and the content are centred and capped to `AGENTS_CENTERED_CONTENT_MAX_WIDTH` (the same measure the session views use); a view may override it with `AbstractCustomView.maxWidth`. Views only fill the content container and are disposed when hidden. + +**Card chrome is shared.** The Sessions Part and the Custom View Grid both carry the `agents-part-card` class (`AGENTS_PART_CARD_CLASS`) and use `agentsPartCard.ts` for their metrics, themed colors and content-box math, so their padding, margins, background, border and corner radius are defined once and are identical. + --- ## 3. Titlebar diff --git a/src/vs/sessions/browser/layoutActions.ts b/src/vs/sessions/browser/layoutActions.ts index 6323b922a1f..8ea43786422 100644 --- a/src/vs/sessions/browser/layoutActions.ts +++ b/src/vs/sessions/browser/layoutActions.ts @@ -16,7 +16,7 @@ import { KeybindingWeight } from '../../platform/keybinding/common/keybindingsRe import { registerIcon } from '../../platform/theme/common/iconRegistry.js'; import { AuxiliaryBarVisibleContext, IsAuxiliaryWindowContext, IsSessionsWindowContext, IsTopRightEditorGroupContext, IsWindowAlwaysOnTopContext, SideBarVisibleContext } from '../../workbench/common/contextkeys.js'; import { IWorkbenchLayoutService, Parts } from '../../workbench/services/layout/browser/layoutService.js'; -import { SessionsWelcomeVisibleContext, SinglePaneLayoutEnabledContext } from '../common/contextkeys.js'; +import { SessionsWelcomeVisibleContext, SinglePaneLayoutEnabledContext, CustomViewVisibleContext } from '../common/contextkeys.js'; // Register Icons const panelCloseIcon = registerIcon('agent-panel-close', Codicon.close, localize('agentPanelCloseIcon', "Icon to close the panel.")); @@ -80,6 +80,7 @@ registerAction2(ToggleSidebarVisibilityAction); const editorTitleAuxiliaryBarWhen = ContextKeyExpr.and( IsSessionsWindowContext, IsAuxiliaryWindowContext.toNegated(), + CustomViewVisibleContext.negate(), IsTopRightEditorGroupContext); const isSinglePaneDetailPanelDisabled = SinglePaneLayoutEnabledContext.negate(); diff --git a/src/vs/sessions/browser/media/workbench.css b/src/vs/sessions/browser/media/workbench.css index 71476d86020..4659249feaa 100644 --- a/src/vs/sessions/browser/media/workbench.css +++ b/src/vs/sessions/browser/media/workbench.css @@ -141,7 +141,9 @@ border-bottom-right-radius: 8px; } -.agent-sessions-workbench .part.sessionspart { +/* Floating content card. Shared by every full-surface content part (the + sessions grid and the custom view grid) so they are visually identical. */ +.agent-sessions-workbench .agents-part-card { margin: 0 var(--vscode-agents-layout-floatingPanelGap) 0 0; background: var(--part-background); border: 1px solid var(--part-border-color, transparent); @@ -149,7 +151,7 @@ box-sizing: border-box; } -.agent-sessions-workbench.noeditorpane .part.sessionspart { +.agent-sessions-workbench.noeditorpane .agents-part-card { margin-right: 0; } @@ -183,7 +185,7 @@ box-sizing: border-box; } -.monaco-workbench.vs.agent-sessions-workbench .part.sessionspart, +.monaco-workbench.vs.agent-sessions-workbench .agents-part-card, .monaco-workbench.vs.agent-sessions-workbench .part.auxiliarybar, .monaco-workbench.vs.agent-sessions-workbench .part.panel { border-color: var(--vscode-editorWidget-border, var(--vscode-widget-border, transparent)); @@ -328,7 +330,7 @@ .agent-sessions-workbench .part.auxiliarybar, .agent-sessions-workbench .part.panel, -.agent-sessions-workbench .part.sessionspart { +.agent-sessions-workbench .agents-part-card { transition: opacity 250ms ease-out, margin-top 250ms ease-out, @@ -346,7 +348,7 @@ .agent-sessions-workbench .part.auxiliarybar, .agent-sessions-workbench .part.panel, - .agent-sessions-workbench .part.sessionspart { + .agent-sessions-workbench .agents-part-card { opacity: 0; border-color: transparent; background: color-mix(in srgb, var(--part-background) 60%, var(--vscode-sideBar-background)); @@ -365,7 +367,7 @@ margin: 0 16px 0 6px; } - .agent-sessions-workbench .part.sessionspart { + .agent-sessions-workbench .agents-part-card { margin: 6px 16px 0 16px; } } @@ -374,7 +376,7 @@ .agent-sessions-workbench .part.auxiliarybar, .agent-sessions-workbench .part.panel, - .agent-sessions-workbench .part.sessionspart, + .agent-sessions-workbench .agents-part-card, .agent-sessions-workbench .part.sidebar > .content { transition: none; } diff --git a/src/vs/sessions/browser/menus.ts b/src/vs/sessions/browser/menus.ts index 896dc61248c..8b094c543c9 100644 --- a/src/vs/sessions/browser/menus.ts +++ b/src/vs/sessions/browser/menus.ts @@ -33,6 +33,9 @@ export const Menus = { GoMenu: new MenuId('SessionsGoMenu'), AgentFeedbackEditorContent: new MenuId('AgentFeedbackEditorContent'), + /** Header actions of the test custom view. */ + CustomViewTest: new MenuId('SessionsCustomViewTest'), + NewSessionConfig: new MenuId('NewSessions.SessionConfigMenu'), NewSessionControl: new MenuId('NewSessions.SessionControlMenu'), NewSessionRepositoryConfig: new MenuId('NewSessions.RepositoryConfigMenu'), diff --git a/src/vs/sessions/browser/mobileNavigationStack.ts b/src/vs/sessions/browser/mobileNavigationStack.ts index 020022bf65c..3f8975f9471 100644 --- a/src/vs/sessions/browser/mobileNavigationStack.ts +++ b/src/vs/sessions/browser/mobileNavigationStack.ts @@ -7,7 +7,7 @@ import { Disposable } from '../../base/common/lifecycle.js'; import { Emitter, Event } from '../../base/common/event.js'; import { mainWindow } from '../../base/browser/window.js'; -export type MobileNavigationLayer = 'sidebar' | 'editor' | 'panel' | 'auxbar'; +export type MobileNavigationLayer = 'sidebar' | 'editor' | 'panel' | 'auxbar' | 'customView'; interface MobileNavigationEntry { readonly layer: MobileNavigationLayer; diff --git a/src/vs/sessions/browser/parts/agentsPartCard.ts b/src/vs/sessions/browser/parts/agentsPartCard.ts new file mode 100644 index 00000000000..4c7fc4791b9 --- /dev/null +++ b/src/vs/sessions/browser/parts/agentsPartCard.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IColorTheme } from '../../../platform/theme/common/themeService.js'; +import { agentsPanelBackground, agentsPanelBorder, agentsPanelForeground } from '../../common/theme.js'; +import { AGENTS_FLOATING_PANEL_GAP } from '../../common/sizes.js'; + +/** + * Marks a part as a floating content card. Carries the shared background, + * border, corner radius and outer margin (see `media/workbench.css`) so every + * content part in the Agents window is styled from one place. + */ +export const AGENTS_PART_CARD_CLASS = 'agents-part-card'; + +/** Visual metrics of a card part, kept in sync with the CSS in `media/workbench.css`. */ +export const AgentsPartCard = { + MARGIN_TOP: 0, + MARGIN_LEFT: 0, + MARGIN_RIGHT: AGENTS_FLOATING_PANEL_GAP, + MARGIN_RIGHT_NO_EDITOR_PANE: 0, + MARGIN_BOTTOM: 0, + BORDER_WIDTH: 1, +} as const; + +/** + * Content box of a card part, i.e. its grid-allocated size minus the card's + * visual margins and border. + */ +export function getAgentsPartCardContentSize(width: number, height: number, editorPaneVisible: boolean): { readonly width: number; readonly height: number } { + const borderTotal = AgentsPartCard.BORDER_WIDTH * 2; + const marginRight = editorPaneVisible ? AgentsPartCard.MARGIN_RIGHT : AgentsPartCard.MARGIN_RIGHT_NO_EDITOR_PANE; + + return { + width: width - AgentsPartCard.MARGIN_LEFT - marginRight - borderTotal, + height: height - AgentsPartCard.MARGIN_TOP - AgentsPartCard.MARGIN_BOTTOM - borderTotal + }; +} + +/** Publishes the themed card colors that `media/workbench.css` draws the card from. */ +export function applyAgentsPartCardStyles(container: HTMLElement, theme: IColorTheme): void { + container.style.setProperty('--part-background', theme.getColor(agentsPanelBackground)?.toString() ?? ''); + container.style.setProperty('--part-border-color', theme.getColor(agentsPanelBorder)?.toString() ?? 'transparent'); + container.style.setProperty('--part-foreground', theme.getColor(agentsPanelForeground)?.toString() ?? ''); + container.style.backgroundColor = theme.getColor(agentsPanelBackground)?.toString() ?? ''; +} + +/** Clears the inline card colors so CSS can take over (phone layout). */ +export function clearAgentsPartCardStyles(container: HTMLElement): void { + container.style.backgroundColor = ''; + container.style.removeProperty('--part-background'); + container.style.removeProperty('--part-border-color'); + container.style.color = ''; +} diff --git a/src/vs/sessions/browser/parts/customViewGridPart.ts b/src/vs/sessions/browser/parts/customViewGridPart.ts new file mode 100644 index 00000000000..a3c7b8df692 --- /dev/null +++ b/src/vs/sessions/browser/parts/customViewGridPart.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/customViewGridPart.css'; +import { $, size } from '../../../base/browser/dom.js'; +import { LayoutPriority } from '../../../base/browser/ui/splitview/splitview.js'; +import { assertReturnsDefined } from '../../../base/common/types.js'; +import { MutableDisposable } from '../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; +import { IStorageService } from '../../../platform/storage/common/storage.js'; +import { IThemeService } from '../../../platform/theme/common/themeService.js'; +import { Part } from '../../../workbench/browser/part.js'; +import { Parts } from '../../../workbench/services/layout/browser/layoutService.js'; +import { ICustomViewDescriptor } from '../../services/customView/browser/customView.js'; +import { applyAgentsPartCardStyles, clearAgentsPartCardStyles, getAgentsPartCardContentSize } from './agentsPartCard.js'; +import { CustomViewNode } from './customViewNode.js'; +import { isPhoneLayout } from './mobile/mobileLayout.js'; +import { IAgentWorkbenchLayoutService } from '../workbench.js'; + +/** + * Hosts the custom views that replace the sessions grid while one is shown. It + * is a passive renderer: the workbench drives it from + * `ICustomViewService.activeCustomView` via {@link setView}. + * + * Only a single view can be shown today; the part is structured as a grid of + * {@link CustomViewNode} leaves so more can be added later. + */ +export class CustomViewGridPart extends Part { + + override readonly minimumWidth: number = 300; + override readonly maximumWidth: number = Number.POSITIVE_INFINITY; + override readonly minimumHeight: number = 0; + override readonly maximumHeight: number = Number.POSITIVE_INFINITY; + get snap(): boolean { return false; } + + readonly priority = LayoutPriority.High; + + private _contentArea: HTMLElement | undefined; + private readonly _node = this._register(new MutableDisposable()); + private _descriptor: ICustomViewDescriptor | undefined; + private _lastContentSize: { readonly width: number; readonly height: number } | undefined; + + constructor( + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IAgentWorkbenchLayoutService private readonly agentWorkbenchLayoutService: IAgentWorkbenchLayoutService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super( + Parts.CUSTOM_VIEW_GRID_PART, + { hasTitle: false, borderWidth: () => 0 }, + themeService, + storageService, + agentWorkbenchLayoutService + ); + } + + override create(parent: HTMLElement): void { + this.element = parent; + + super.create(parent); + } + + protected override createContentArea(parent: HTMLElement): HTMLElement { + const contentArea = $('.custom-view-grid'); + parent.appendChild(contentArea); + this._contentArea = contentArea; + + this._renderView(); + + return contentArea; + } + + /** Renders the given custom view, replacing (and disposing) the previous one. */ + setView(descriptor: ICustomViewDescriptor | undefined): void { + if (this._descriptor === descriptor) { + return; + } + + this._descriptor = descriptor; + this._renderView(); + } + + private _renderView(): void { + if (!this._contentArea) { + return; + } + + this._node.clear(); + + if (!this._descriptor) { + return; + } + + const node = this.instantiationService.createInstance(CustomViewNode, this._descriptor); + this._node.value = node; + this._contentArea.appendChild(node.element); + + if (this._lastContentSize) { + this._layoutNode(this._lastContentSize.width, this._lastContentSize.height); + } + } + + focus(): void { + this._node.value?.focus(); + } + + override updateStyles(): void { + super.updateStyles(); + + const container = assertReturnsDefined(this.getContainer()); + if (isPhoneLayout(this.layoutService)) { + clearAgentsPartCardStyles(container); + return; + } + + applyAgentsPartCardStyles(container, this.theme); + } + + override layout(width: number, height: number, top: number, left: number): void { + if (!this.layoutService.isVisible(Parts.CUSTOM_VIEW_GRID_PART)) { + return; + } + + // On phone the part fills the grid cell without the card margins/border. + const cardSize = isPhoneLayout(this.layoutService) + ? { width, height } + : getAgentsPartCardContentSize(width, height, this.agentWorkbenchLayoutService.isEditorPaneVisible()); + + const { contentSize } = this.layoutContents(cardSize.width, cardSize.height); + this._layoutNode(contentSize.width, contentSize.height); + + super.layout(width, height, top, left); + } + + private _layoutNode(width: number, height: number): void { + this._lastContentSize = { width, height }; + + const node = this._node.value; + if (!node) { + return; + } + + size(node.element, width, height); + node.layout(width, height); + } + + toJSON(): object { + return { + type: Parts.CUSTOM_VIEW_GRID_PART + }; + } +} diff --git a/src/vs/sessions/browser/parts/customViewGridParts.ts b/src/vs/sessions/browser/parts/customViewGridParts.ts new file mode 100644 index 00000000000..34561933633 --- /dev/null +++ b/src/vs/sessions/browser/parts/customViewGridParts.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; +import { ICustomViewDescriptor } from '../../services/customView/browser/customView.js'; +import { ICustomViewGridPartService } from '../../services/customView/browser/customViewGridPartService.js'; +import { CustomViewGridPart } from './customViewGridPart.js'; + +/** + * Owns the lifecycle of the {@link CustomViewGridPart}. Registered as an eager + * singleton so the part registers itself with the workbench layout service + * before the workbench starts laying out parts. + */ +export class CustomViewGridParts extends Disposable implements ICustomViewGridPartService { + + declare readonly _serviceBrand: undefined; + + private readonly _mainPart: CustomViewGridPart; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + this._mainPart = this._register(instantiationService.createInstance(CustomViewGridPart)); + } + + setView(descriptor: ICustomViewDescriptor | undefined): void { + this._mainPart.setView(descriptor); + } + + focusActiveView(): void { + this._mainPart.focus(); + } +} + +registerSingleton(ICustomViewGridPartService, CustomViewGridParts, InstantiationType.Eager); diff --git a/src/vs/sessions/browser/parts/customViewNode.ts b/src/vs/sessions/browser/parts/customViewNode.ts new file mode 100644 index 00000000000..83f91d1f1a8 --- /dev/null +++ b/src/vs/sessions/browser/parts/customViewNode.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/customViewGridPart.css'; +import { $, isAncestorOfActiveElement } from '../../../base/browser/dom.js'; +import { DomScrollableElement } from '../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { autorun } from '../../../base/common/observable.js'; +import { ScrollbarVisibility } from '../../../base/common/scrollable.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../platform/actions/browser/toolbar.js'; +import { MenuItemAction } from '../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; +import { asCssVariable } from '../../../platform/theme/common/colorUtils.js'; +import { AGENTS_CENTERED_CONTENT_MAX_WIDTH } from '../../common/sizes.js'; +import { activeSessionViewBackground, activeSessionViewForeground } from '../../common/theme.js'; +import { AbstractCustomView, ICustomViewDescriptor } from '../../services/customView/browser/customView.js'; +import { SessionHeaderMetaActionViewItem } from './sessionHeaderMetaActionViewItem.js'; + +/** + * A leaf of the custom view grid. Owns the shared chrome — a header with the + * title, an optional description and the contributed actions, above a scroll + * container — and hosts one {@link AbstractCustomView} inside it. The header + * stays put while the content scrolls beneath it and grows a bottom border as + * soon as the content is scrolled. + */ +export class CustomViewNode extends Disposable { + + readonly element: HTMLElement = $('.custom-view-node'); + + private readonly _headerEl: HTMLElement; + private readonly _headerBandEl: HTMLElement; + private readonly _titleEl: HTMLElement; + private readonly _descriptionEl: HTMLElement; + private readonly _contentEl: HTMLElement; + private readonly _scrollable: DomScrollableElement; + private readonly _view: AbstractCustomView; + private readonly _maxWidth: number; + + private _lastLayout: { readonly width: number; readonly height: number } | undefined; + + constructor( + descriptor: ICustomViewDescriptor, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + this._view = this._register(instantiationService.createInstance(descriptor.ctor)); + this._maxWidth = this._view.maxWidth ?? AGENTS_CENTERED_CONTENT_MAX_WIDTH; + + // Mirror the active session view's surface colors so a custom view is + // visually indistinguishable from a session view. + this.element.style.setProperty('--session-view-background', asCssVariable(activeSessionViewBackground)); + this.element.style.setProperty('--session-view-foreground', asCssVariable(activeSessionViewForeground)); + this.element.setAttribute('role', 'region'); + + this._headerEl = $('.custom-view-header'); + this.element.appendChild(this._headerEl); + + this._headerBandEl = $('.custom-view-header-band'); + this._headerEl.appendChild(this._headerBandEl); + + const titleRow = $('.custom-view-header-title-row'); + this._headerBandEl.appendChild(titleRow); + + this._titleEl = $('.custom-view-header-title'); + titleRow.appendChild(this._titleEl); + + this._descriptionEl = $('.custom-view-header-description'); + this._headerBandEl.appendChild(this._descriptionEl); + + if (descriptor.actions) { + const buttonBar = descriptor.actions.style === 'buttonBar'; + const actionsContainer = $('.custom-view-header-actions'); + actionsContainer.classList.toggle('custom-view-header-actions-buttons', buttonBar); + titleRow.appendChild(actionsContainer); + + const toolbar = this._register(instantiationService.createInstance(MenuWorkbenchToolBar, actionsContainer, descriptor.actions.menuId, { + hiddenItemStrategy: HiddenItemStrategy.Ignore, + menuOptions: { shouldForwardArgs: true }, + toolbarOptions: { primaryGroup: () => true }, + actionViewItemProvider: buttonBar + ? (action, options) => action instanceof MenuItemAction + ? instantiationService.createInstance(SessionHeaderMetaActionViewItem, undefined, action, options) + : undefined + : undefined, + })); + this._register(toolbar.onDidChangeMenuItems(() => this._layoutChildren())); + } + + const scrollContent = $('.custom-view-scroll-content'); + this._contentEl = $('.custom-view-content'); + this._contentEl.tabIndex = -1; + scrollContent.appendChild(this._contentEl); + + this._scrollable = this._register(new DomScrollableElement(scrollContent, { + horizontal: ScrollbarVisibility.Hidden, + vertical: ScrollbarVisibility.Auto, + useShadows: false, + })); + this._scrollable.getDomNode().classList.add('custom-view-body'); + this.element.appendChild(this._scrollable.getDomNode()); + this._register(this._scrollable.onScroll(e => { + this._headerEl.classList.toggle('scrolled', e.scrollTop > 0); + })); + + this._view.render(this._contentEl); + + // The content grows and shrinks as the view loads, so keep the scrollbar in sync with it. + const resizeObserver = new ResizeObserver(() => this._scrollable.scanDomNode()); + resizeObserver.observe(this._contentEl); + this._register(toDisposable(() => resizeObserver.disconnect())); + + this._register(autorun(reader => { + const title = this._view.title.read(reader); + this._titleEl.textContent = title; + this.element.setAttribute('aria-label', title); + this._layoutChildren(); + })); + + this._register(autorun(reader => { + const description = this._view.description.read(reader); + this._descriptionEl.textContent = description ?? ''; + this._descriptionEl.classList.toggle('hidden', !description); + this._layoutChildren(); + })); + + this._register(toDisposable(() => this.element.remove())); + } + + layout(width: number, height: number): void { + this._lastLayout = { width, height }; + this._layoutChildren(); + } + + focus(): void { + this._view.focus(); + if (!isAncestorOfActiveElement(this.element)) { + this._contentEl.focus(); + } + } + + private _layoutChildren(): void { + if (!this._lastLayout) { + return; + } + + const { width, height } = this._lastLayout; + const bandWidth = Math.min(width, this._maxWidth); + this._headerBandEl.style.width = `${bandWidth}px`; + this._contentEl.style.width = `${bandWidth}px`; + + // The scroll container is sized by flex, so only the view needs to be told + // how much room is left below the header. + this._view.layout(bandWidth, Math.max(0, height - this._headerEl.offsetHeight)); + this._scrollable.scanDomNode(); + } +} diff --git a/src/vs/sessions/browser/parts/media/customViewGridPart.css b/src/vs/sessions/browser/parts/media/customViewGridPart.css new file mode 100644 index 00000000000..956edb36215 --- /dev/null +++ b/src/vs/sessions/browser/parts/media/customViewGridPart.css @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench.nocustomviewgrid .part.customviewgridpart { + display: none !important; + visibility: hidden !important; +} + +.monaco-workbench .part.customviewgridpart > .content { + display: flex; +} + +.custom-view-grid { + display: flex; + flex-direction: row; + width: 100%; + height: 100%; + overflow: hidden; +} + +.custom-view-node { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + position: relative; + background-color: var(--session-view-background); + color: var(--session-view-foreground); +} + +/* Header: a centered, width-capped band matching the session header's measure + and side padding, so a custom view and a session view align. */ +.custom-view-header { + flex-shrink: 0; +} + +.custom-view-header-band { + box-sizing: border-box; + margin: 0 auto; + padding: 6px 10px; + border-bottom: 1px solid transparent; +} + +.custom-view-header.scrolled .custom-view-header-band { + border-bottom-color: color-mix(in srgb, var(--session-view-foreground) 12%, transparent); +} + +.custom-view-header-title-row { + display: flex; + align-items: center; + gap: 6px; + min-height: 26px; +} + +.custom-view-header-title { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: var(--vscode-agents-fontSize-heading3, 13px); + font-weight: var(--vscode-agents-fontWeight-semiBold, 600); +} + +.custom-view-header-actions { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.custom-view-header-actions.custom-view-header-actions-buttons .monaco-action-bar .actions-container { + gap: var(--vscode-spacing-size60, 6px); +} + +.custom-view-header-description { + margin-top: 2px; + font-size: var(--vscode-agents-fontSize-body2, 12px); + opacity: 0.8; +} + +.custom-view-header-description.hidden { + display: none; +} + +/* The scroll container is scrolled natively by DomScrollableElement, so it must + fill the body and clip; the content keeps its natural height. */ +.custom-view-body { + flex: 1 1 auto; + min-height: 0; +} + +.custom-view-scroll-content { + width: 100%; + height: 100%; + min-height: 100%; + overflow: hidden; +} + +.custom-view-content { + box-sizing: border-box; + margin: 0 auto; + padding: 10px; + outline: none; +} diff --git a/src/vs/sessions/browser/parts/mobile/mobileChatShell.css b/src/vs/sessions/browser/parts/mobile/mobileChatShell.css index 14d562f4fe5..81bc56f8e3f 100644 --- a/src/vs/sessions/browser/parts/mobile/mobileChatShell.css +++ b/src/vs/sessions/browser/parts/mobile/mobileChatShell.css @@ -203,7 +203,7 @@ /* Remove card appearance from ALL parts on phone. Specificity wins over the desktop card rule in style.css without !important; width/height match what the mobile Part.layout() already inlines. */ -.agent-sessions-workbench.phone-layout .part.sessionspart, +.agent-sessions-workbench.phone-layout .agents-part-card, .agent-sessions-workbench.phone-layout .part.sidebar, .agent-sessions-workbench.phone-layout .part.auxiliarybar, .agent-sessions-workbench.phone-layout .part.panel { @@ -222,7 +222,7 @@ policy above). Without this, opening the sidebar — which makes the splitview share space between sidebar and sessions part — would shrink the sessions part's content during the drawer slide animation. */ -.agent-sessions-workbench.phone-layout .part.sessionspart > .content, +.agent-sessions-workbench.phone-layout .agents-part-card > .content, .agent-sessions-workbench.phone-layout .part.sidebar > .content, .agent-sessions-workbench.phone-layout .part.auxiliarybar > .content, .agent-sessions-workbench.phone-layout .part.panel > .content { diff --git a/src/vs/sessions/browser/parts/mobile/mobileSessionsPart.ts b/src/vs/sessions/browser/parts/mobile/mobileSessionsPart.ts index c04360bb7a4..6b5c9549435 100644 --- a/src/vs/sessions/browser/parts/mobile/mobileSessionsPart.ts +++ b/src/vs/sessions/browser/parts/mobile/mobileSessionsPart.ts @@ -6,6 +6,7 @@ import { Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; import { Part } from '../../../../workbench/browser/part.js'; import { SessionsPart } from '../sessionsPart.js'; +import { clearAgentsPartCardStyles } from '../agentsPartCard.js'; import { isPhoneLayout } from './mobileLayout.js'; /** @@ -31,10 +32,7 @@ export class MobileSessionsPart extends SessionsPart { const container = this.getContainer(); if (container) { - container.style.backgroundColor = ''; - container.style.removeProperty('--part-background'); - container.style.removeProperty('--part-border-color'); - container.style.color = ''; + clearAgentsPartCardStyles(container); } } diff --git a/src/vs/sessions/browser/parts/sessionView.ts b/src/vs/sessions/browser/parts/sessionView.ts index 510b82869ed..6cf624cf6f1 100644 --- a/src/vs/sessions/browser/parts/sessionView.ts +++ b/src/vs/sessions/browser/parts/sessionView.ts @@ -26,6 +26,7 @@ import { ISessionContext, SessionContext } from '../../services/sessions/browser import { autorun, observableFromEvent, observableValue } from '../../../base/common/observable.js'; import { SessionIsMaximizedContext } from '../../common/contextkeys.js'; import { UNARCHIVE_SESSION_COMMAND_ID } from '../../common/sessionCommands.js'; +import { AGENTS_CENTERED_CONTENT_MAX_WIDTH } from '../../common/sizes.js'; import { setActiveSessionContextKeys } from '../../services/sessions/common/sessionContextKeys.js'; import { activeSessionViewBackground, activeSessionViewForeground, inactiveSessionViewBackground, inactiveSessionViewForeground } from '../../common/theme.js'; import { ChatInteractivity, SessionStatus } from '../../services/sessions/common/session.js'; @@ -49,7 +50,7 @@ export interface ISessionViewOptions extends IChatViewOptions { } export class SessionView extends Disposable implements ISerializableView { static readonly TYPE = 'sessions.sessionView'; - private static readonly CENTERED_CONTENT_MAX_WIDTH = 950; + private static readonly CENTERED_CONTENT_MAX_WIDTH = AGENTS_CENTERED_CONTENT_MAX_WIDTH; private static readonly ACTIVE_BACKGROUND = asCssVariable(activeSessionViewBackground); private static readonly ACTIVE_FOREGROUND = asCssVariable(activeSessionViewForeground); private static readonly INACTIVE_BACKGROUND = asCssVariable(inactiveSessionViewBackground); diff --git a/src/vs/sessions/browser/parts/sessionsPart.ts b/src/vs/sessions/browser/parts/sessionsPart.ts index 31234fb42f9..7e7f99d1392 100644 --- a/src/vs/sessions/browser/parts/sessionsPart.ts +++ b/src/vs/sessions/browser/parts/sessionsPart.ts @@ -8,8 +8,7 @@ import { IContextKey, IContextKeyService } from '../../../platform/contextkey/co import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { IStorageService } from '../../../platform/storage/common/storage.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; -import { agentsPanelBackground, agentsPanelBorder, agentsPanelForeground } from '../../common/theme.js'; -import { AGENTS_FLOATING_PANEL_GAP } from '../../common/sizes.js'; +import { agentsPanelBorder } from '../../common/theme.js'; import { Parts } from '../../../workbench/services/layout/browser/layoutService.js'; import { assertReturnsDefined } from '../../../base/common/types.js'; import { LayoutPriority } from '../../../base/browser/ui/splitview/splitview.js'; @@ -31,6 +30,7 @@ import { AbstractProgressScope, ScopedProgressIndicator } from '../../../workben import { observableValue } from '../../../base/common/observable.js'; import { IWorkbenchAssignmentService } from '../../../workbench/services/assignment/common/assignmentService.js'; import { IAgentWorkbenchLayoutService } from '../workbench.js'; +import { applyAgentsPartCardStyles, getAgentsPartCardContentSize } from './agentsPartCard.js'; /** * ExP treatment that, when enabled, moves the session type ("harness") picker @@ -56,13 +56,6 @@ export class SessionsPart extends Part { override readonly maximumHeight: number = Number.POSITIVE_INFINITY; get snap(): boolean { return false; } - /** Visual margin values for the card-like appearance */ - static readonly MARGIN_TOP = 0; - static readonly MARGIN_LEFT = 0; - static readonly MARGIN_RIGHT = AGENTS_FLOATING_PANEL_GAP; - static readonly MARGIN_RIGHT_NO_EDITOR_PANE = 0; - static readonly MARGIN_BOTTOM = 0; - /** Border width on the card (1px each side) */ static readonly BORDER_WIDTH = 1; @@ -406,11 +399,7 @@ export class SessionsPart extends Part { const container = assertReturnsDefined(this.getContainer()); - // Store background and border as CSS variables for the card styling on .part - container.style.setProperty('--part-background', this.getColor(agentsPanelBackground) || ''); - container.style.setProperty('--part-border-color', this.getColor(agentsPanelBorder) || 'transparent'); - container.style.setProperty('--part-foreground', this.getColor(agentsPanelForeground) || ''); - container.style.backgroundColor = this.getColor(agentsPanelBackground) || ''; + applyAgentsPartCardStyles(container, this.theme); this._gridWidget?.style({ separatorBorder: this._gridSeparatorBorder }); } @@ -422,17 +411,10 @@ export class SessionsPart extends Part { this._lastLayout = { width, height, top, left }; - // Compute content dimensions accounting for visual margins and border. - const borderTotal = SessionsPart.BORDER_WIDTH * 2; - const marginLeft = SessionsPart.MARGIN_LEFT; - const marginBottom = SessionsPart.MARGIN_BOTTOM; - const marginRight = this.agentWorkbenchLayoutService.isEditorPaneVisible() ? SessionsPart.MARGIN_RIGHT : SessionsPart.MARGIN_RIGHT_NO_EDITOR_PANE; + const cardSize = getAgentsPartCardContentSize(width, height, this.agentWorkbenchLayoutService.isEditorPaneVisible()); // Size the content area with the reduced dimensions. - const { contentSize } = this.layoutContents( - width - marginLeft - marginRight - borderTotal, - height - SessionsPart.MARGIN_TOP - marginBottom - borderTotal - ); + const { contentSize } = this.layoutContents(cardSize.width, cardSize.height); // Layout the internal grid widget within the content area. this._gridWidget?.layout(contentSize.width, contentSize.height, top, left); diff --git a/src/vs/sessions/browser/singlePaneWorkbench.ts b/src/vs/sessions/browser/singlePaneWorkbench.ts index 4f6a029e276..bf4db60f198 100644 --- a/src/vs/sessions/browser/singlePaneWorkbench.ts +++ b/src/vs/sessions/browser/singlePaneWorkbench.ts @@ -186,15 +186,22 @@ export class SinglePaneWorkbench extends Workbench { return editorVisible || auxBarVisible; } - protected override _topRightSectionChildren(sessionsNode: ISerializedNode, editorNode: ISerializedNode, _auxiliaryBarNode: ISerializedNode): ISerializedNode[] { + protected override _topRightSectionChildren(sessionsNode: ISerializedNode, editorNode: ISerializedNode, _auxiliaryBarNode: ISerializedNode, customViewGridNode: ISerializedNode): ISerializedNode[] { // The auxiliary bar is inside the editor part and omitted from the grid. - return [sessionsNode, editorNode]; + return [sessionsNode, editorNode, customViewGridNode]; } protected override _layoutSidePane(): void { this._layoutDockedAuxBar(); } + protected override _applyEditorAreaVisibility(): void { + // The auxiliary bar is docked inside the editor node rather than being a + // grid view of its own, so the node covers both. + this.workbenchGrid.setViewVisible(this.editorPartView, this._editorNodeShouldBeVisible()); + this._layoutDockedAuxBar(); + } + protected override _onGridDidChange(): void { this._syncEditorVisibility(this.workbenchGrid.getViewSize(this.editorPartView).width); } @@ -292,7 +299,7 @@ export class SinglePaneWorkbench extends Workbench { const shouldRestoreSavedWidth = !hidden && !shouldRestoreDockedEditorSize && canRestoreSavedWidth; const shouldApplyEvenSplit = !hidden && !shouldRestoreDockedEditorSize && !shouldRestoreSavedWidth; - this.workbenchGrid.setViewVisible(this.editorPartView, this.partVisibility.editor || this.partVisibility.auxiliaryBar); + this.workbenchGrid.setViewVisible(this.editorPartView, this._editorNodeShouldBeVisible()); if (hidden) { // Only "Hide Editor" (detail still visible) keeps the editor grid node @@ -373,7 +380,7 @@ export class SinglePaneWorkbench extends Workbench { if (this.workbenchGrid) { this.workbenchGrid.setViewVisible( this.editorPartView, - this.partVisibility.editor || this.partVisibility.auxiliaryBar + this._editorNodeShouldBeVisible() ); if (!hidden && !this.partVisibility.editor) { this._syncingEditorVisibility = true; diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 69438ed9c74..6330813577a 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -66,20 +66,24 @@ import { EditorMarkdownCodeBlockRenderer } from '../../editor/browser/widget/mar import { SyncDescriptor } from '../../platform/instantiation/common/descriptors.js'; import { TitleService } from './parts/titlebarPart.js'; import { EDITOR_PART_DEFAULT_WIDTH, EDITOR_PART_MINIMUM_WIDTH } from './parts/editorPartSizing.js'; -import { IContextKeyService } from '../../platform/contextkey/common/contextkey.js'; -import { EditorMaximizedContext, IsPhoneLayoutContext, SinglePaneLayoutEnabledContext } from '../common/contextkeys.js'; +import { IContextKey, IContextKeyService } from '../../platform/contextkey/common/contextkey.js'; +import { CustomViewVisibleContext, EditorMaximizedContext, IsPhoneLayoutContext, SinglePaneLayoutEnabledContext } from '../common/contextkeys.js'; import { NotificationsPosition, NotificationsSettings, getNotificationsPosition } from '../../workbench/common/notifications.js'; import { SessionsLayoutPolicy } from './layoutPolicy.js'; +import { AGENTS_PART_CARD_CLASS } from './parts/agentsPartCard.js'; import { MobileNavigationStack } from './mobileNavigationStack.js'; import { MobileTitlebarPart } from './parts/mobile/mobileTitlebarPart.js'; import { IMobileVisualViewport } from './parts/mobile/mobileVisualViewport.js'; import { autorun } from '../../base/common/observable.js'; import { ISessionsService } from '../services/sessions/browser/sessionsService.js'; import { ISessionsPartService } from '../services/sessions/browser/sessionsPartService.js'; +import { ICustomViewService } from '../services/customView/browser/customViewService.js'; +import { ICustomViewGridPartService } from '../services/customView/browser/customViewGridPartService.js'; +import { ICustomViewDescriptor } from '../services/customView/browser/customView.js'; import { ISessionsSetUpService } from './sessionsSetUpService.js'; //#region Workbench Options @@ -102,6 +106,7 @@ enum LayoutClasses { AUXILIARYBAR_HIDDEN = 'noauxiliarybar', EDITOR_PANE_HIDDEN = 'noeditorpane', SESSIONS_HIDDEN = 'nosessionspart', + CUSTOM_VIEW_GRID_HIDDEN = 'nocustomviewgrid', STATUSBAR_HIDDEN = 'nostatusbar', SHELL_GRADIENT_BACKGROUND = 'shell-gradient-background', FULLSCREEN = 'fullscreen', @@ -120,6 +125,7 @@ export interface IPartVisibilityState { editor: boolean; panel: boolean; sessions: boolean; + customViewGrid: boolean; } interface IPartSizesState { @@ -354,6 +360,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic protected editorPartView!: ISerializableView; protected sessionsPartView!: ISerializableView; + protected customViewGridPartView!: ISerializableView; /** The editor part container; the auxiliary bar is docked inside it. */ protected _editorPartContainer: HTMLElement | undefined; @@ -369,7 +376,8 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic auxiliaryBar: true, editor: false, panel: false, - sessions: true + sessions: true, + customViewGrid: false }; private mainWindowFullscreen = false; @@ -380,6 +388,9 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic private readonly mobileTopBarDisposables = this._register(new DisposableStore()); private _editorMaximized = false; + private _customViewVisibleKey!: IContextKey; + /** Guards the grid updates that show/hide the custom view from feeding back into the desired part visibility. */ + private _applyingCustomViewGridVisibility = false; private _editorLastNonMaximizedVisibility: IPartVisibilityState | undefined; private _editorLastNonMaximizedSize: IViewSize | undefined; private _restoreAttachedEditorMaximizedOnShow = false; @@ -407,6 +418,8 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic private viewDescriptorService!: IViewDescriptorService; private sessionsService!: ISessionsService; private sessionsPartService!: ISessionsPartService; + private customViewService!: ICustomViewService; + private customViewGridPartService!: ICustomViewGridPartService; private instantiationService!: IInstantiationService; private storageService!: IStorageService; @@ -785,7 +798,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // size (wide) here would restore a wide node on reload and flicker the editor // open via the width-based reveal-sync. Classic layout is unaffected // (`_editorNodeVisible` returns `partVisibility.editor` there). - const editorNodeVisible = this._editorNodeVisible(this.partVisibility.editor, this.partVisibility.auxiliaryBar); + const editorNodeVisible = this._editorNodeShouldBeVisible(); const editorGridWidth = this._persistedGridViewSize(this.editorPartView, 'width', editorNodeVisible); let editorWidth = this._persistedEditorWidth(editorGridWidth); @@ -808,10 +821,10 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic const sizes: IPartSizesState = { sidebar: this._persistedGridViewSize(this.sideBarPartView, 'width', this.partVisibility.sidebar), - auxiliaryBar: this._persistedGridViewSize(this.auxiliaryBarPartView, 'width', this.partVisibility.auxiliaryBar), - sessions: this._persistedGridViewSize(this.sessionsPartView, 'width', this.partVisibility.sessions), + auxiliaryBar: this._persistedGridViewSize(this.auxiliaryBarPartView, 'width', this._effectiveVisible(Parts.AUXILIARYBAR_PART)), + sessions: this._persistedGridViewSize(this.sessionsPartView, 'width', this._effectiveVisible(Parts.SESSIONS_PART)), editor: editorWidth, - panel: this._persistedGridViewSize(this.panelPartView, 'height', this.partVisibility.panel), + panel: this._persistedGridViewSize(this.panelPartView, 'height', this._effectiveVisible(Parts.PANEL_PART)), }; this.storageService.store(Workbench._PART_SIZES_KEY, JSON.stringify(sizes), StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -886,6 +899,9 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // Create Sessions Part this.createSessionsPart(); + // Create Custom View Grid Part (hidden by default) + this.createCustomViewGridPart(); + // Notification Handlers this.createNotificationsHandlers(instantiationService, notificationService, configurationService); @@ -1051,7 +1067,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic private createSessionsPart(): void { const sessionsPartContainer = document.createElement('div'); - sessionsPartContainer.classList.add('part', 'sessionspart', 'basepanel', 'right'); + sessionsPartContainer.classList.add('part', 'sessionspart', 'basepanel', 'right', AGENTS_PART_CARD_CLASS); sessionsPartContainer.id = Parts.SESSIONS_PART; sessionsPartContainer.setAttribute('role', 'main'); @@ -1062,6 +1078,19 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic this.mainContainer.appendChild(sessionsPartContainer); } + private createCustomViewGridPart(): void { + const customViewGridPartContainer = document.createElement('div'); + customViewGridPartContainer.classList.add('part', 'customviewgridpart', 'basepanel', 'right', AGENTS_PART_CARD_CLASS); + customViewGridPartContainer.id = Parts.CUSTOM_VIEW_GRID_PART; + customViewGridPartContainer.setAttribute('role', 'main'); + + mark(`code/willCreatePart/${Parts.CUSTOM_VIEW_GRID_PART}`); + this.getPart(Parts.CUSTOM_VIEW_GRID_PART).create(customViewGridPartContainer); + mark(`code/didCreatePart/${Parts.CUSTOM_VIEW_GRID_PART}`); + + this.mainContainer.appendChild(customViewGridPartContainer); + } + private restore(lifecycleService: ILifecycleService): void { // Update perf marks mark('code/didStartWorkbench'); @@ -1121,6 +1150,9 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // Forces eager creation of the sessions part so it registers itself with the // layout service before renderWorkbench() looks it up via getPart(). this.sessionsPartService = accessor.get(ISessionsPartService); + this.customViewService = accessor.get(ICustomViewService); + // Same for the custom view grid part. + this.customViewGridPartService = accessor.get(ICustomViewGridPartService); this.instantiationService = accessor.get(IInstantiationService); this.storageService = accessor.get(IStorageService); accessor.get(ITitleService); @@ -1131,6 +1163,13 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // Register layout listeners this.registerLayoutListeners(); + // A custom view replaces the sessions grid (and the editor, side panel and + // bottom panel) for as long as it is shown. + this._customViewVisibleKey = CustomViewVisibleContext.bindTo(accessor.get(IContextKeyService)); + this._register(autorun(reader => { + this._applyCustomViewGridVisibility(this.customViewService.activeCustomView.read(reader)); + })); + // Editor opens should only affect the main editor part when // they actually target one of the main editor groups. Modal // opens stay neutral. Programmatic opens that suppress auto @@ -1318,8 +1357,8 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic return editorVisible; } - protected _topRightSectionChildren(sessionsNode: ISerializedNode, editorNode: ISerializedNode, auxiliaryBarNode: ISerializedNode): ISerializedNode[] { - return [sessionsNode, editorNode, auxiliaryBarNode]; + protected _topRightSectionChildren(sessionsNode: ISerializedNode, editorNode: ISerializedNode, auxiliaryBarNode: ISerializedNode, customViewGridNode: ISerializedNode): ISerializedNode[] { + return [sessionsNode, editorNode, auxiliaryBarNode, customViewGridNode]; } /** Attach any per-layout controllers once the editor part container exists. */ @@ -1343,7 +1382,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // editor is hidden) before revealing, so the even split can halve it. const mainAreaWidth = this.workbenchGrid.getViewSize(this.sessionsPartView).width; - this.workbenchGrid.setViewVisible(this.editorPartView, !hidden); + this.workbenchGrid.setViewVisible(this.editorPartView, this._editorNodeShouldBeVisible()); if (shouldApplyEvenSplit) { this._hasAppliedInitialEditorSplit = true; @@ -1358,7 +1397,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // BlockRestore contribution) runs before createWorkbenchLayout(), so the // visibility is recorded in partVisibility and applied when the grid is built. if (this.workbenchGrid) { - this.workbenchGrid.setViewVisible(this.auxiliaryBarPartView, !hidden); + this.workbenchGrid.setViewVisible(this.auxiliaryBarPartView, this._effectiveVisible(Parts.AUXILIARYBAR_PART)); } } @@ -1417,6 +1456,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic const auxiliaryBarPart = this.getPart(Parts.AUXILIARYBAR_PART); const sideBar = this.getPart(Parts.SIDEBAR_PART); const sessionsPart = this.getPart(Parts.SESSIONS_PART); + const customViewGridPart = this.getPart(Parts.CUSTOM_VIEW_GRID_PART); // View references for parts in the grid this.titleBarPartView = titleBar; @@ -1424,6 +1464,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic this.panelPartView = panelPart; this.auxiliaryBarPartView = auxiliaryBarPart; this.sessionsPartView = sessionsPart; + this.customViewGridPartView = customViewGridPart; this.editorPartView = editorPart; const viewMap: { [key: string]: ISerializableView } = { @@ -1432,6 +1473,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic [Parts.SIDEBAR_PART]: this.sideBarPartView, [Parts.AUXILIARYBAR_PART]: this.auxiliaryBarPartView, [Parts.SESSIONS_PART]: this.sessionsPartView, + [Parts.CUSTOM_VIEW_GRID_PART]: this.customViewGridPartView, [Parts.EDITOR_PART]: this.editorPartView }; @@ -1457,6 +1499,13 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // Listen for part visibility changes (for parts in grid) for (const part of [titleBar, panelPart, sideBar, auxiliaryBarPart, sessionsPart, editorPart]) { this._register(part.onDidVisibilityChange(visible => { + // A custom view renders over these parts without changing what the layout + // wants them to be, so its grid updates must not feed back into the + // desired state — otherwise there is nothing left to restore. + if (this._applyingCustomViewGridVisibility) { + return; + } + // The editor part's grid-view visibility is fully owned by // `_onEditorPartGridVisibilityChange`: in the classic layout it maps to // the editor visibility and raises the part-visibility event; single-pane @@ -1496,6 +1545,9 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic case 'auxbar': this.setAuxiliaryBarHidden(true); break; + case 'customView': + this.customViewService.hideCustomView(); + break; case 'editor': // Editor modal close is handled by the editor service break; @@ -1591,36 +1643,45 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic type: 'leaf', data: { type: Parts.SESSIONS_PART }, size: sessionsWidth, - visible: this.partVisibility.sessions + visible: this._effectiveVisible(Parts.SESSIONS_PART) + }; + + // Mutually exclusive with the sessions part (and the editor / auxiliary bar / + // panel), so it always claims the full row when it is visible. + const customViewGridNode: ISerializedLeafNode = { + type: 'leaf', + data: { type: Parts.CUSTOM_VIEW_GRID_PART }, + size: rightSectionWidth, + visible: this.partVisibility.customViewGrid }; const editorNode: ISerializedLeafNode = { type: 'leaf', data: { type: Parts.EDITOR_PART }, size: this._editorNodeSize(effectiveEditorWidth, effectiveAuxBarWidth), - visible: this._editorNodeVisible(this.partVisibility.editor, this.partVisibility.auxiliaryBar) + visible: this._editorNodeShouldBeVisible() }; const auxiliaryBarNode: ISerializedLeafNode = { type: 'leaf', data: { type: Parts.AUXILIARYBAR_PART }, size: auxiliaryBarSize, - visible: this.partVisibility.auxiliaryBar + visible: this._effectiveVisible(Parts.AUXILIARYBAR_PART) }; const panelNode: ISerializedLeafNode = { type: 'leaf', data: { type: Parts.PANEL_PART }, size: panelSize, - visible: this.partVisibility.panel + visible: this._effectiveVisible(Parts.PANEL_PART) }; - // Top right section: Chat Bar | Editor [| Auxiliary Bar] (horizontal). + // Top right section: Chat Bar | Editor [| Auxiliary Bar] | Custom View Grid (horizontal). // When docked, the auxiliary bar is inside the editor part and // omitted from the grid; otherwise it is its own trailing grid column. const topRightSection: ISerializedNode = { type: 'branch', - data: this._topRightSectionChildren(sessionsNode, editorNode, auxiliaryBarNode), + data: this._topRightSectionChildren(sessionsNode, editorNode, auxiliaryBarNode, customViewGridNode), size: topRightHeight }; @@ -1821,11 +1882,12 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic getLayoutClasses(): string[] { return coalesce([ !this.partVisibility.sidebar ? LayoutClasses.SIDEBAR_HIDDEN : undefined, - !this.partVisibility.editor ? LayoutClasses.MAIN_EDITOR_AREA_HIDDEN : undefined, - !this.partVisibility.panel ? LayoutClasses.PANEL_HIDDEN : undefined, - !this.partVisibility.auxiliaryBar ? LayoutClasses.AUXILIARYBAR_HIDDEN : undefined, + !this._effectiveVisible(Parts.EDITOR_PART) ? LayoutClasses.MAIN_EDITOR_AREA_HIDDEN : undefined, + !this._effectiveVisible(Parts.PANEL_PART) ? LayoutClasses.PANEL_HIDDEN : undefined, + !this._effectiveVisible(Parts.AUXILIARYBAR_PART) ? LayoutClasses.AUXILIARYBAR_HIDDEN : undefined, !this.isEditorPaneVisible() ? LayoutClasses.EDITOR_PANE_HIDDEN : undefined, - !this.partVisibility.sessions ? LayoutClasses.SESSIONS_HIDDEN : undefined, + !this._effectiveVisible(Parts.SESSIONS_PART) ? LayoutClasses.SESSIONS_HIDDEN : undefined, + !this.partVisibility.customViewGrid ? LayoutClasses.CUSTOM_VIEW_GRID_HIDDEN : undefined, LayoutClasses.STATUSBAR_HIDDEN, // agents window never has a status bar this.mainWindowFullscreen ? LayoutClasses.FULLSCREEN : undefined, this.layoutPolicy.viewportClass.get() === 'phone' ? LayoutClasses.PHONE_LAYOUT : undefined, @@ -1833,7 +1895,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic } isEditorPaneVisible(): boolean { - return this.partVisibility.editor || this.partVisibility.auxiliaryBar; + return this._effectiveVisible(Parts.EDITOR_PART) || this._effectiveVisible(Parts.AUXILIARYBAR_PART); } private _updateEditorPaneVisibilityClass(): void { @@ -1892,6 +1954,9 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // TODO: focus chat bar content once it is wired up this.getPart(Parts.SESSIONS_PART).getContainer()?.focus(); break; + case Parts.CUSTOM_VIEW_GRID_PART: + this.customViewGridPartService.focusActiveView(); + break; default: { const container = this.getContainer(targetWindow, part); container?.focus(); @@ -1942,6 +2007,48 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic return true; // No activity bar in this layout } + /** + * Parts a visible custom view replaces. While the custom view grid is shown + * these keep their desired (per-session) visibility state but are not + * rendered, so hiding the custom view restores whatever the layout + * controller last asked for — including changes made while it was shown. + */ + private static readonly _CUSTOM_VIEW_EXCLUSIVE_PARTS = [ + Parts.SESSIONS_PART, + Parts.EDITOR_PART, + Parts.AUXILIARYBAR_PART, + Parts.PANEL_PART + ] as const; + + /** The desired visibility of a part, ignoring any custom view showing over it. */ + private _desiredVisible(part: Parts): boolean { + switch (part) { + case Parts.SESSIONS_PART: + return this.partVisibility.sessions; + case Parts.EDITOR_PART: + return this.partVisibility.editor; + case Parts.AUXILIARYBAR_PART: + return this.partVisibility.auxiliaryBar; + case Parts.PANEL_PART: + return this.partVisibility.panel; + default: + return false; + } + } + + /** Whether a part is actually rendered right now. */ + protected _effectiveVisible(part: Parts): boolean { + return this._desiredVisible(part) && !this.partVisibility.customViewGrid; + } + + /** + * Whether the editor grid node should be shown. In the single-pane layout the + * node also hosts the docked auxiliary bar, so it follows both parts. + */ + protected _editorNodeShouldBeVisible(): boolean { + return this._editorNodeVisible(this._effectiveVisible(Parts.EDITOR_PART), this._effectiveVisible(Parts.AUXILIARYBAR_PART)); + } + isVisible(part: SINGLE_WINDOW_PARTS): boolean; isVisible(part: MULTI_WINDOW_PARTS, targetWindow: Window): boolean; isVisible(part: Parts, targetWindow?: Window): boolean { @@ -1952,13 +2059,12 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic case Parts.SIDEBAR_PART: return this.partVisibility.sidebar; case Parts.AUXILIARYBAR_PART: - return this.partVisibility.auxiliaryBar; case Parts.EDITOR_PART: - return this.partVisibility.editor; case Parts.PANEL_PART: - return this.partVisibility.panel; case Parts.SESSIONS_PART: - return this.partVisibility.sessions; + return this._effectiveVisible(part); + case Parts.CUSTOM_VIEW_GRID_PART: + return this.partVisibility.customViewGrid; case Parts.ACTIVITYBAR_PART: case Parts.STATUSBAR_PART: case Parts.BANNER_PART: @@ -1988,6 +2094,11 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic } toggleSecondarySideBar(): void { + // The side panel is replaced by the custom view grid while one is shown. + if (this.partVisibility.customViewGrid) { + return; + } + const visible = !this.isSecondarySideBarVisible(); this.setAuxiliaryBarHidden(!visible); alert(visible @@ -2058,7 +2169,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic this._onWillHideAuxiliaryBar(hidden); this.partVisibility.auxiliaryBar = !hidden; - this.mainContainer.classList.toggle(LayoutClasses.AUXILIARYBAR_HIDDEN, hidden); + this.mainContainer.classList.toggle(LayoutClasses.AUXILIARYBAR_HIDDEN, !this._effectiveVisible(Parts.AUXILIARYBAR_PART)); this._applyAuxiliaryBarVisibility(hidden, source); this._updateEditorPaneVisibilityClass(); @@ -2121,7 +2232,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic } this.partVisibility.editor = !hidden; - this.mainContainer.classList.toggle(LayoutClasses.MAIN_EDITOR_AREA_HIDDEN, hidden); + this.mainContainer.classList.toggle(LayoutClasses.MAIN_EDITOR_AREA_HIDDEN, !this._effectiveVisible(Parts.EDITOR_PART)); if (this.editorPartView) { this._applyEditorVisibility(hidden); @@ -2172,12 +2283,12 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic const panelHadFocus = !hidden || this.hasFocus(Parts.PANEL_PART); this.partVisibility.panel = !hidden; - this.mainContainer.classList.toggle(LayoutClasses.PANEL_HIDDEN, hidden); + this.mainContainer.classList.toggle(LayoutClasses.PANEL_HIDDEN, !this._effectiveVisible(Parts.PANEL_PART)); // Propagate to grid this.workbenchGrid.setViewVisible( this.panelPartView, - !hidden, + this._effectiveVisible(Parts.PANEL_PART), ); // If panel becomes hidden, also hide the current active pane composite @@ -2200,7 +2311,10 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic } } - this.focusPart(Parts.PANEL_PART); + // A custom view is showing over the panel, so it must not take focus. + if (this._effectiveVisible(Parts.PANEL_PART)) { + this.focusPart(Parts.PANEL_PART); + } } } @@ -2210,10 +2324,109 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic } this.partVisibility.sessions = !hidden; - this.mainContainer.classList.toggle(LayoutClasses.SESSIONS_HIDDEN, hidden); + this.mainContainer.classList.toggle(LayoutClasses.SESSIONS_HIDDEN, !this._effectiveVisible(Parts.SESSIONS_PART)); // Propagate to grid - this.workbenchGrid.setViewVisible(this.sessionsPartView, !hidden); + this.workbenchGrid.setViewVisible(this.sessionsPartView, this._effectiveVisible(Parts.SESSIONS_PART)); + } + + /** + * Shows or hides the custom view grid. The custom view grid and the sessions + * grid are mutually exclusive and exactly one of them owns the row, so hiding + * the custom view always brings the sessions grid back (together with the side + * panel and panel state the layout wants for the active session). The parts it + * covers keep their desired visibility while it is shown, so the restore + * reflects whatever the layout controller last asked for. + */ + private _applyCustomViewGridVisibility(descriptor: ICustomViewDescriptor | undefined): void { + const visible = !!descriptor; + if (this.partVisibility.customViewGrid === visible) { + return; + } + + const wasVisible = Workbench._CUSTOM_VIEW_EXCLUSIVE_PARTS.map(part => this._effectiveVisible(part)); + + // A maximized editor owns the row instead of the sessions grid, which would + // leave the row without an owner once the custom view goes away. + if (visible && this._editorMaximized) { + this.setEditorMaximized(false); + } + + this.customViewGridPartService.setView(descriptor); + this.partVisibility.customViewGrid = visible; + this._customViewVisibleKey.set(visible); + + if (!this.workbenchGrid) { + return; // still starting up; the grid descriptor picks this state up + } + + this._applyingCustomViewGridVisibility = true; + try { + // One pass, revealing before hiding so the row never goes empty in between. + if (visible) { + this.workbenchGrid.setViewVisible(this.customViewGridPartView, true); + this._applyExclusivePartVisibility(); + } else { + this._applyExclusivePartVisibility(); + this.workbenchGrid.setViewVisible(this.customViewGridPartView, false); + } + } finally { + this._applyingCustomViewGridVisibility = false; + } + + this._updateExclusiveLayoutClasses(); + this.mainContainer.classList.toggle(LayoutClasses.CUSTOM_VIEW_GRID_HIDDEN, !visible); + this._updateMobileCustomViewNavigation(visible); + + Workbench._CUSTOM_VIEW_EXCLUSIVE_PARTS.forEach((part, index) => { + const nowVisible = this._effectiveVisible(part); + if (nowVisible !== wasVisible[index]) { + this._fireDidChangePartVisibility(part, nowVisible); + } + }); + + this.layout(); + + if (visible) { + this.focusPart(Parts.CUSTOM_VIEW_GRID_PART); + } else { + this.sessionsPartService.focusSession(this.sessionsService.activeSession.get()); + } + } + + private _applyExclusivePartVisibility(): void { + this.workbenchGrid.setViewVisible(this.sessionsPartView, this._effectiveVisible(Parts.SESSIONS_PART)); + this.workbenchGrid.setViewVisible(this.panelPartView, this._effectiveVisible(Parts.PANEL_PART)); + this._applyEditorAreaVisibility(); + } + + /** Pushes the editor and auxiliary bar node visibility into the grid. */ + protected _applyEditorAreaVisibility(): void { + this.workbenchGrid.setViewVisible(this.editorPartView, this._editorNodeShouldBeVisible()); + this.workbenchGrid.setViewVisible(this.auxiliaryBarPartView, this._effectiveVisible(Parts.AUXILIARYBAR_PART)); + } + + private _updateExclusiveLayoutClasses(): void { + this.mainContainer.classList.toggle(LayoutClasses.SESSIONS_HIDDEN, !this._effectiveVisible(Parts.SESSIONS_PART)); + this.mainContainer.classList.toggle(LayoutClasses.MAIN_EDITOR_AREA_HIDDEN, !this._effectiveVisible(Parts.EDITOR_PART)); + this.mainContainer.classList.toggle(LayoutClasses.AUXILIARYBAR_HIDDEN, !this._effectiveVisible(Parts.AUXILIARYBAR_PART)); + this.mainContainer.classList.toggle(LayoutClasses.PANEL_HIDDEN, !this._effectiveVisible(Parts.PANEL_PART)); + this._updateEditorPaneVisibilityClass(); + } + + /** Keeps the Android back button in sync with a shown custom view. */ + private _updateMobileCustomViewNavigation(visible: boolean): void { + if (this.layoutPolicy.viewportClass.get() !== 'phone') { + return; + } + + if (visible) { + if (!this.mobileNavStack.has('customView')) { + this.mobileNavStack.push('customView'); + } + } else if (this.mobileNavStack.has('customView')) { + this.mobileNavStack.popSilently('customView'); + } } //#endregion @@ -2297,6 +2510,8 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic return this.panelPartView; case Parts.SESSIONS_PART: return this.sessionsPartView; + case Parts.CUSTOM_VIEW_GRID_PART: + return this.customViewGridPartView; default: return undefined; } @@ -2370,6 +2585,7 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic editor: this.partVisibility.editor, panel: this.partVisibility.panel, sessions: this.partVisibility.sessions, + customViewGrid: this.partVisibility.customViewGrid, }; // Save the editor part size so it can be restored on un-maximize. diff --git a/src/vs/sessions/common/contextkeys.ts b/src/vs/sessions/common/contextkeys.ts index e69b6640739..92c76ff30f2 100644 --- a/src/vs/sessions/common/contextkeys.ts +++ b/src/vs/sessions/common/contextkeys.ts @@ -53,6 +53,12 @@ export const MultipleSessionsVisibleContext = new RawContextKey('multip //#endregion +//#region < --- Custom View Grid --- > + +export const CustomViewVisibleContext = new RawContextKey('customViewVisible', false, localize('customViewVisible', "Whether a custom view is shown in place of the sessions grid. The side panel and the panel are hidden while it is.")); + +//#endregion + //#region < --- Welcome --- > export const SessionsWelcomeVisibleContext = new RawContextKey('sessionsWelcomeVisible', false, localize('sessionsWelcomeVisible', "Whether the sessions welcome overlay is visible")); diff --git a/src/vs/sessions/common/sizes.ts b/src/vs/sessions/common/sizes.ts index 7043df8510e..5a28cd0967a 100644 --- a/src/vs/sessions/common/sizes.ts +++ b/src/vs/sessions/common/sizes.ts @@ -26,6 +26,13 @@ export const agentsLayoutFloatingPanelGap = registerSize( localize('agents.layout.floatingPanelGap', "Gap between floating panels in the Agents window.") ); +/** + * Width the centered content band of a content part is capped to (session + * views and custom views), so every content surface in the Agents window keeps + * the same measure. + */ +export const AGENTS_CENTERED_CONTENT_MAX_WIDTH = 950; + // ============================================================================ // Agents window — font ramp // ============================================================================ diff --git a/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts b/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts new file mode 100644 index 00000000000..7249649926a --- /dev/null +++ b/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/customViewTest.css'; +import { $ } from '../../../../base/browser/dom.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { constObservable, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { localize, localize2 } from '../../../../nls.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IsDevelopmentContext } from '../../../../platform/contextkey/common/contextkeys.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Menus } from '../../../browser/menus.js'; +import { AbstractCustomView } from '../../../services/customView/browser/customView.js'; +import { ICustomViewService } from '../../../services/customView/browser/customViewService.js'; + +const TEST_CUSTOM_VIEW_ID = 'sessions.customView.test'; + +/** Placeholder content used to exercise the custom view grid until real views exist. */ +class TestCustomView extends AbstractCustomView { + + private readonly _itemCount = observableValue(this, 40); + + readonly title: IObservable = constObservable(localize('testCustomView.title', "Test Custom View")); + override readonly description: IObservable = constObservable( + localize('testCustomView.description', "A placeholder view used to verify the custom view grid layout, header and scrolling.")); + + private _content: HTMLElement | undefined; + + render(container: HTMLElement): void { + this._content = container; + this._renderItems(); + } + + layout(_width: number, _height: number): void { } + + addItem(): void { + this._itemCount.set(this._itemCount.get() + 10, undefined); + this._renderItems(); + } + + private _renderItems(): void { + if (!this._content) { + return; + } + + this._content.textContent = ''; + for (let i = 0; i < this._itemCount.get(); i++) { + this._content.appendChild($('.custom-view-test-item', undefined, `Item ${i + 1}`)); + } + } +} + +class TestCustomViewContribution extends Disposable { + + static readonly ID = 'sessions.contrib.customViewTest'; + + constructor( + @ICustomViewService customViewService: ICustomViewService, + ) { + super(); + + this._register(customViewService.registerCustomView({ + id: TEST_CUSTOM_VIEW_ID, + title: localize('testCustomView.title', "Test Custom View"), + ctor: new SyncDescriptor(TestCustomView), + actions: { style: 'toolbar', menuId: Menus.CustomViewTest }, + })); + } +} + +registerWorkbenchContribution2(TestCustomViewContribution.ID, TestCustomViewContribution, WorkbenchPhase.BlockRestore); + +class ShowTestCustomViewAction extends Action2 { + + constructor() { + super({ + id: 'sessions.customView.showTestView', + title: localize2('showTestCustomView', "Show Test Custom View"), + category: Categories.Developer, + f1: true, + precondition: IsDevelopmentContext, + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(ICustomViewService).showCustomView(TEST_CUSTOM_VIEW_ID); + } +} + +class HideTestCustomViewAction extends Action2 { + + constructor() { + super({ + id: 'sessions.customView.hideTestView', + title: localize2('hideTestCustomView', "Hide Test Custom View"), + category: Categories.Developer, + f1: true, + precondition: IsDevelopmentContext, + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(ICustomViewService).hideCustomView(); + } +} + +/** Sample header action so the custom view header toolbar has something to render. */ +class TestCustomViewPingAction extends Action2 { + + constructor() { + super({ + id: 'sessions.customView.testView.ping', + title: localize2('testCustomViewPing', "Ping Test Custom View"), + icon: Codicon.debugAlt, + menu: [{ id: Menus.CustomViewTest, group: 'navigation', order: 1 }], + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(INotificationService).info(localize('testCustomViewPinged', "Test custom view action ran.")); + } +} + +registerAction2(ShowTestCustomViewAction); +registerAction2(HideTestCustomViewAction); +registerAction2(TestCustomViewPingAction); diff --git a/src/vs/sessions/contrib/customViewTest/browser/media/customViewTest.css b/src/vs/sessions/contrib/customViewTest/browser/media/customViewTest.css new file mode 100644 index 00000000000..68953f6405b --- /dev/null +++ b/src/vs/sessions/contrib/customViewTest/browser/media/customViewTest.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.custom-view-test-item { + padding: var(--vscode-spacing-size60, 6px) 0; + border-bottom: 1px solid color-mix(in srgb, var(--session-view-foreground) 8%, transparent); +} diff --git a/src/vs/sessions/contrib/layout/browser/baseSessionLayoutController.ts b/src/vs/sessions/contrib/layout/browser/baseSessionLayoutController.ts index 3f215fb09e4..7ffcd1ced17 100644 --- a/src/vs/sessions/contrib/layout/browser/baseSessionLayoutController.ts +++ b/src/vs/sessions/contrib/layout/browser/baseSessionLayoutController.ts @@ -36,7 +36,7 @@ import { IPaneCompositePartService } from '../../../../workbench/services/paneco import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; import { IAgentWorkbenchLayoutService } from '../../../browser/workbench.js'; import { Menus } from '../../../browser/menus.js'; -import { SessionsWelcomeVisibleContext, IsQuickChatSessionContext } from '../../../common/contextkeys.js'; +import { SessionsWelcomeVisibleContext, IsQuickChatSessionContext, CustomViewVisibleContext } from '../../../common/contextkeys.js'; import { logSidePanelToggle } from '../../../common/sessionsTelemetry.js'; import { ISessionChangesService } from '../../changes/browser/sessionChangesService.js'; import { IChangesViewService } from '../../changes/common/changesViewService.js'; @@ -226,7 +226,7 @@ export abstract class BaseLayoutController extends Disposable { if (e.partId !== Parts.PANEL_PART) { return; } - if (this.multipleSessionsVisibleObs.get()) { + if (this.multipleSessionsVisibleObs.get() || this._isCustomViewVisible()) { return; } const activeSession = this._sessionsService.activeSession.get(); @@ -248,7 +248,7 @@ export abstract class BaseLayoutController extends Disposable { if (e.partId !== Parts.EDITOR_PART || this._isRestoringSessionLayout) { return; } - if (this.multipleSessionsVisibleObs.get()) { + if (this.multipleSessionsVisibleObs.get() || this._isCustomViewVisible()) { return; } const activeSession = this._sessionsService.activeSession.get(); @@ -350,6 +350,15 @@ export abstract class BaseLayoutController extends Disposable { */ protected _registerAuxiliaryControllers(): void { } + /** + * Whether a custom view currently replaces the sessions grid. The parts it + * covers are force-hidden, so those transitions must not be captured as the + * active session's layout preference. + */ + protected _isCustomViewVisible(): boolean { + return this._layoutService.isVisible(Parts.CUSTOM_VIEW_GRID_PART); + } + /** * Registers the `Toggle Side Panel` action (menu item, keybinding, * command-palette entry). The action delegates straight to `toggleSidePane()`, @@ -374,8 +383,9 @@ export abstract class BaseLayoutController extends Disposable { category: Categories.View, f1: true, // A quick chat has no side pane (Round 20 hides the empty aux bar - // and the chat is full-width), so toggling it is meaningless. - precondition: IsQuickChatSessionContext.negate(), + // and the chat is full-width), so toggling it is meaningless. A custom + // view replaces the side pane entirely. + precondition: ContextKeyExpr.and(IsQuickChatSessionContext.negate(), CustomViewVisibleContext.negate()), keybinding: { weight: KeybindingWeight.SessionsContrib, primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyB diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index a5eedbb0626..5b167ae49d1 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -20,7 +20,7 @@ import { TerminalCapability } from '../../../../platform/terminal/common/capabil import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { Menus } from '../../../browser/menus.js'; import { isAgentHostProvider, LOCAL_AGENT_HOST_PROVIDER_ID } from '../../../common/agentHostSessionsProvider.js'; -import { SessionsWelcomeVisibleContext, IsPhoneLayoutContext } from '../../../common/contextkeys.js'; +import { SessionsWelcomeVisibleContext, IsPhoneLayoutContext, CustomViewVisibleContext } from '../../../common/contextkeys.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; import { ISession } from '../../../services/sessions/common/session.js'; @@ -765,6 +765,8 @@ class OpenSessionInTerminalAction extends Action2 { id: 'agentSession.openInTerminal', title: localize2('openInTerminal', "Open Terminal"), icon: Codicon.terminal, + // The panel is hidden while a custom view replaces the sessions grid. + precondition: CustomViewVisibleContext.negate(), toggled: { condition: SessionsTerminalViewVisibleContext, title: localize('hideTerminal', "Hide Terminal"), diff --git a/src/vs/sessions/services/customView/browser/customView.ts b/src/vs/sessions/services/customView/browser/customView.ts new file mode 100644 index 00000000000..89cfecaf0e5 --- /dev/null +++ b/src/vs/sessions/services/customView/browser/customView.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { constObservable, IObservable } from '../../../../base/common/observable.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; + +/** + * How a custom view renders the actions in its header: as an icon toolbar or as + * a row of labelled buttons. Both render the same menu, so contributed actions + * and their `when` clauses are identical either way. + */ +export type CustomViewActionsStyle = 'toolbar' | 'buttonBar'; + +export interface ICustomViewActions { + readonly style: CustomViewActionsStyle; + readonly menuId: MenuId; +} + +export interface ICustomViewDescriptor { + + /** Stable id, used by `ICustomViewService.showCustomView`. */ + readonly id: string; + + /** Title used before the view instance exists (aria, command labels). */ + readonly title: string; + + readonly ctor: SyncDescriptor; + + readonly actions?: ICustomViewActions; +} + +/** + * A full-surface view hosted in the custom view grid, in place of the sessions + * grid. The host renders the surrounding chrome (header with title, description + * and actions, plus the scroll container); a view only fills its content area + * and is disposed when it is hidden. + */ +export abstract class AbstractCustomView extends Disposable { + + /** Shown in the header. Observable so it can settle after an async load. */ + abstract readonly title: IObservable; + + /** Optional secondary line below the title. */ + readonly description: IObservable = constObservable(undefined); + + /** + * Width the content is capped to. Defaults to the same measure the session + * views use. + */ + readonly maxWidth: number | undefined = undefined; + + /** Renders the content into the host-provided container. Called once. */ + abstract render(container: HTMLElement): void; + + /** Called whenever the available content area changes. */ + abstract layout(width: number, height: number): void; + + focus(): void { } +} diff --git a/src/vs/sessions/services/customView/browser/customViewGridPartService.ts b/src/vs/sessions/services/customView/browser/customViewGridPartService.ts new file mode 100644 index 00000000000..a9e3793f16a --- /dev/null +++ b/src/vs/sessions/services/customView/browser/customViewGridPartService.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ICustomViewDescriptor } from './customView.js'; + +export const ICustomViewGridPartService = createDecorator('customViewGridPartService'); + +/** + * Renders the custom view grid part. The part is a passive renderer: the + * Agents workbench drives it from `ICustomViewService.activeCustomView` so the + * view is rendered, made visible and focused in one ordered step. + */ +export interface ICustomViewGridPartService { + + readonly _serviceBrand: undefined; + + /** Renders the given custom view, replacing (and disposing) the previous one. */ + setView(descriptor: ICustomViewDescriptor | undefined): void; + + /** Moves keyboard focus into the rendered custom view. */ + focusActiveView(): void; +} diff --git a/src/vs/sessions/services/customView/browser/customViewService.ts b/src/vs/sessions/services/customView/browser/customViewService.ts new file mode 100644 index 00000000000..d838bfe4434 --- /dev/null +++ b/src/vs/sessions/services/customView/browser/customViewService.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable, observableValue } from '../../../../base/common/observable.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { ICustomViewDescriptor } from './customView.js'; + +export const ICustomViewService = createDecorator('customViewService'); + +/** + * Owns which custom view (if any) should be rendered in place of the sessions + * grid. Only one view can be shown at a time. The Agents workbench observes + * {@link activeCustomView} and, while it is set, renders the custom view grid + * and hides the sessions grid, the side panel and the bottom panel. + */ +export interface ICustomViewService { + + readonly _serviceBrand: undefined; + + /** The view that should currently be rendered, or `undefined` for none. */ + readonly activeCustomView: IObservable; + + registerCustomView(descriptor: ICustomViewDescriptor): IDisposable; + + /** Shows the registered view with the given id, replacing any shown view. */ + showCustomView(id: string): void; + + hideCustomView(): void; +} + +export class CustomViewService extends Disposable implements ICustomViewService { + + declare readonly _serviceBrand: undefined; + + private readonly _descriptors = new Map(); + + private readonly _activeCustomView = observableValue(this, undefined); + readonly activeCustomView: IObservable = this._activeCustomView; + + constructor( + @ILogService private readonly _logService: ILogService, + ) { + super(); + } + + registerCustomView(descriptor: ICustomViewDescriptor): IDisposable { + if (this._descriptors.has(descriptor.id)) { + throw new Error(`A custom view with id '${descriptor.id}' is already registered`); + } + + this._descriptors.set(descriptor.id, descriptor); + + return toDisposable(() => { + this._descriptors.delete(descriptor.id); + if (this._activeCustomView.get() === descriptor) { + this._activeCustomView.set(undefined, undefined); + } + }); + } + + showCustomView(id: string): void { + const descriptor = this._descriptors.get(id); + if (!descriptor) { + this._logService.warn(`[CustomViewService] showCustomView: no custom view registered with id '${id}'`); + return; + } + + this._activeCustomView.set(descriptor, undefined); + } + + hideCustomView(): void { + this._activeCustomView.set(undefined, undefined); + } +} + +registerSingleton(ICustomViewService, CustomViewService, InstantiationType.Delayed); diff --git a/src/vs/sessions/services/customView/test/browser/customViewService.test.ts b/src/vs/sessions/services/customView/test/browser/customViewService.test.ts new file mode 100644 index 00000000000..09f589ebacf --- /dev/null +++ b/src/vs/sessions/services/customView/test/browser/customViewService.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 { constObservable, IObservable } from '../../../../../base/common/observable.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { AbstractCustomView, ICustomViewDescriptor } from '../../browser/customView.js'; +import { CustomViewService } from '../../browser/customViewService.js'; + +class TestCustomView extends AbstractCustomView { + readonly title: IObservable = constObservable('test'); + render(): void { } + layout(): void { } +} + +suite('Sessions - CustomViewService', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + function createService(): CustomViewService { + return disposables.add(new CustomViewService(new NullLogService())); + } + + function descriptor(id: string): ICustomViewDescriptor { + return { id, title: id, ctor: new SyncDescriptor(TestCustomView) }; + } + + test('shows, replaces and hides registered views', () => { + const service = createService(); + const first = descriptor('first'); + const second = descriptor('second'); + disposables.add(service.registerCustomView(first)); + disposables.add(service.registerCustomView(second)); + + const initial = service.activeCustomView.get(); + service.showCustomView('first'); + const shown = service.activeCustomView.get(); + service.showCustomView('second'); + const replaced = service.activeCustomView.get(); + service.hideCustomView(); + + assert.deepStrictEqual({ + initial, + shown, + replaced, + hidden: service.activeCustomView.get(), + }, { + initial: undefined, + shown: first, + replaced: second, + hidden: undefined, + }); + }); + + test('ignores an unknown id and drops the active view when it is unregistered', () => { + const service = createService(); + const registration = service.registerCustomView(descriptor('first')); + + service.showCustomView('unknown'); + const afterUnknown = service.activeCustomView.get(); + service.showCustomView('first'); + registration.dispose(); + + assert.deepStrictEqual({ + afterUnknown, + afterUnregister: service.activeCustomView.get(), + }, { + afterUnknown: undefined, + afterUnregister: undefined, + }); + }); + + test('rejects a duplicate registration', () => { + const service = createService(); + disposables.add(service.registerCustomView(descriptor('first'))); + + assert.throws(() => service.registerCustomView(descriptor('first'))); + }); +}); diff --git a/src/vs/sessions/services/sessions/browser/sessionsService.ts b/src/vs/sessions/services/sessions/browser/sessionsService.ts index d1ed7a48515..6e5c73d1b38 100644 --- a/src/vs/sessions/services/sessions/browser/sessionsService.ts +++ b/src/vs/sessions/services/sessions/browser/sessionsService.ts @@ -25,6 +25,7 @@ import { SessionsRecencyHistory } from './sessionsRecencyHistory.js'; import { VisibleSessions } from './visibleSessions.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ISessionsPartService } from './sessionsPartService.js'; +import { ICustomViewService } from '../../customView/browser/customViewService.js'; import { IsNewChatSessionContext } from '../../../common/contextkeys.js'; import { setActiveSessionContextKeys } from '../common/sessionContextKeys.js'; @@ -308,6 +309,7 @@ export class SessionsService extends Disposable implements ISessionsService { @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, @ISessionsPartService private readonly sessionsPartService: ISessionsPartService, + @ICustomViewService private readonly customViewService: ICustomViewService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, ) { @@ -586,6 +588,10 @@ export class SessionsService extends Disposable implements ISessionsService { * Cancel any in-flight open-session/restore and return a fresh cancellation token. */ private _startOpenSession(): CancellationToken { + // Opening a session is the gesture that dismisses a custom view; the + // workbench then restores the sessions grid and its side panel state. + this.customViewService.hideCustomView(); + this._openSessionCts.value?.cancel(); const cts = new CancellationTokenSource(); this._openSessionCts.value = cts; diff --git a/src/vs/sessions/services/sessions/test/browser/sessionsManagementService.test.ts b/src/vs/sessions/services/sessions/test/browser/sessionsManagementService.test.ts index 53d461e6667..0c3021862c7 100644 --- a/src/vs/sessions/services/sessions/test/browser/sessionsManagementService.test.ts +++ b/src/vs/sessions/services/sessions/test/browser/sessionsManagementService.test.ts @@ -34,6 +34,7 @@ import { SessionsManagementService } from '../../browser/sessionsManagementServi import { ISessionsManagementService, ICreateNewSessionOptions, inheritableSessionTarget, WorkspaceNotTrustedError } from '../../common/sessionsManagement.js'; import { SessionsService } from '../../browser/sessionsService.js'; import { ISessionsPartService } from '../../browser/sessionsPartService.js'; +import { CustomViewService, ICustomViewService } from '../../../customView/browser/customViewService.js'; import { ISessionsProvidersService } from '../../browser/sessionsProvidersService.js'; import { LOCAL_AGENT_HOST_PROVIDER_ID } from '../../../../common/agentHostSessionsProvider.js'; @@ -233,6 +234,7 @@ class TestSessionsPartService extends mock() { function createView(instantiationService: TestInstantiationService, service: ISessionsManagementService, disposables: ReturnType): SessionsService { instantiationService.stub(ISessionsManagementService, service); instantiationService.stub(ISessionsPartService, new TestSessionsPartService()); + instantiationService.stub(ICustomViewService, disposables.add(new CustomViewService(new NullLogService()))); return disposables.add(instantiationService.createInstance(SessionsService)); } diff --git a/src/vs/sessions/sessions.common.main.ts b/src/vs/sessions/sessions.common.main.ts index 8971c2b89cf..ef7b1fe2e92 100644 --- a/src/vs/sessions/sessions.common.main.ts +++ b/src/vs/sessions/sessions.common.main.ts @@ -454,7 +454,9 @@ import '../workbench/contrib/opener/browser/opener.contribution.js'; import './browser/paneCompositePartService.js'; import './browser/parts/editorParts.js'; import './browser/parts/sessionsParts.js'; +import './browser/parts/customViewGridParts.js'; import './services/sessions/browser/sessionsService.js'; +import './services/customView/browser/customViewService.js'; import './browser/parts/menubar.contribution.js'; import './browser/layoutActions.js'; @@ -490,6 +492,7 @@ import './contrib/workspace/browser/workspace.contribution.js'; import './contrib/aquarium/browser/aquarium.contribution.js'; import './contrib/policyBlocked/browser/policyBlocked.contribution.js'; import './contrib/automations/browser/automations.contribution.js'; +import './contrib/customViewTest/browser/customViewTest.contribution.js'; // Onboarding: the engine + spotlight presentation (from the workbench layer) and // the Agents window scenario data. diff --git a/src/vs/sessions/test/browser/workbench.test.ts b/src/vs/sessions/test/browser/workbench.test.ts index 85b22829381..9e0d27c6ca2 100644 --- a/src/vs/sessions/test/browser/workbench.test.ts +++ b/src/vs/sessions/test/browser/workbench.test.ts @@ -52,11 +52,16 @@ suite('Sessions - Workbench', () => { const isSinglePaneEditorPaneVisible = SinglePaneWorkbench.prototype.isEditorPaneVisible as (this: ITestWorkbench) => boolean; const toggleSecondarySideBarSinglePane = SinglePaneWorkbench.prototype.toggleSecondarySideBar as (this: ITestWorkbench) => void; const isSecondarySideBarVisibleSinglePane = SinglePaneWorkbench.prototype.isSecondarySideBarVisible as (this: ITestWorkbench) => boolean; + const applyCustomViewGridVisibility = Reflect.get(Workbench.prototype, '_applyCustomViewGridVisibility') as (this: ITestWorkbench, descriptor: object | undefined) => void; + const setSessionsHidden = Reflect.get(Workbench.prototype, 'setSessionsHidden') as (this: ITestWorkbench, hidden: boolean) => void; + const setPanelHidden = Reflect.get(Workbench.prototype, 'setPanelHidden') as (this: ITestWorkbench, hidden: boolean) => void; + const isVisible = Workbench.prototype.isVisible as (this: ITestWorkbench, part: Parts) => boolean; + const toggleSecondarySideBar = Workbench.prototype.toggleSecondarySideBar as (this: ITestWorkbench) => void; // --- Harness ------------------------------------------------------------ interface ITestWorkbench { - partVisibility: { sidebar: boolean; auxiliaryBar: boolean; editor: boolean; panel: boolean; sessions: boolean }; + partVisibility: { sidebar: boolean; auxiliaryBar: boolean; editor: boolean; panel: boolean; sessions: boolean; customViewGrid: boolean }; auxiliaryBarPartView: object; _savedPartSizes: { sidebar?: number; auxiliaryBar?: number; editor?: number; sessions?: number; panel?: number }; _editorMaximized: boolean; @@ -74,6 +79,13 @@ suite('Sessions - Workbench', () => { readonly counts: { save: number; layout: number }; readonly sidePaneReveals: boolean[]; readonly focusedParts: Parts[]; + readonly renderedCustomViews: (object | undefined)[]; + readonly gridVisibility: Map; + readonly focusedSessions: number; + sessionsPartView: object; + panelPartView: object; + customViewGridPartView: object; + editorPartView: object; setEditorHidden(hidden: boolean, explicit?: boolean): void; setAuxiliaryBarHidden(hidden: boolean): void; } @@ -114,6 +126,8 @@ suite('Sessions - Workbench', () => { sideBarWidth?: number; dockedWidth?: number; hasAppliedInitialEditorSplit?: boolean; + /** Use the real `setEditorMaximized` instead of the no-op stub. */ + editorMaximize?: boolean; suppressionCount?: number; focusedPart?: Parts; editorGroupService?: { mainPart: { groups: readonly { isEmpty: boolean }[] } }; @@ -129,6 +143,8 @@ suite('Sessions - Workbench', () => { const sessionsPartView = {}; const sideBarPartView = {}; const auxiliaryBarPartView = {}; + const panelPartView = {}; + const customViewGridPartView = {}; const resizes: IViewSize[] = []; const visibilityChanges: boolean[] = []; const events: IPartVisibilityChangeEvent[] = []; @@ -136,6 +152,10 @@ suite('Sessions - Workbench', () => { const counts = { save: 0, layout: 0 }; const sidePaneReveals: boolean[] = []; const focusedParts: Parts[] = []; + const renderedCustomViews: (object | undefined)[] = []; + const gridVisibility = new Map(); + let focusedSessions = 0; + const notifyPartVisibility = (view: object, visible: boolean) => notifyPartVisibilityOn(host as unknown as ITestWorkbench, view, visible); let editorNodeVisible = (options.partVisibility?.editor ?? false) || (options.partVisibility?.auxiliaryBar ?? true); const viewSizes = new Map([ [editorPartView, { width: options.editorWidth ?? 0, height: 800 }], @@ -144,12 +164,14 @@ suite('Sessions - Workbench', () => { [auxiliaryBarPartView, { width: 300, height: 800 }], ]); - const partVisibility = { sidebar: true, auxiliaryBar: true, editor: false, panel: false, sessions: true, ...options.partVisibility }; + const partVisibility = { sidebar: true, auxiliaryBar: true, editor: false, panel: false, sessions: true, customViewGrid: false, ...options.partVisibility }; const host = { editorPartView, sessionsPartView, sideBarPartView, auxiliaryBarPartView, + panelPartView, + customViewGridPartView, _editorPartContainer: undefined, mainContainer: { classList: { toggle: (name: string, force: boolean) => { classToggles.push({ name, force }); } } }, partVisibility, @@ -158,11 +180,15 @@ suite('Sessions - Workbench', () => { layout: () => { }, getViewSize: (view: object) => viewSizes.get(view) ?? { width: 0, height: 0 }, isViewVisible: (view: object) => view === editorPartView ? editorNodeVisible : true, + hasMaximizedView: () => false, + exitMaximizedView: () => { }, setViewVisible: (view: object, visible: boolean) => { if (view === editorPartView) { editorNodeVisible = visible; } + gridVisibility.set(view, visible); visibilityChanges.push(visible); + notifyPartVisibility(view, visible); }, resizeView: (view: object, size: IViewSize) => { resizes.push(size); viewSizes.set(view, size); }, }, @@ -191,12 +217,18 @@ suite('Sessions - Workbench', () => { _savePartVisibility: () => { counts.save++; }, _fireDidChangePartVisibility: (partId: Parts, visible: boolean, source?: 'resize') => { events.push({ partId, visible, ...(source ? { source } : {}) }); }, _onDidRevealSidePane: { fire: () => { sidePaneReveals.push(true); } }, + _onDidChangeEditorMaximized: { fire: () => { } }, _notifyContainerDidLayout: () => { }, _layoutDockedAuxBar: () => { counts.layout++; }, layoutMobileSidebar: () => { }, - setEditorMaximized: () => { }, + ...(options.editorMaximize ? {} : { setEditorMaximized: () => { } }), hasFocus: (part: Parts) => options.focusedPart === part, focusPart: (part: Parts) => { focusedParts.push(part); }, + layout: () => { }, + customViewGridPartService: { setView: (descriptor: object | undefined) => { renderedCustomViews.push(descriptor); }, focusActiveView: () => { } }, + _customViewVisibleKey: { set: () => { } }, + sessionsPartService: { focusSession: () => { focusedSessions++; } }, + sessionsService: { activeSession: { get: () => undefined } }, // captures resizes, visibilityChanges, @@ -205,12 +237,31 @@ suite('Sessions - Workbench', () => { counts, sidePaneReveals, focusedParts, + renderedCustomViews, + gridVisibility, + get focusedSessions() { return focusedSessions; }, }; Object.setPrototypeOf(host, options.single ? SinglePaneWorkbench.prototype : Workbench.prototype); return host as unknown as ITestWorkbench; } + // The real SplitView calls `Part.setVisible` when a view's grid visibility + // changes, which the workbench maps back onto the desired part visibility. + // Reproduce that feedback so tests catch state being overwritten by it. + function notifyPartVisibilityOn(host: ITestWorkbench, view: object, visible: boolean): void { + if ((host as unknown as { _applyingCustomViewGridVisibility: boolean })._applyingCustomViewGridVisibility) { + return; + } + if (view === host.sessionsPartView) { + setSessionsHidden.call(host, !visible); + } else if (view === host.panelPartView) { + setPanelHidden.call(host, !visible); + } else if (view === host.auxiliaryBarPartView) { + host.setAuxiliaryBarHidden(!visible); + } + } + // --- Editor split / reveal --------------------------------------------- test('tracks editor pane visibility across editor and auxiliary bar changes', () => { @@ -1485,6 +1536,7 @@ suite('Sessions - Workbench', () => { editor: false, panel: false, sessions: true, + customViewGrid: false, }, suppression: 0, }); @@ -1745,6 +1797,125 @@ suite('Sessions - Workbench', () => { }); }); + // --- Custom view grid --------------------------------------------------- + + test('showing a custom view hides the sessions grid, editor, side panel and panel', () => { + const host = createHost({ partVisibility: { editor: true, auxiliaryBar: true, panel: true, sessions: true } }); + const descriptor = {}; + + applyCustomViewGridVisibility.call(host, descriptor); + + assert.deepStrictEqual({ + renderedCustomViews: host.renderedCustomViews, + customViewGridVisible: isVisible.call(host, Parts.CUSTOM_VIEW_GRID_PART), + sessions: isVisible.call(host, Parts.SESSIONS_PART), + editor: isVisible.call(host, Parts.EDITOR_PART), + auxiliaryBar: isVisible.call(host, Parts.AUXILIARYBAR_PART), + panel: isVisible.call(host, Parts.PANEL_PART), + sideBar: isVisible.call(host, Parts.SIDEBAR_PART), + gridNodes: { + customViewGrid: host.gridVisibility.get(host.customViewGridPartView), + sessions: host.gridVisibility.get(host.sessionsPartView), + editor: host.gridVisibility.get(host.editorPartView), + panel: host.gridVisibility.get(host.panelPartView), + }, + events: host.events, + focusedParts: host.focusedParts, + }, { + renderedCustomViews: [descriptor], + customViewGridVisible: true, + sessions: false, + editor: false, + auxiliaryBar: false, + panel: false, + sideBar: true, + gridNodes: { + customViewGrid: true, + sessions: false, + editor: false, + panel: false, + }, + events: [ + { partId: Parts.SESSIONS_PART, visible: false }, + { partId: Parts.EDITOR_PART, visible: false }, + { partId: Parts.AUXILIARYBAR_PART, visible: false }, + { partId: Parts.PANEL_PART, visible: false }, + ], + focusedParts: [Parts.CUSTOM_VIEW_GRID_PART], + }); + }); + + test('hiding the custom view restores the desired part visibility, including changes made while it was shown', () => { + const host = createHost({ partVisibility: { editor: true, auxiliaryBar: true, panel: false, sessions: true } }); + + applyCustomViewGridVisibility.call(host, {}); + + // The layout controller reacts to a session switch while the custom view is + // up: the desired state changes but nothing is rendered. + setEditorHidden.call(host, true); + const whileShown = { + editor: isVisible.call(host, Parts.EDITOR_PART), + editorNode: host.gridVisibility.get(host.editorPartView), + }; + + applyCustomViewGridVisibility.call(host, undefined); + + assert.deepStrictEqual({ + whileShown, + customViewGridVisible: isVisible.call(host, Parts.CUSTOM_VIEW_GRID_PART), + renderedCustomViewCount: host.renderedCustomViews.length, + lastRenderedCustomView: host.renderedCustomViews[host.renderedCustomViews.length - 1], + sessions: isVisible.call(host, Parts.SESSIONS_PART), + editor: isVisible.call(host, Parts.EDITOR_PART), + auxiliaryBar: isVisible.call(host, Parts.AUXILIARYBAR_PART), + panel: isVisible.call(host, Parts.PANEL_PART), + focusedSessions: host.focusedSessions, + }, { + whileShown: { editor: false, editorNode: false }, + customViewGridVisible: false, + renderedCustomViewCount: 2, + lastRenderedCustomView: undefined, + sessions: true, + editor: false, + auxiliaryBar: true, + panel: false, + focusedSessions: 1, + }); + }); + + test('the secondary side bar toggle is inert while a custom view is shown', () => { + const host = createHost({ partVisibility: { auxiliaryBar: true } }); + + applyCustomViewGridVisibility.call(host, {}); + toggleSecondarySideBar.call(host); + + assert.strictEqual(host.partVisibility.auxiliaryBar, true); + }); + + test('showing a custom view un-maximizes the editor so the sessions grid owns the row again on hide', () => { + const host = createHost({ editorMaximize: true, partVisibility: { editor: true, auxiliaryBar: true, sessions: true } }); + setEditorMaximized.call(host as unknown as IMaximizeTestHarness, true); + + applyCustomViewGridVisibility.call(host, {}); + const whileShown = { + editorMaximized: host._editorMaximized, + sessions: isVisible.call(host, Parts.SESSIONS_PART), + customViewGrid: isVisible.call(host, Parts.CUSTOM_VIEW_GRID_PART), + }; + + applyCustomViewGridVisibility.call(host, undefined); + + assert.deepStrictEqual({ + whileShown, + sessions: isVisible.call(host, Parts.SESSIONS_PART), + customViewGrid: isVisible.call(host, Parts.CUSTOM_VIEW_GRID_PART), + }, { + whileShown: { editorMaximized: false, sessions: false, customViewGrid: true }, + sessions: true, + customViewGrid: false, + }); + }); + // --- Persistence gating ------------------------------------------------- test('does not restore saved desktop part visibility on phone layout', () => { diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index c860606b45f..c9e81066784 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -26,6 +26,7 @@ export const enum Parts { PANEL_PART = 'workbench.parts.panel', AUXILIARYBAR_PART = 'workbench.parts.auxiliarybar', SESSIONS_PART = 'workbench.parts.sessions', + CUSTOM_VIEW_GRID_PART = 'workbench.parts.customViewGrid', EDITOR_PART = 'workbench.parts.editor', STATUSBAR_PART = 'workbench.parts.statusbar' } diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/customViewNode.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/customViewNode.fixture.ts new file mode 100644 index 00000000000..adf23ebe8dd --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/customViewNode.fixture.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from '../../../../../base/browser/dom.js'; +import { constObservable, IObservable } from '../../../../../base/common/observable.js'; +import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; +// eslint-disable-next-line local/code-import-patterns +import { AbstractCustomView, ICustomViewDescriptor } from '../../../../../sessions/services/customView/browser/customView.js'; +// eslint-disable-next-line local/code-import-patterns +import { CustomViewNode } from '../../../../../sessions/browser/parts/customViewNode.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../fixtureUtils.js'; + +const NODE_WIDTH = 720; +const NODE_HEIGHT = 320; + +class FixtureCustomView extends AbstractCustomView { + + readonly title: IObservable; + override readonly description: IObservable; + override readonly maxWidth: number | undefined; + + constructor( + title: string, + description: string | undefined, + private readonly _itemCount: number, + maxWidth?: number, + ) { + super(); + this.title = constObservable(title); + this.description = constObservable(description); + this.maxWidth = maxWidth; + } + + render(container: HTMLElement): void { + for (let i = 0; i < this._itemCount; i++) { + container.appendChild($('div', undefined, `Item ${i + 1}`)); + } + } + + layout(): void { } +} + +export default defineThemedFixtureGroup({ path: 'sessions/' }, { + CustomViewNodeTitleOnly: defineComponentFixture({ + render: ctx => renderNode(ctx, { title: 'Automations', itemCount: 4 }), + }), + CustomViewNodeWithDescription: defineComponentFixture({ + render: ctx => renderNode(ctx, { + title: 'Automations', + description: 'Scheduled agents that run on a trigger, defined in this workspace.', + itemCount: 40, + }), + }), + CustomViewNodeNarrowMaxWidth: defineComponentFixture({ + render: ctx => renderNode(ctx, { title: 'Automations', itemCount: 4, maxWidth: 360 }), + }), +}); + +interface IFixtureOptions { + readonly title: string; + readonly description?: string; + readonly itemCount: number; + readonly maxWidth?: number; +} + +function renderNode(ctx: ComponentFixtureContext, options: IFixtureOptions): void { + const { container, disposableStore } = ctx; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: ctx.theme, + additionalServices: reg => registerWorkbenchServices(reg), + }); + + // The node reads the session-view surface colors that the hosting part sets. + container.style.width = `${NODE_WIDTH}px`; + container.style.height = `${NODE_HEIGHT}px`; + container.style.setProperty('--session-view-background', 'var(--vscode-agentsPanel-background, var(--vscode-sideBar-background))'); + container.style.setProperty('--session-view-foreground', 'var(--vscode-agentsPanel-foreground, var(--vscode-sideBar-foreground))'); + container.style.backgroundColor = 'var(--session-view-background)'; + + const descriptor: ICustomViewDescriptor = { + id: 'fixture.customView', + title: options.title, + ctor: new SyncDescriptor(FixtureCustomView, [options.title, options.description, options.itemCount, options.maxWidth]), + }; + + const node = disposableStore.add(instantiationService.createInstance(CustomViewNode, descriptor)); + node.element.style.height = '100%'; + container.appendChild(node.element); + node.layout(NODE_WIDTH, NODE_HEIGHT); +} From a84c9c4b265719bd6495b96bc91127c4cb93123d Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 30 Jul 2026 17:21:16 +0200 Subject: [PATCH 04/86] fixes --- .../parts/media/customViewGridPart.css | 7 ++- src/vs/sessions/browser/workbench.ts | 28 +++++++---- .../browser/customViewTest.contribution.ts | 27 ++-------- .../sessions/test/browser/workbench.test.ts | 50 +++++++++++++++++++ 4 files changed, 80 insertions(+), 32 deletions(-) diff --git a/src/vs/sessions/browser/parts/media/customViewGridPart.css b/src/vs/sessions/browser/parts/media/customViewGridPart.css index 956edb36215..556516e140a 100644 --- a/src/vs/sessions/browser/parts/media/customViewGridPart.css +++ b/src/vs/sessions/browser/parts/media/customViewGridPart.css @@ -103,5 +103,10 @@ box-sizing: border-box; margin: 0 auto; padding: 10px; - outline: none; +} + +/* The content is the focus fallback when a view does not focus a child of its own. */ +.custom-view-content:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; } diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 6330813577a..8688386d257 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -1776,9 +1776,11 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic // Re-run updateStyles() on pane composite parts so that // mobile Part subclasses can re-apply or clear card-chrome // inline styles based on the new `.phone-layout` class. - for (const partId of [Parts.SESSIONS_PART, Parts.SIDEBAR_PART, Parts.AUXILIARYBAR_PART, Parts.PANEL_PART]) { + for (const partId of [Parts.SESSIONS_PART, Parts.CUSTOM_VIEW_GRID_PART, Parts.SIDEBAR_PART, Parts.AUXILIARYBAR_PART, Parts.PANEL_PART]) { this.parts.get(partId)?.updateStyles(); } + + this._updateMobileCustomViewNavigation(); } this._previousViewportClass = currentClass; @@ -2341,6 +2343,8 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic private _applyCustomViewGridVisibility(descriptor: ICustomViewDescriptor | undefined): void { const visible = !!descriptor; if (this.partVisibility.customViewGrid === visible) { + // Swapping one custom view for another only changes what is rendered. + this.customViewGridPartService.setView(descriptor); return; } @@ -2376,14 +2380,21 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic this._updateExclusiveLayoutClasses(); this.mainContainer.classList.toggle(LayoutClasses.CUSTOM_VIEW_GRID_HIDDEN, !visible); - this._updateMobileCustomViewNavigation(visible); + this._updateMobileCustomViewNavigation(); + // Mirror the reveal-before-hide order of the grid updates. + if (visible) { + this._fireDidChangePartVisibility(Parts.CUSTOM_VIEW_GRID_PART, true); + } Workbench._CUSTOM_VIEW_EXCLUSIVE_PARTS.forEach((part, index) => { const nowVisible = this._effectiveVisible(part); if (nowVisible !== wasVisible[index]) { this._fireDidChangePartVisibility(part, nowVisible); } }); + if (!visible) { + this._fireDidChangePartVisibility(Parts.CUSTOM_VIEW_GRID_PART, false); + } this.layout(); @@ -2415,16 +2426,15 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic } /** Keeps the Android back button in sync with a shown custom view. */ - private _updateMobileCustomViewNavigation(visible: boolean): void { - if (this.layoutPolicy.viewportClass.get() !== 'phone') { + private _updateMobileCustomViewNavigation(): void { + const tracked = this.layoutPolicy.viewportClass.get() === 'phone' && this.partVisibility.customViewGrid; + if (tracked === this.mobileNavStack.has('customView')) { return; } - if (visible) { - if (!this.mobileNavStack.has('customView')) { - this.mobileNavStack.push('customView'); - } - } else if (this.mobileNavStack.has('customView')) { + if (tracked) { + this.mobileNavStack.push('customView'); + } else { this.mobileNavStack.popSilently('customView'); } } diff --git a/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts b/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts index 7249649926a..529a8d801bf 100644 --- a/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts +++ b/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts @@ -6,7 +6,7 @@ import './media/customViewTest.css'; import { $ } from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { constObservable, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { constObservable, IObservable } from '../../../../base/common/observable.js'; import { localize, localize2 } from '../../../../nls.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; @@ -25,36 +25,19 @@ const TEST_CUSTOM_VIEW_ID = 'sessions.customView.test'; /** Placeholder content used to exercise the custom view grid until real views exist. */ class TestCustomView extends AbstractCustomView { - private readonly _itemCount = observableValue(this, 40); + private static readonly ITEM_COUNT = 40; readonly title: IObservable = constObservable(localize('testCustomView.title', "Test Custom View")); override readonly description: IObservable = constObservable( localize('testCustomView.description', "A placeholder view used to verify the custom view grid layout, header and scrolling.")); - private _content: HTMLElement | undefined; - render(container: HTMLElement): void { - this._content = container; - this._renderItems(); + for (let i = 0; i < TestCustomView.ITEM_COUNT; i++) { + container.appendChild($('.custom-view-test-item', undefined, localize('testCustomView.item', "Item {0}", i + 1))); + } } layout(_width: number, _height: number): void { } - - addItem(): void { - this._itemCount.set(this._itemCount.get() + 10, undefined); - this._renderItems(); - } - - private _renderItems(): void { - if (!this._content) { - return; - } - - this._content.textContent = ''; - for (let i = 0; i < this._itemCount.get(); i++) { - this._content.appendChild($('.custom-view-test-item', undefined, `Item ${i + 1}`)); - } - } } class TestCustomViewContribution extends Disposable { diff --git a/src/vs/sessions/test/browser/workbench.test.ts b/src/vs/sessions/test/browser/workbench.test.ts index 9e0d27c6ca2..11a9dcc15e0 100644 --- a/src/vs/sessions/test/browser/workbench.test.ts +++ b/src/vs/sessions/test/browser/workbench.test.ts @@ -55,6 +55,7 @@ suite('Sessions - Workbench', () => { const applyCustomViewGridVisibility = Reflect.get(Workbench.prototype, '_applyCustomViewGridVisibility') as (this: ITestWorkbench, descriptor: object | undefined) => void; const setSessionsHidden = Reflect.get(Workbench.prototype, 'setSessionsHidden') as (this: ITestWorkbench, hidden: boolean) => void; const setPanelHidden = Reflect.get(Workbench.prototype, 'setPanelHidden') as (this: ITestWorkbench, hidden: boolean) => void; + const updateMobileCustomViewNavigation = Reflect.get(Workbench.prototype, '_updateMobileCustomViewNavigation') as (this: ITestWorkbench) => void; const isVisible = Workbench.prototype.isVisible as (this: ITestWorkbench, part: Parts) => boolean; const toggleSecondarySideBar = Workbench.prototype.toggleSecondarySideBar as (this: ITestWorkbench) => void; @@ -81,7 +82,9 @@ suite('Sessions - Workbench', () => { readonly focusedParts: Parts[]; readonly renderedCustomViews: (object | undefined)[]; readonly gridVisibility: Map; + readonly mobileNavLayers: string[]; readonly focusedSessions: number; + layoutPolicy: { viewportClass: { get(): string } }; sessionsPartView: object; panelPartView: object; customViewGridPartView: object; @@ -154,6 +157,7 @@ suite('Sessions - Workbench', () => { const focusedParts: Parts[] = []; const renderedCustomViews: (object | undefined)[] = []; const gridVisibility = new Map(); + const mobileNavLayers: string[] = []; let focusedSessions = 0; const notifyPartVisibility = (view: object, visible: boolean) => notifyPartVisibilityOn(host as unknown as ITestWorkbench, view, visible); let editorNodeVisible = (options.partVisibility?.editor ?? false) || (options.partVisibility?.auxiliaryBar ?? true); @@ -225,6 +229,11 @@ suite('Sessions - Workbench', () => { hasFocus: (part: Parts) => options.focusedPart === part, focusPart: (part: Parts) => { focusedParts.push(part); }, layout: () => { }, + mobileNavStack: { + has: (layer: string) => mobileNavLayers.includes(layer), + push: (layer: string) => { mobileNavLayers.push(layer); }, + popSilently: (layer: string) => { mobileNavLayers.splice(mobileNavLayers.indexOf(layer), 1); }, + }, customViewGridPartService: { setView: (descriptor: object | undefined) => { renderedCustomViews.push(descriptor); }, focusActiveView: () => { } }, _customViewVisibleKey: { set: () => { } }, sessionsPartService: { focusSession: () => { focusedSessions++; } }, @@ -239,6 +248,7 @@ suite('Sessions - Workbench', () => { focusedParts, renderedCustomViews, gridVisibility, + mobileNavLayers, get focusedSessions() { return focusedSessions; }, }; @@ -1836,6 +1846,7 @@ suite('Sessions - Workbench', () => { panel: false, }, events: [ + { partId: Parts.CUSTOM_VIEW_GRID_PART, visible: true }, { partId: Parts.SESSIONS_PART, visible: false }, { partId: Parts.EDITOR_PART, visible: false }, { partId: Parts.AUXILIARYBAR_PART, visible: false }, @@ -1883,6 +1894,45 @@ suite('Sessions - Workbench', () => { }); }); + test('swapping to another custom view re-renders it without touching the layout', () => { + const host = createHost({ partVisibility: { editor: true, auxiliaryBar: true, sessions: true } }); + const first = {}; + const second = {}; + + applyCustomViewGridVisibility.call(host, first); + const eventsAfterShow = host.events.length; + applyCustomViewGridVisibility.call(host, second); + + assert.deepStrictEqual({ + renderedCustomViews: host.renderedCustomViews, + customViewGridVisible: isVisible.call(host, Parts.CUSTOM_VIEW_GRID_PART), + sessions: isVisible.call(host, Parts.SESSIONS_PART), + eventsAfterSwap: host.events.length - eventsAfterShow, + }, { + renderedCustomViews: [first, second], + customViewGridVisible: true, + sessions: false, + eventsAfterSwap: 0, + }); + }); + + test('tracks the custom view in the phone navigation stack and drops it when leaving phone layout', () => { + const host = createHost(); + host.layoutPolicy.viewportClass.get = () => 'phone'; + + applyCustomViewGridVisibility.call(host, {}); + const onPhone = [...host.mobileNavLayers]; + + // Rotating back to a desktop-class viewport must not leave a stale entry behind. + host.layoutPolicy.viewportClass.get = () => 'desktop'; + updateMobileCustomViewNavigation.call(host); + + assert.deepStrictEqual({ onPhone, afterLeavingPhone: host.mobileNavLayers }, { + onPhone: ['customView'], + afterLeavingPhone: [], + }); + }); + test('the secondary side bar toggle is inert while a custom view is shown', () => { const host = createHost({ partVisibility: { auxiliaryBar: true } }); From d9d6728092f8be8bba69bbbb5cc97beb953ae465 Mon Sep 17 00:00:00 2001 From: Benjamin Steenhoek Date: Thu, 30 Jul 2026 10:57:55 -0500 Subject: [PATCH 05/86] Set default NES aggressiveness to medium (#327049) --- .../common/userInteractionMonitor.spec.ts | 42 +++++++++++-- .../xtab/test/node/xtabProvider.spec.ts | 60 ++++++++++++++++++- .../common/configurationService.ts | 2 +- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/extensions/copilot/src/extension/inlineEdits/test/common/userInteractionMonitor.spec.ts b/extensions/copilot/src/extension/inlineEdits/test/common/userInteractionMonitor.spec.ts index 60f6990aace..54547474407 100644 --- a/extensions/copilot/src/extension/inlineEdits/test/common/userInteractionMonitor.spec.ts +++ b/extensions/copilot/src/extension/inlineEdits/test/common/userInteractionMonitor.spec.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { beforeEach, describe, expect, test } from 'vitest'; -import { ConfigKey } from '../../../../platform/configuration/common/configurationService'; +import { ConfigKey, ExperimentBasedConfig, ExperimentBasedConfigType } from '../../../../platform/configuration/common/configurationService'; import { DefaultsOnlyConfigurationService } from '../../../../platform/configuration/common/defaultsOnlyConfigurationService'; import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService'; -import { AggressivenessLevel, DEFAULT_USER_HAPPINESS_SCORE_CONFIGURATION, UserHappinessScoreConfiguration } from '../../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions'; +import { AggressivenessLevel, AggressivenessSetting, DEFAULT_USER_HAPPINESS_SCORE_CONFIGURATION, UserHappinessScoreConfiguration } from '../../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions'; import { ILogService } from '../../../../platform/log/common/logService'; import { IExperimentationService, NullExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService'; import { NullTelemetryService } from '../../../../platform/telemetry/common/nullTelemetryService'; @@ -47,9 +47,22 @@ class TestUserInteractionMonitor extends UserInteractionMonitor { * Mock configuration service that allows setting specific config values for testing. */ class MockConfigurationService extends InMemoryConfigurationService { + private _useAdaptiveAggressiveness = false; + constructor() { super(new DefaultsOnlyConfigurationService()); } + + useAdaptiveAggressiveness(): void { + this._useAdaptiveAggressiveness = true; + } + + override getExperimentBasedConfig(key: ExperimentBasedConfig, experimentationService: IExperimentationService): T { + if (this._useAdaptiveAggressiveness && key === ConfigKey.TeamInternal.InlineEditsXtabAggressivenessLevel) { + return undefined as T; + } + return super.getExperimentBasedConfig(key, experimentationService); + } } interface TelemetryCall { @@ -197,13 +210,25 @@ describe('UserInteractionMonitor', () => { }); describe('aggressiveness level calculation', () => { - test('returns neutral aggressiveness with no history', () => { - // With no data, score is 0.5, which is between low and medium thresholds for the default config - const level = monitor.getAggressivenessLevel().aggressivenessLevel; - expect(level).toBe(AggressivenessLevel.Medium); + test('defaults to medium aggressiveness without using adaptive scoring', () => { + expect(monitor.getAggressivenessLevel()).toEqual({ + aggressivenessLevel: AggressivenessLevel.Medium, + userHappinessScore: undefined, + }); + }); + + test('explicit user eagerness takes priority over configured aggressiveness', () => { + configurationService.setConfig(ConfigKey.Advanced.InlineEditsAggressiveness, AggressivenessSetting.High); + configurationService.setConfig(ConfigKey.TeamInternal.InlineEditsXtabAggressivenessLevel, AggressivenessLevel.Low); + + expect(monitor.getAggressivenessLevel()).toEqual({ + aggressivenessLevel: AggressivenessLevel.High, + userHappinessScore: undefined, + }); }); test('returns high aggressiveness after many acceptances', () => { + configurationService.useAdaptiveAggressiveness(); // Fill with 10 acceptances for (let i = 0; i < 10; i++) { monitor.handleAcceptance(); @@ -214,6 +239,7 @@ describe('UserInteractionMonitor', () => { }); test('returns low aggressiveness after many rejections', () => { + configurationService.useAdaptiveAggressiveness(); // Fill with 10 rejections for (let i = 0; i < 10; i++) { monitor.handleRejection(); @@ -239,6 +265,7 @@ describe('UserInteractionMonitor', () => { }); test('recent actions have more weight than older ones', () => { + configurationService.useAdaptiveAggressiveness(); // Start with acceptances, end with rejections for (let i = 0; i < 5; i++) { monitor.handleAcceptance(); @@ -270,6 +297,7 @@ describe('UserInteractionMonitor', () => { describe('ignored action limiting', () => { test('ignored actions are included in aggressiveness calculation', () => { + configurationService.useAdaptiveAggressiveness(); // With custom config that includes ignored actions const customConfig: UserHappinessScoreConfiguration = { ...DEFAULT_USER_HAPPINESS_SCORE_CONFIGURATION, @@ -294,6 +322,7 @@ describe('UserInteractionMonitor', () => { }); test('total ignored limit is respected', () => { + configurationService.useAdaptiveAggressiveness(); const customConfig: UserHappinessScoreConfiguration = { ...DEFAULT_USER_HAPPINESS_SCORE_CONFIGURATION, includeIgnored: true, @@ -325,6 +354,7 @@ describe('UserInteractionMonitor', () => { let mockTelemetryService: MockTelemetryService; beforeEach(() => { + configurationService.useAdaptiveAggressiveness(); mockTelemetryService = new MockTelemetryService(); monitor = new TestUserInteractionMonitor(configurationService, experimentationService, logService, mockTelemetryService); }); diff --git a/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts b/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts index b835a18866d..525543dc72d 100644 --- a/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts +++ b/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts @@ -14,7 +14,7 @@ import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/do import { Edits } from '../../../../platform/inlineEdits/common/dataTypes/edit'; import { ImportChanges } from '../../../../platform/inlineEdits/common/dataTypes/importFilteringOptions'; import { LanguageId } from '../../../../platform/inlineEdits/common/dataTypes/languageId'; -import { DEFAULT_OPTIONS, EarlyDivergenceCancellationMode, LanguageContextLanguages, LintOptionShowCode, LintOptionWarning, ModelConfiguration, PatchModelPrediction, PromptingStrategy, ResponseFormat } from '../../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions'; +import { AggressivenessLevel, DEFAULT_OPTIONS, EarlyDivergenceCancellationMode, LanguageContextLanguages, LintOptionShowCode, LintOptionWarning, ModelConfiguration, PatchModelPrediction, PromptingStrategy, ResponseFormat } from '../../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions'; import { InlineEditRequestLogContext } from '../../../../platform/inlineEdits/common/inlineEditLogContext'; import { IInlineEditsModelService } from '../../../../platform/inlineEdits/common/inlineEditsModelService'; import { NoNextEditReason, StatelessNextEditDocument, StatelessNextEditRequest, StreamedEdit, WithStatelessProviderTelemetry } from '../../../../platform/inlineEdits/common/statelessNextEditProvider'; @@ -1047,6 +1047,40 @@ describe('XtabProvider integration', () => { expect(getMessageText(systemMessage!)).toBe(xtab275SystemPrompt); }); + it('applies configured aggressiveness only to aggressiveness strategies', async () => { + const lines = ['const x = 1;', 'const y = 2;']; + const captureUserPrompt = async (promptingStrategy: PromptingStrategy, aggressivenessLevel: AggressivenessLevel) => { + mockModelService.setSelectedConfig({ promptingStrategy }); + await configService.setConfig(ConfigKey.TeamInternal.InlineEditsXtabAggressivenessLevel, aggressivenessLevel); + streamingFetcher.setStreamingLines(lines); + + const gen = createProvider().provideNextEdit(createRequestWithEdit(lines, { insertionOffset: 3, insertedText: 'a' }), createMockLogger(), createLogContext(), CancellationToken.None); + await AsyncIterUtils.drainUntilReturn(gen); + + const messages = streamingFetcher.capturedOptions.at(-1)?.messages; + const userMessage = messages?.find(message => message.role === Raw.ChatRole.User); + expect(userMessage).toBeDefined(); + return getMessageText(userMessage!); + }; + + const nonAggressiveLow = await captureUserPrompt(PromptingStrategy.Xtab275, AggressivenessLevel.Low); + const nonAggressiveHigh = await captureUserPrompt(PromptingStrategy.Xtab275, AggressivenessLevel.High); + const aggressiveLow = await captureUserPrompt(PromptingStrategy.XtabAggressiveness, AggressivenessLevel.Low); + const aggressiveHigh = await captureUserPrompt(PromptingStrategy.XtabAggressiveness, AggressivenessLevel.High); + + expect({ + nonAggressivePromptsMatch: nonAggressiveLow === nonAggressiveHigh, + nonAggressivePromptHasLevel: nonAggressiveLow.includes('<|aggressive|>'), + aggressiveLowHasLevel: aggressiveLow.includes('<|aggressive|>low<|/aggressive|>'), + aggressiveHighHasLevel: aggressiveHigh.includes('<|aggressive|>high<|/aggressive|>'), + }).toEqual({ + nonAggressivePromptsMatch: true, + nonAggressivePromptHasLevel: false, + aggressiveLowHasLevel: true, + aggressiveHighHasLevel: true, + }); + }); + it('retries with default model after NotFound response', async () => { const provider = createProvider(); @@ -1977,6 +2011,30 @@ describe('XtabProvider integration', () => { // ======================================================================== describe('debounce behavior', () => { + it('does not change timing for a non-aggressiveness strategy when user eagerness is default', async () => { + mockModelService.setSelectedConfig({ promptingStrategy: PromptingStrategy.Xtab275 }); + const setBaseDebounceTime = vi.spyOn(DelaySession.prototype, 'setBaseDebounceTime'); + const setExpectedTotalTime = vi.spyOn(DelaySession.prototype, 'setExpectedTotalTime'); + + try { + const lines = ['const x = 1;', 'const y = 2;']; + streamingFetcher.setStreamingLines(lines); + const gen = createProvider().provideNextEdit(createRequestWithEdit(lines, { insertionOffset: 3, insertedText: 'a' }), createMockLogger(), createLogContext(), CancellationToken.None); + await AsyncIterUtils.drainUntilReturn(gen); + + expect({ + setBaseDebounceTimeCalls: setBaseDebounceTime.mock.calls.length, + setExpectedTotalTimeCalls: setExpectedTotalTime.mock.calls.length, + }).toEqual({ + setBaseDebounceTimeCalls: 0, + setExpectedTotalTimeCalls: 0, + }); + } finally { + setBaseDebounceTime.mockRestore(); + setExpectedTotalTime.mockRestore(); + } + }); + it('debounce is skipped in simulation tests', async () => { // Override the simulation test context to indicate we're in sim tests const testingServiceCollection = createExtensionUnitTestingServices(disposables); diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index ada9b967f22..9b819f82618 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -907,7 +907,7 @@ export namespace ConfigKey { export const InlineEditsXtabOnlyMergeConflictLines = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.onlyMergeConflictLines', ConfigType.ExperimentBased, false); export const InlineEditsXtabDuplicateAdditionsMode = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.diffPatch.duplicateAdditionsMode', ConfigType.ExperimentBased, DuplicateAdditionsMode.Off, DuplicateAdditionsMode.VALIDATOR); export const InlineEditsXtabSplitPatchOnDiff = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.diffPatch.splitOnDiff', ConfigType.ExperimentBased, false, vBoolean()); - export const InlineEditsXtabAggressivenessLevel = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.aggressivenessLevel', ConfigType.ExperimentBased, undefined); + export const InlineEditsXtabAggressivenessLevel = defineTeamInternalSetting('chat.advanced.inlineEdits.xtabProvider.aggressivenessLevel', ConfigType.ExperimentBased, xtabPromptOptions.AggressivenessLevel.Medium); export const InlineEditsAggressivenessLowMinResponseTimeMs = defineTeamInternalSetting('chat.advanced.inlineEdits.aggressiveness.lowMinResponseTimeMs', ConfigType.ExperimentBased, 1500); export const InlineEditsAggressivenessMediumMinResponseTimeMs = defineTeamInternalSetting('chat.advanced.inlineEdits.aggressiveness.mediumMinResponseTimeMs', ConfigType.ExperimentBased, 700); export const InlineEditsAggressivenessHighDebounceMs = defineTeamInternalSetting('chat.advanced.inlineEdits.aggressiveness.highDebounceMs', ConfigType.ExperimentBased, 0); From bd773464cb07080db45ceb6a6b400608af2dc586 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 30 Jul 2026 12:31:11 -0400 Subject: [PATCH 06/86] Refine Voice Mode and dictation context menus (#328216) * Refine microphone context menus Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: db94f3da-b5b5-4265-b2f6-f4dd41a4527c * Apply suggestion from @meganrogge * Apply suggestion from @meganrogge * Apply suggestion from @meganrogge * Apply suggestion from @meganrogge --------- Copilot-Session: db94f3da-b5b5-4265-b2f6-f4dd41a4527c --- .../speechToText/micButtonMenuActions.ts | 81 +++++++++++-------- .../test/browser/micButtonMenuActions.test.ts | 49 +++++++++++ 2 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/micButtonMenuActions.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/micButtonMenuActions.ts b/src/vs/workbench/contrib/chat/browser/speechToText/micButtonMenuActions.ts index b1ee7c2a47a..60116a95917 100644 --- a/src/vs/workbench/contrib/chat/browser/speechToText/micButtonMenuActions.ts +++ b/src/vs/workbench/contrib/chat/browser/speechToText/micButtonMenuActions.ts @@ -5,7 +5,7 @@ import { addDisposableListener, getWindow } from '../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; -import { IAction, toAction } from '../../../../../base/common/actions.js'; +import { IAction, Separator, toAction } from '../../../../../base/common/actions.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../nls.js'; import { createConfigureKeybindingAction } from '../../../../../platform/actions/common/menuService.js'; @@ -24,11 +24,15 @@ const CANCEL_DICTATION_COMMAND = 'workbench.action.chat.cancelSpeechToText'; const VOICE_DISCONNECT_COMMAND = 'agentsVoice.disconnect'; /** Command that opens the Voice Mode settings; the affordance that used to live behind the toolbar gear. */ const VOICE_OPEN_SETTINGS_COMMAND = 'agentsVoice.openSettings'; +/** Command that opens the Settings editor. */ +const OPEN_SETTINGS_COMMAND = 'workbench.action.openSettings'; +/** Narrows the Settings editor to dictation settings. */ +const DICTATION_SETTINGS_QUERY = 'dictation'; /** Command that shows the Voice Mode onboarding card again. */ export const SHOW_VOICE_MODE_ONBOARDING_COMMAND = 'agentsVoice.showOnboarding'; -/** Setting that enables dictation; toggled off by "Disable Dictation". */ +/** Setting that enables dictation; toggled off by "Disable". */ const DICTATION_ENABLED_SETTING = 'dictation.enabled'; -/** Setting that enables Voice Mode; toggled off by "Disable Voice Mode". */ +/** Setting that enables Voice Mode; toggled off by "Disable". */ const VOICE_ENABLED_SETTING = 'agents.voice.enabled'; /** @@ -44,14 +48,14 @@ function createSelectMicrophoneAction(commandService: ICommandService): IAction } /** - * "Disable Dictation" entry. Cancels any active/preparing dictation first so + * "Disable" entry for dictation. Cancels any active/preparing dictation first so * disabling the setting doesn't leave the microphone capturing while the toolbar * affordance disappears, then turns off the feature setting. */ function createDisableDictationAction(commandService: ICommandService, configurationService: IConfigurationService): IAction { return toAction({ id: 'chat.dictation.disable', - label: localize('dictation.disable', "Disable Dictation"), + label: localize('dictation.disable', "Disable"), run: async () => { await commandService.executeCommand(CANCEL_DICTATION_COMMAND); await configurationService.updateValue(DICTATION_ENABLED_SETTING, false); @@ -68,14 +72,14 @@ function createShowDictationOnboardingAction(commandService: ICommandService): I } /** - * "Disable Voice Mode" entry. Tears down any active session first so disabling + * "Disable" entry for Voice Mode. Tears down any active session first so disabling * the setting doesn't leave the microphone capturing while the toolbar * affordance disappears, then turns off the feature setting. */ function createDisableVoiceModeAction(commandService: ICommandService, configurationService: IConfigurationService): IAction { return toAction({ id: 'chat.voiceMode.disable', - label: localize('voiceMode.disable', "Disable Voice Mode"), + label: localize('voiceMode.disable', "Disable"), run: async () => { await commandService.executeCommand(VOICE_DISCONNECT_COMMAND); await configurationService.updateValue(VOICE_ENABLED_SETTING, false); @@ -84,23 +88,26 @@ function createDisableVoiceModeAction(commandService: ICommandService, configura } /** - * Actions for the dictation mic button context menu: "Configure Keybinding" - * (always enabled so a removed binding can be restored), "Select Microphone" - * and "Disable Dictation". `keybindingCommandId` is the stable command the - * keybinding entry targets. + * Actions for the dictation mic button context menu. Keybinding and feature + * disabling are grouped separately from configuration and onboarding. */ export function getDictationContextMenuActions(commandService: ICommandService, configurationService: IConfigurationService, keybindingService: IKeybindingService, keybindingCommandId: string): IAction[] { - return [ - createConfigureKeybindingAction(commandService, keybindingService, keybindingCommandId), - createConfigureInstructionsAction(commandService, CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID, localize('dictation.configureInstructions', "Configure Dictation Instructions")), - createShowDictationOnboardingAction(commandService), - createSelectMicrophoneAction(commandService), - createDisableDictationAction(commandService, configurationService), - ]; + return Separator.join( + [ + createConfigureKeybindingAction(commandService, keybindingService, keybindingCommandId), + createDisableDictationAction(commandService, configurationService), + ], + [ + createDictationSettingsAction(commandService), + createConfigureInstructionsAction(commandService, CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID, localize('dictation.configureInstructions', "Configure Instructions")), + createShowDictationOnboardingAction(commandService), + createSelectMicrophoneAction(commandService), + ], + ); } /** - * "Voice Mode Settings" entry. Opens the Voice Mode settings — the affordance + * "Settings" entry. Opens the Voice Mode settings — the affordance * that used to live behind the toolbar gear button. */ function createVoiceModeSettingsAction(commandService: ICommandService): IAction { @@ -111,6 +118,14 @@ function createVoiceModeSettingsAction(commandService: ICommandService): IAction }); } +function createDictationSettingsAction(commandService: ICommandService): IAction { + return toAction({ + id: 'chat.dictation.openSettings', + label: localize('dictation.openSettings', "Open Settings"), + run: () => commandService.executeCommand(OPEN_SETTINGS_COMMAND, { query: DICTATION_SETTINGS_QUERY }), + }); +} + function createShowVoiceModeOnboardingAction(commandService: ICommandService): IAction { return toAction({ id: SHOW_VOICE_MODE_ONBOARDING_COMMAND, @@ -128,22 +143,22 @@ function createConfigureInstructionsAction(commandService: ICommandService, comm } /** - * Actions for the Voice Mode mic button context menu, mirroring - * {@link getDictationContextMenuActions} but with "Disable Voice Mode". The - * "Configure Keybinding" entry opens the keybindings editor scoped to the Voice - * Mode keybinding and "Voice Mode Settings" opens the Voice Mode settings — the - * affordances that used to live behind the toolbar gear button. - * `keybindingCommandId` is the stable command the keybinding entry targets. + * Actions for the Voice Mode mic button context menu. Keybinding and feature + * disabling are grouped separately from configuration and onboarding. */ export function getVoiceModeContextMenuActions(commandService: ICommandService, configurationService: IConfigurationService, keybindingService: IKeybindingService, keybindingCommandId: string): IAction[] { - return [ - createConfigureKeybindingAction(commandService, keybindingService, keybindingCommandId), - createVoiceModeSettingsAction(commandService), - createConfigureInstructionsAction(commandService, CONFIGURE_VOICE_INSTRUCTIONS_ACTION_ID, localize('voiceMode.configureInstructions', "Configure Voice Mode Instructions")), - createShowVoiceModeOnboardingAction(commandService), - createSelectMicrophoneAction(commandService), - createDisableVoiceModeAction(commandService, configurationService), - ]; + return Separator.join( + [ + createConfigureKeybindingAction(commandService, keybindingService, keybindingCommandId), + createDisableVoiceModeAction(commandService, configurationService), + ], + [ + createVoiceModeSettingsAction(commandService), + createConfigureInstructionsAction(commandService, CONFIGURE_VOICE_INSTRUCTIONS_ACTION_ID, localize('voiceMode.configureInstructions', "Configure Instructions")), + createShowVoiceModeOnboardingAction(commandService), + createSelectMicrophoneAction(commandService), + ], + ); } /** diff --git a/src/vs/workbench/contrib/chat/test/browser/micButtonMenuActions.test.ts b/src/vs/workbench/contrib/chat/test/browser/micButtonMenuActions.test.ts new file mode 100644 index 00000000000..a3f1684ad19 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/micButtonMenuActions.test.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { upcastPartial } from '../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import { getDictationContextMenuActions, getVoiceModeContextMenuActions } from '../../browser/speechToText/micButtonMenuActions.js'; + +suite('Mic button menu actions', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const commandService = upcastPartial({}); + const configurationService = upcastPartial({}); + const keybindingService = upcastPartial({}); + + test('groups and shortens Voice Mode actions', () => { + const actions = getVoiceModeContextMenuActions(commandService, configurationService, keybindingService, 'voice.start'); + + assert.deepStrictEqual(actions.map(action => action.label), [ + 'Configure Keybinding', + 'Disable', + '', + 'Open Settings', + 'Configure Instructions', + 'Show Introduction', + 'Select Microphone', + ]); + }); + + test('groups and shortens dictation actions', () => { + const actions = getDictationContextMenuActions(commandService, configurationService, keybindingService, 'dictation.start'); + + assert.deepStrictEqual(actions.map(action => action.label), [ + 'Configure Keybinding', + 'Disable', + '', + 'Open Settings', + 'Configure Instructions', + 'Show Introduction', + 'Select Microphone', + ]); + }); +}); From 18c7959c4ea67e287f6ff4d2c3ac50cc1fff6b0a Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 30 Jul 2026 18:33:49 +0200 Subject: [PATCH 07/86] fixes --- build/lib/i18n.resources.json | 4 ++++ .../contrib/layout/test/browser/layoutControllerTestUtils.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index d56fe189549..acfd99d583d 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -692,6 +692,10 @@ "name": "vs/sessions/contrib/codeReview", "project": "vscode-sessions" }, + { + "name": "vs/sessions/contrib/customViewTest", + "project": "vscode-sessions" + }, { "name": "vs/sessions/contrib/fileTreeView", "project": "vscode-sessions" diff --git a/src/vs/sessions/contrib/layout/test/browser/layoutControllerTestUtils.ts b/src/vs/sessions/contrib/layout/test/browser/layoutControllerTestUtils.ts index c897959d39e..de1dd5daf5a 100644 --- a/src/vs/sessions/contrib/layout/test/browser/layoutControllerTestUtils.ts +++ b/src/vs/sessions/contrib/layout/test/browser/layoutControllerTestUtils.ts @@ -274,6 +274,7 @@ export function createTestHarness(store: DisposableStore, options: ICreateOption [Parts.AUXILIARYBAR_PART, true], [Parts.PANEL_PART, false], [Parts.EDITOR_PART, true], + [Parts.CUSTOM_VIEW_GRID_PART, false], ...(options.initialPartVisibility ?? []), ]), openedViewContainers: [], From 52d13c08d03ae4edc15347c853591868deea1f9f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 31 Jul 2026 02:35:35 +1000 Subject: [PATCH 08/86] agentHost: discover Claude customizations across workspace roots (#328207) * Initiall * agentHost: parallelize Claude customization scans Preserve deterministic scope and settings precedence while reading independent customization roots concurrently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: fix Claude multi-root discovery edge cases Run independent scans concurrently while preserving user-scope attribution and primary-root behavior for cached plugin hooks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: preserve Claude single-root discovery behavior Route one-root sessions through the original scanners and watcher lifecycle while reserving multi-root precedence and concurrency for sessions with additional directories. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: clarify Claude customization scope buckets Document how URI-backed buckets distinguish multiple workspace roots from user scope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../node/claude/claudeAgentSession.ts | 43 +++++--- .../claudeCustomizationPolicy.ts | 42 +++++++ .../claudeMultiRootCustomizationDiscovery.ts | 72 ++++++++++++ .../claudeSessionCustomizationDiscovery.ts | 104 ++++++++++-------- .../scan/claudeAgentSkillScan.ts | 39 ++++--- .../scan/claudeNativePluginScan.ts | 58 +++++++++- .../agentHost/test/node/claudeAgent.test.ts | 32 ++++++ ...udeMultiRootCustomizationDiscovery.test.ts | 100 +++++++++++++++++ ...laudeSessionCustomizationDiscovery.test.ts | 70 ++++++++++++ .../scan/claudeNativePluginScan.test.ts | 63 ++++++++++- src/vs/sessions/AI_CUSTOMIZATIONS.md | 2 + 11 files changed, 542 insertions(+), 83 deletions(-) create mode 100644 src/vs/platform/agentHost/node/claude/customizations/claudeCustomizationPolicy.ts create mode 100644 src/vs/platform/agentHost/node/claude/customizations/claudeMultiRootCustomizationDiscovery.ts create mode 100644 src/vs/platform/agentHost/test/node/customizations/claudeMultiRootCustomizationDiscovery.test.ts diff --git a/src/vs/platform/agentHost/node/claude/claudeAgentSession.ts b/src/vs/platform/agentHost/node/claude/claudeAgentSession.ts index 472f46a51ab..05e5babd86f 100644 --- a/src/vs/platform/agentHost/node/claude/claudeAgentSession.ts +++ b/src/vs/platform/agentHost/node/claude/claudeAgentSession.ts @@ -7,7 +7,7 @@ import type { McpSdkServerConfigWithInstance, OnElicitation, Options, Permission import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { isEqual } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { INativeEnvironmentService } from '../../../environment/common/environment.js'; @@ -36,12 +36,11 @@ import { SessionClientToolsDiff } from './clientTools/claudeSessionClientToolsMo import { SessionClientCustomizationsDiff } from './customizations/claudeSessionClientCustomizationsModel.js'; import { ClaudeCustomizationWatcher, buildDiscoveredCustomizations, resolveClaudeAgentName } from './customizations/claudeSessionCustomizationDiscovery.js'; import { applyMcpServerEnablement, findMcpChildId, findMcpServerName, getEffectiveMcpServerCustomizations } from '../shared/mcpCustomizationController.js'; -import { scanClaudeDiskCustomizations } from './customizations/scan/claudeAgentSkillScan.js'; import { scanClaudeHooks } from './customizations/scan/claudeHookScan.js'; import { scanClaudeMcpServers } from './customizations/scan/claudeMcpScan.js'; -import { scanClaudeNativePlugins } from './customizations/scan/claudeNativePluginScan.js'; import { AgentHostStateManager, IAgentHostStateManager } from '../agentHostStateManager.js'; import { scanClaudeRules } from './customizations/scan/claudeRuleScan.js'; +import { discoverClaudeMultiRootCustomizations } from './customizations/claudeMultiRootCustomizationDiscovery.js'; import { resolvePromptToContentBlocks } from './claudePromptResolver.js'; import type { ClaudeTransport } from './claudeProxyService.js'; import { ClaudeSdkPipeline, IRematerializer, type ISdkResolvedCustomizations } from './claudeSdkPipeline.js'; @@ -97,6 +96,16 @@ function resolveCurrentPermissionMode( return readClaudePermissionMode(configurationService, sessionUri) ?? permissionModeFallback; } +function sameWorkingDirectories(a: readonly URI[] | undefined, b: readonly URI[] | undefined): boolean { + if (!a || !b) { + return a === b; + } + if (a.length !== b.length) { + return false; + } + return a.every((directory, index) => isEqual(directory, b[index])); +} + /** * Per-session coordinator. Owns: * • Per-session identity (sessionId / sessionUri / workspace / @@ -177,7 +186,7 @@ export class ClaudeAgentSession extends Disposable { const primary = this.workingDirectory; return primary ? [primary, ...this._additionalDirectories] : undefined; } - private readonly _customizationWatcher = this._register(new DisposableStore()); + private readonly _customizationWatcher = this._register(new MutableDisposable()); /** Exposed for the materializer's MCP-server build closure. */ get pendingClientToolCalls(): PendingRequestRegistry { return this._pendingClientToolCalls; } @@ -385,18 +394,19 @@ export class ClaudeAgentSession extends Disposable { this.toolDiff = this._register(toolDiff); this._register(this.clientCustomizationsDiff.onDidChange(() => this._onDidCustomizationsChange.fire())); - this._watchCustomizations(this.workspace); + this._watchCustomizations(this.workingDirectories); } - private _watchCustomizations(directory: URI | undefined): void { - this._customizationWatcher.clear(); - const watcher = this._customizationWatcher.add(new ClaudeCustomizationWatcher( - directory, + private _watchCustomizations(directories: readonly URI[] | undefined): void { + const store = new DisposableStore(); + const watcher = store.add(new ClaudeCustomizationWatcher( + directories, this._environmentService.userHome, this._fileService, this._logService, )); - this._customizationWatcher.add(watcher.onDidChange(() => this._onDidCustomizationsChange.fire())); + store.add(watcher.onDidChange(() => this._onDidCustomizationsChange.fire())); + this._customizationWatcher.value = store; } /** @@ -470,14 +480,18 @@ export class ClaudeAgentSession extends Disposable { // roots) takes precedence and also refreshes the additional-directory // tail; the singular `workingDirectory` stays supported for single-root // callers that only resolve the primary. + const previousWorkingDirectories = this.workingDirectories; const resolvedPrimary = ctx.workingDirectories?.[0] ?? ctx.workingDirectory; if (resolvedPrimary && !isEqual(resolvedPrimary, this.workingDirectory)) { this._workingDirectory = resolvedPrimary; - this._watchCustomizations(resolvedPrimary); } if (ctx.workingDirectories && ctx.workingDirectories.length > 0) { this._additionalDirectories = ctx.workingDirectories.slice(1); } + const currentWorkingDirectories = this.workingDirectories; + if (!sameWorkingDirectories(previousWorkingDirectories, currentWorkingDirectories)) { + this._watchCustomizations(currentWorkingDirectories); + } if (!this.workingDirectory) { throw new Error(`Cannot materialize Claude session ${this.sessionId}: workingDirectory is required`); } @@ -1057,12 +1071,11 @@ export class ClaudeAgentSession extends Disposable { async getSessionCustomizations(): Promise { const { synced } = this.clientCustomizationsDiff.model.state.get(); const userHome = this._environmentService.userHome; - const [discovered, rules, mcpServers, hooks, nativePlugins] = await Promise.all([ - scanClaudeDiskCustomizations(this.workingDirectory, userHome, this._fileService), + const [multiRoot, rules, mcpServers, hooks] = await Promise.all([ + discoverClaudeMultiRootCustomizations(this.workingDirectories, userHome, this._fileService, this._logService), scanClaudeRules(this.workingDirectory, userHome, this._fileService), scanClaudeMcpServers(this.workingDirectory, userHome, this._fileService), scanClaudeHooks(this.workingDirectory, userHome, this._fileService), - scanClaudeNativePlugins(this.workingDirectory, userHome, this._fileService, this._logService), ]); // Post-materialize, the live SDK snapshot filters the disk set down to @@ -1082,7 +1095,7 @@ export class ClaudeAgentSession extends Disposable { // `buildDiscoveredCustomizations` also folds in the read-only "Built-in" // surfacing (curated pre-materialize, SDK-derived post-materialize) for // both agents and skills, so the SDK-vs-curated decision lives in one place. - const discoveredCustomizations = buildDiscoveredCustomizations([...discovered, ...rules], mcpServers, hooks, nativePlugins, this.workingDirectory, userHome, sdk); + const discoveredCustomizations = buildDiscoveredCustomizations([...multiRoot.discovered, ...rules], mcpServers, hooks, multiRoot.nativePlugins, multiRoot.workingDirectories, userHome, sdk); // Final projection: the client-pushed tier first, then the discovered // tier, with session MCP enablement applied to both. diff --git a/src/vs/platform/agentHost/node/claude/customizations/claudeCustomizationPolicy.ts b/src/vs/platform/agentHost/node/claude/customizations/claudeCustomizationPolicy.ts new file mode 100644 index 00000000000..2f4e3b1bf96 --- /dev/null +++ b/src/vs/platform/agentHost/node/claude/customizations/claudeCustomizationPolicy.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isEqualOrParent } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; + +export function selectFirstClaudeCustomizationByKey(groups: readonly (readonly T[])[], keyOf: (item: T) => string): readonly T[] { + const selected = new Map(); + for (const group of groups) { + for (const item of group) { + const key = keyOf(item); + if (!selected.has(key)) { + selected.set(key, item); + } + } + } + return [...selected.values()]; +} + +export function selectEnabledClaudePluginIds(groups: readonly ReadonlyMap[]): readonly string[] { + const selected = new Map(); + for (const group of groups) { + for (const [id, enabled] of group) { + if (!selected.has(id)) { + selected.set(id, enabled); + } + } + } + return [...selected].filter(([, enabled]) => enabled).map(([id]) => id); +} + +export function findMostSpecificClaudeWorkspaceRoot(resource: URI, workingDirectories: readonly URI[]): URI | undefined { + let result: URI | undefined; + for (const directory of workingDirectories) { + if (resource.scheme === directory.scheme && isEqualOrParent(resource, directory) && (!result || directory.path.length > result.path.length)) { + result = directory; + } + } + return result; +} diff --git a/src/vs/platform/agentHost/node/claude/customizations/claudeMultiRootCustomizationDiscovery.ts b/src/vs/platform/agentHost/node/claude/customizations/claudeMultiRootCustomizationDiscovery.ts new file mode 100644 index 00000000000..8210cd1124a --- /dev/null +++ b/src/vs/platform/agentHost/node/claude/customizations/claudeMultiRootCustomizationDiscovery.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResourceSet } from '../../../../../base/common/map.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IFileService } from '../../../../files/common/files.js'; +import { ILogService } from '../../../../log/common/log.js'; +import { CustomizationType } from '../../../common/state/protocol/channels-session/state.js'; +import type { IParsedAgent, IParsedSkill } from '../../../../agentPlugins/common/pluginParsers.js'; +import { scanClaudeCustomizationScope, scanClaudeDiskCustomizations } from './scan/claudeAgentSkillScan.js'; +import { scanClaudeNativePlugins, scanClaudeNativePluginsForRoots, type IResolvedNativePlugin } from './scan/claudeNativePluginScan.js'; +import { selectFirstClaudeCustomizationByKey } from './claudeCustomizationPolicy.js'; + +export interface IClaudeMultiRootCustomizations { + readonly workingDirectories: readonly URI[]; + readonly discovered: readonly (IParsedAgent | IParsedSkill)[]; + readonly nativePlugins: readonly IResolvedNativePlugin[]; +} + +export function distinctClaudeWorkingDirectories(workingDirectories: readonly URI[] | undefined): readonly URI[] { + const seen = new ResourceSet(); + const result: URI[] = []; + for (const directory of workingDirectories ?? []) { + if (!seen.has(directory)) { + seen.add(directory); + result.push(directory); + } + } + return result; +} + +function isParsedAgent(item: IParsedAgent | IParsedSkill): item is IParsedAgent { + return item.customization.type === CustomizationType.Agent; +} + +function isParsedSkill(item: IParsedAgent | IParsedSkill): item is IParsedSkill { + return item.customization.type === CustomizationType.Skill; +} + +export async function discoverClaudeMultiRootCustomizations( + workingDirectories: readonly URI[] | undefined, + userHome: URI, + fileService: IFileService, + logService: ILogService, +): Promise { + const roots = distinctClaudeWorkingDirectories(workingDirectories); + if (roots.length <= 1) { + const [discovered, nativePlugins] = await Promise.all([ + scanClaudeDiskCustomizations(roots[0], userHome, fileService), + scanClaudeNativePlugins(roots[0], userHome, fileService, logService), + ]); + return { workingDirectories: roots, discovered, nativePlugins }; + } + const [scopes, nativePlugins] = await Promise.all([ + Promise.all([ + ...roots.map((root, index) => scanClaudeCustomizationScope(root, fileService, index === 0)), + scanClaudeCustomizationScope(userHome, fileService), + ]), + scanClaudeNativePluginsForRoots(roots, userHome, fileService, logService), + ]); + const discovered = [ + ...selectFirstClaudeCustomizationByKey(scopes.map(items => items.filter(isParsedAgent)), item => item.name), + ...selectFirstClaudeCustomizationByKey(scopes.map(items => items.filter(isParsedSkill)), item => item.name), + ]; + return { + workingDirectories: roots, + discovered, + nativePlugins, + }; +} diff --git a/src/vs/platform/agentHost/node/claude/customizations/claudeSessionCustomizationDiscovery.ts b/src/vs/platform/agentHost/node/claude/customizations/claudeSessionCustomizationDiscovery.ts index 53e92dc5ff2..061026de091 100644 --- a/src/vs/platform/agentHost/node/claude/customizations/claudeSessionCustomizationDiscovery.ts +++ b/src/vs/platform/agentHost/node/claude/customizations/claudeSessionCustomizationDiscovery.ts @@ -18,6 +18,8 @@ import { deriveMcpState } from './scan/claudeMcpScan.js'; import { claudeMemoryFiles } from './scan/claudeRuleScan.js'; import type { IResolvedNativePlugin } from './scan/claudeNativePluginScan.js'; import { CLAUDE_BUILTIN_AGENTS, buildClaudeBuiltinSkillsContainer, buildSdkBuiltinSkillsContainer } from './claudeBuiltinCommands.js'; +import { distinctClaudeWorkingDirectories } from './claudeMultiRootCustomizationDiscovery.js'; +import { findMostSpecificClaudeWorkspaceRoot } from './claudeCustomizationPolicy.js'; /** * The Claude SDK's built-in default agent. Hidden from the picker: @@ -87,24 +89,27 @@ function makePlugin(plugin: IResolvedNativePlugin): PluginCustomization { } /** - * The scope a discovered customization belongs to, derived from which - * `.claude/` tree contains its source file. + * A URI-backed scope bucket. The base URI distinguishes workspace A, + * workspace B, and user scope without a separate scope enum. */ -const enum ClaudeCustomizationScope { - Workspace = 'workspace', - User = 'user', +interface ICustomizationBucket { + readonly base: URI; + readonly agents: AgentCustomization[]; + readonly skills: SkillCustomization[]; + readonly rules: RuleCustomization[]; + readonly hooks: HookCustomization[]; } -/** - * Attributes a discovered file to the scope whose `.claude/` directory - * contains it. SDK-only (`claude-internal:`) and any out-of-tree URIs fall - * back to the user scope. Drives per-scope grouping so the workbench can - * label containers "Workspace" vs "User". - */ -function scopeOf(uri: URI, workingDirectory: URI | undefined): ClaudeCustomizationScope { - return workingDirectory && uri.scheme === workingDirectory.scheme && isEqualOrParent(uri, workingDirectory) - ? ClaudeCustomizationScope.Workspace - : ClaudeCustomizationScope.User; +function createBucket(base: URI): ICustomizationBucket { + return { base, agents: [], skills: [], rules: [], hooks: [] }; +} + +function findCustomizationBucket(uri: URI, workspaceBuckets: readonly ICustomizationBucket[], userBucket: ICustomizationBucket): ICustomizationBucket { + const root = findMostSpecificClaudeWorkspaceRoot(uri, workspaceBuckets.map(bucket => bucket.base)); + if (workspaceBuckets.length > 1 && uri.scheme === userBucket.base.scheme && isEqualOrParent(uri, userBucket.base) && (!root || userBucket.base.path.length > root.path.length)) { + return userBucket; + } + return workspaceBuckets.find(bucket => bucket.base === root) ?? userBucket; } /** @@ -122,15 +127,14 @@ export function mapDiscoveredCustomizations( mcpServers: readonly McpServerCustomization[], hooks: readonly HookCustomization[], nativePlugins: readonly IResolvedNativePlugin[], - workingDirectory: URI | undefined, + workingDirectories: readonly URI[] | URI | undefined, userHome: URI, ): readonly Customization[] { - const buckets = new Map([ - [ClaudeCustomizationScope.Workspace, { agents: [], skills: [], rules: [], hooks: [] }], - [ClaudeCustomizationScope.User, { agents: [], skills: [], rules: [], hooks: [] }], - ]); + const roots = distinctClaudeWorkingDirectories(Array.isArray(workingDirectories) ? workingDirectories : workingDirectories ? [workingDirectories] : []); + const workspaceBuckets = roots.map(createBucket); + const userBucket = createBucket(userHome); for (const d of discovered) { - const bucket = buckets.get(scopeOf(d.uri, workingDirectory))!; + const bucket = findCustomizationBucket(d.uri, workspaceBuckets, userBucket); if (d.customization.type === CustomizationType.Agent) { bucket.agents.push(d.customization); } else if (d.customization.type === CustomizationType.Skill) { @@ -143,32 +147,22 @@ export function mapDiscoveredCustomizations( // carry no `IParsed*` wrapper, so attribute them to scope via their source // settings-file uri. for (const hook of hooks) { - buckets.get(scopeOf(URI.parse(hook.uri), workingDirectory))!.hooks.push(hook); + findCustomizationBucket(URI.parse(hook.uri), workspaceBuckets, userBucket).hooks.push(hook); } const result: Customization[] = []; - // Workspace containers first (precedence), then user. `base` is the scope - // root the container `.claude/` uri is built from. - const orderedScopes: readonly (readonly [ClaudeCustomizationScope, URI | undefined])[] = [ - [ClaudeCustomizationScope.Workspace, workingDirectory], - [ClaudeCustomizationScope.User, userHome], - ]; - for (const [scope, base] of orderedScopes) { - if (!base) { - continue; - } - const bucket = buckets.get(scope)!; + for (const bucket of [...workspaceBuckets, userBucket]) { if (bucket.agents.length > 0) { - result.push(makeDirectory(base, 'agents', CustomizationType.Agent, bucket.agents)); + result.push(makeDirectory(bucket.base, 'agents', CustomizationType.Agent, bucket.agents)); } if (bucket.skills.length > 0) { - result.push(makeDirectory(base, 'skills', CustomizationType.Skill, bucket.skills)); + result.push(makeDirectory(bucket.base, 'skills', CustomizationType.Skill, bucket.skills)); } if (bucket.rules.length > 0) { - result.push(makeDirectory(base, 'rules', CustomizationType.Rule, bucket.rules)); + result.push(makeDirectory(bucket.base, 'rules', CustomizationType.Rule, bucket.rules)); } if (bucket.hooks.length > 0) { - result.push(makeDirectory(base, 'hooks', CustomizationType.Hook, bucket.hooks)); + result.push(makeDirectory(bucket.base, 'hooks', CustomizationType.Hook, bucket.hooks)); } } @@ -278,7 +272,7 @@ export function buildDiscoveredCustomizations( mcpServers: readonly McpServerCustomization[], hooks: readonly HookCustomization[], nativePlugins: readonly IResolvedNativePlugin[], - workingDirectory: URI | undefined, + workingDirectories: readonly URI[] | URI | undefined, userHome: URI, sdk: ISdkResolvedCustomizations | undefined, ): readonly Customization[] { @@ -350,7 +344,7 @@ export function buildDiscoveredCustomizations( const builtinAgents = CLAUDE_BUILTIN_AGENTS .filter(a => a.name !== CLAUDE_SDK_DEFAULT_AGENT_NAME && !diskAgentNames.has(a.name)) .map(a => toParsedAgent({ uri: nonEditableUri('agent', a.name), name: a.name, description: a.description() })); - return withBuiltinSkills(mapDiscoveredCustomizations([...discovered, ...builtinAgents], mcpServers, hooks, nativePlugins, workingDirectory, userHome)); + return withBuiltinSkills(mapDiscoveredCustomizations([...discovered, ...builtinAgents], mcpServers, hooks, nativePlugins, workingDirectories, userHome)); } const agentNames = new Set(sdk.agents.map(a => a.name)); @@ -430,7 +424,7 @@ export function buildDiscoveredCustomizations( // Native plugins were matched to the live SDK set at the top of this // function (`visiblePlugins`); surface them as top-level containers. - return withBuiltinSkills(mapDiscoveredCustomizations(entries, servers, hooks, visiblePlugins, workingDirectory, userHome)); + return withBuiltinSkills(mapDiscoveredCustomizations(entries, servers, hooks, visiblePlugins, workingDirectories, userHome)); } /** @@ -479,7 +473,7 @@ export class ClaudeCustomizationWatcher extends Disposable { readonly onDidChange: Event; constructor( - workingDirectory: URI | undefined, + workingDirectories: readonly URI[] | URI | undefined, userHome: URI, fileService: IFileService, logService: ILogService, @@ -487,9 +481,16 @@ export class ClaudeCustomizationWatcher extends Disposable { ) { super(); + const roots = distinctClaudeWorkingDirectories(Array.isArray(workingDirectories) ? workingDirectories : workingDirectories ? [workingDirectories] : []); // URIs whose subtree (or exact file, for `.mcp.json`) signals a re-scan. const triggers: URI[] = []; + const watched = new Set(); const watch = (uri: URI, recursive: boolean) => { + const key = `${recursive}:${uri.toString()}`; + if (watched.has(key)) { + return; + } + watched.add(key); try { this._register(fileService.watch(uri, { recursive, excludes: [] })); } catch (err) { @@ -506,12 +507,23 @@ export class ClaudeCustomizationWatcher extends Disposable { } }; - if (workingDirectory) { - const projectClaude = URI.joinPath(workingDirectory, '.claude'); + const primary = roots[0]; + if (primary) { + const projectClaude = URI.joinPath(primary, '.claude'); watch(projectClaude, true); addClaudeTriggers(projectClaude); - watch(workingDirectory, false); - triggers.push(URI.joinPath(workingDirectory, '.mcp.json')); + watch(primary, false); + triggers.push(URI.joinPath(primary, '.mcp.json')); + } + for (const additional of roots.slice(1)) { + const projectClaude = URI.joinPath(additional, '.claude'); + watch(projectClaude, true); + triggers.push( + URI.joinPath(projectClaude, 'agents'), + URI.joinPath(projectClaude, 'skills'), + URI.joinPath(projectClaude, 'settings.json'), + URI.joinPath(projectClaude, 'settings.local.json'), + ); } const userClaude = URI.joinPath(userHome, '.claude'); watch(userClaude, true); @@ -521,7 +533,7 @@ export class ClaudeCustomizationWatcher extends Disposable { // canonical list so the watcher never drifts from what it actually // reads. Entries already under a recursively-watched `.claude` root // (e.g. `.claude/CLAUDE.md`) are harmless duplicate triggers. - triggers.push(...claudeMemoryFiles(workingDirectory, userHome)); + triggers.push(...claudeMemoryFiles(primary, userHome)); // Collapse the raw file-change stream into a single debounced signal. // The `DisposableStore` argument is required because `onDidChange` is a diff --git a/src/vs/platform/agentHost/node/claude/customizations/scan/claudeAgentSkillScan.ts b/src/vs/platform/agentHost/node/claude/customizations/scan/claudeAgentSkillScan.ts index ffa9c863836..bb748df4172 100644 --- a/src/vs/platform/agentHost/node/claude/customizations/scan/claudeAgentSkillScan.ts +++ b/src/vs/platform/agentHost/node/claude/customizations/scan/claudeAgentSkillScan.ts @@ -7,6 +7,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { dirname } from '../../../../../../base/common/resources.js'; import { IFileService } from '../../../../../files/common/files.js'; import { detectPluginFormat, readAgentComponents, readSkills, toParsedAgent, toParsedSkill, type INamedPluginResource, type IParsedAgent, type IParsedSkill } from '../../../../../agentPlugins/common/pluginParsers.js'; +import { CustomizationType } from '../../../../common/state/protocol/channels-session/state.js'; /** * The `.claude/` directories one scope (project or user) contributes @@ -54,6 +55,26 @@ async function excludeNativePluginSkills(skills: readonly INamedPluginResource[] return skills.filter((_, i) => !isPluginDir[i]); } +export async function scanClaudeCustomizationScope( + scope: URI, + fileService: IFileService, + includeCommands: boolean = true, +): Promise { + const { agents: agentsDir, skills: skillsDir, commands: commandsDir } = scopeRoots(scope); + const [agentResources, skillResources, commandResources] = await Promise.all([ + readAgentComponents([agentsDir], fileService), + readSkills(skillsDir, [skillsDir], fileService), + includeCommands ? readAgentComponents([commandsDir], fileService) : [], + ]); + const agents = new Map(); + const skills = new Map(); + collectByName(agents, agentResources.map(toParsedAgent)); + const standaloneSkills = await excludeNativePluginSkills(skillResources, fileService); + collectByName(skills, standaloneSkills.map(toParsedSkill)); + collectByName(skills, commandResources.map(toParsedSkill)); + return [...agents.values(), ...skills.values()]; +} + /** * Scans a Claude session's `.claude/{agents,skills,commands}` directories * (project + user scope) and returns the discovered customizations with @@ -84,21 +105,9 @@ export async function scanClaudeDiskCustomizations( const skills = new Map(); for (const scope of scopes) { - const { agents: agentsDir, skills: skillsDir, commands: commandsDir } = scopeRoots(scope); - const [agentRes, skillRes, commandRes] = await Promise.all([ - readAgentComponents([agentsDir], fileService), - // pluginRoot = the skills dir itself, so the readSkills fallback - // targets `/SKILL.md` (a legit single-skill dir), never - // an unrelated `/SKILL.md`. - readSkills(skillsDir, [skillsDir], fileService), - readAgentComponents([commandsDir], fileService), - ]); - collectByName(agents, agentRes.map(toParsedAgent)); - // Skills before commands so a same-named skill wins (spec section 3). - // Drop `@skills-dir` plugin dirs first — they surface as plugins (PB-8). - const standaloneSkills = await excludeNativePluginSkills(skillRes, fileService); - collectByName(skills, standaloneSkills.map(toParsedSkill)); - collectByName(skills, commandRes.map(toParsedSkill)); + const discovered = await scanClaudeCustomizationScope(scope, fileService); + collectByName(agents, discovered.filter((item): item is IParsedAgent => item.customization.type === CustomizationType.Agent)); + collectByName(skills, discovered.filter((item): item is IParsedSkill => item.customization.type === CustomizationType.Skill)); } return [...agents.values(), ...skills.values()]; diff --git a/src/vs/platform/agentHost/node/claude/customizations/scan/claudeNativePluginScan.ts b/src/vs/platform/agentHost/node/claude/customizations/scan/claudeNativePluginScan.ts index 6ca52c8c1d8..70d0608192a 100644 --- a/src/vs/platform/agentHost/node/claude/customizations/scan/claudeNativePluginScan.ts +++ b/src/vs/platform/agentHost/node/claude/customizations/scan/claudeNativePluginScan.ts @@ -8,6 +8,7 @@ import { ResourceSet } from '../../../../../../base/common/map.js'; import { IFileService } from '../../../../../files/common/files.js'; import { ILogService } from '../../../../../log/common/log.js'; import { detectPluginFormat, parsePlugin, readJsonFile, type IParsedPlugin } from '../../../../../agentPlugins/common/pluginParsers.js'; +import { findMostSpecificClaudeWorkspaceRoot, selectEnabledClaudePluginIds } from '../claudeCustomizationPolicy.js'; /** * A Claude-native plugin enabled via `enabledPlugins` and resolved to its @@ -46,6 +47,22 @@ function claudeSettingsFilesByPrecedence(workingDirectory: URI | undefined, user return files; } +async function readEnabledPlugins(uri: URI, fileService: IFileService): Promise> { + const result = new Map(); + const raw = await readJsonFile(uri, fileService); + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { + return result; + } + const enabledPlugins = (raw as Record)['enabledPlugins']; + if (!enabledPlugins || typeof enabledPlugins !== 'object' || Array.isArray(enabledPlugins)) { + return result; + } + for (const [id, value] of Object.entries(enabledPlugins as Record)) { + result.set(id, value !== false); + } + return result; +} + /** * Computes the effective set of enabled plugin ids across the settings * scopes. A plugin's value may be `true`, a `string[]` (version @@ -57,8 +74,6 @@ async function resolveEnabledPluginIds(workingDirectory: URI | undefined, userHo const seenFiles = new ResourceSet(); for (const uri of claudeSettingsFilesByPrecedence(workingDirectory, userHome)) { if (seenFiles.has(uri)) { - // The same settings file can be reached from two scopes (cwd === - // userHome) — read it once. Mirrors the per-scanner dedupe. continue; } seenFiles.add(uri); @@ -114,9 +129,9 @@ async function hasManifest(dir: URI, fileService: IFileService): Promise { +async function resolveSkillsDirRoot(plugin: string, workingDirectories: readonly URI[], userHome: URI, fileService: IFileService): Promise { const candidates: URI[] = []; - if (workingDirectory) { + for (const workingDirectory of workingDirectories) { candidates.push(URI.joinPath(workingDirectory, '.claude', 'skills', plugin)); } candidates.push(URI.joinPath(userHome, '.claude', 'skills', plugin)); @@ -189,6 +204,34 @@ export async function scanClaudeNativePlugins( logService: ILogService, ): Promise { const ids = await resolveEnabledPluginIds(workingDirectory, userHome, fileService); + return resolveNativePlugins(ids, workingDirectory ? [workingDirectory] : [], userHome, fileService, logService); +} + +export async function scanClaudeNativePluginsForRoots( + workingDirectories: readonly URI[], + userHome: URI, + fileService: IFileService, + logService: ILogService, +): Promise { + const settingsFiles: URI[] = []; + for (const workingDirectory of workingDirectories) { + settingsFiles.push( + URI.joinPath(workingDirectory, '.claude', 'settings.local.json'), + URI.joinPath(workingDirectory, '.claude', 'settings.json'), + ); + } + settingsFiles.push(URI.joinPath(userHome, '.claude', 'settings.json')); + const ids = selectEnabledClaudePluginIds(await Promise.all(settingsFiles.map(uri => readEnabledPlugins(uri, fileService)))); + return resolveNativePlugins(ids, workingDirectories, userHome, fileService, logService); +} + +async function resolveNativePlugins( + ids: readonly string[], + workingDirectories: readonly URI[], + userHome: URI, + fileService: IFileService, + logService: ILogService, +): Promise { const result: IResolvedNativePlugin[] = []; const seenRoots = new ResourceSet(); for (const id of ids) { @@ -198,7 +241,7 @@ export async function scanClaudeNativePlugins( continue; } const root = parts.marketplace === SKILLS_DIR_MARKETPLACE - ? await resolveSkillsDirRoot(parts.plugin, workingDirectory, userHome, fileService) + ? await resolveSkillsDirRoot(parts.plugin, workingDirectories, userHome, fileService) : await resolveMarketplaceCacheRoot(parts.plugin, parts.marketplace, userHome, fileService); if (!root) { logService.warn(`[claudeNativePluginScan] could not resolve an on-disk root for enabled plugin '${id}'`); @@ -209,7 +252,10 @@ export async function scanClaudeNativePlugins( } seenRoots.add(root); try { - const parsed = await parsePlugin(root, fileService, workingDirectory, userHome, root); + const workspaceRoot = parts.marketplace === SKILLS_DIR_MARKETPLACE + ? findMostSpecificClaudeWorkspaceRoot(root, workingDirectories) + : undefined; + const parsed = await parsePlugin(root, fileService, workspaceRoot ?? workingDirectories[0], userHome, root); result.push({ id, root, parsed }); } catch (err) { logService.warn(`[claudeNativePluginScan] failed to parse plugin '${id}' at '${root.toString()}': ${err instanceof Error ? err.message : String(err)}`); diff --git a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts index ef3f0c37060..f9163b92101 100644 --- a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts @@ -2273,6 +2273,38 @@ suite('ClaudeAgent', () => { }); }); + test('multi-root session discovers and retains customizations from an additional directory', async () => { + const { agent, sdk, fileService } = createTestContext(disposables, { rootConfig: { [AgentHostClaudeMultiRootEnabledConfigKey]: true } }); + await agent.authenticate(GITHUB_COPILOT_PROTECTED_RESOURCE.resource, 'tok'); + const repoA = URI.file('/repo-a'); + const repoB = URI.file('/repo-b'); + const skillUri = URI.joinPath(repoB, '.claude', 'skills', 'from-b', 'SKILL.md'); + await fileService.writeFile(skillUri, VSBuffer.fromString('---\nname: from-b\ndescription: Skill from B\n---\nbody')); + const created = await agent.createSession({ workingDirectories: [repoA, repoB] }); + const before = await agent.getSessionCustomizations(created.session); + const sessionId = AgentSession.id(created.session); + sdk.supportedAgentsResult = []; + sdk.supportedCommandsResult = [{ name: 'from-b', description: 'Skill from B', argumentHint: '' }]; + sdk.mcpServerStatusResult = []; + sdk.nextQueryMessages = [makeSystemInitMessage(sessionId), makeResultSuccess(sessionId)]; + + await agent.chats.sendMessage(defaultChatUri(created.session), 'hi', [repoA, repoB], undefined, 'turn-1'); + const after = await agent.getSessionCustomizations(created.session); + const skillContainerUri = URI.joinPath(repoB, '.claude', 'skills').toString(); + const names = (customizations: readonly Customization[]) => { + const container = customizations.find(customization => customization.uri === skillContainerUri); + return container?.type === CustomizationType.Directory ? container.children?.map(skill => skill.name) : undefined; + }; + + assert.deepStrictEqual({ + before: names(before), + after: names(after), + }, { + before: ['from-b'], + after: ['from-b'], + }); + }); + test('cold resume recovers the additional directories from the persisted overlay', async () => { const database = new TestSessionDatabase(); const repoA = URI.file('/repo-a'); diff --git a/src/vs/platform/agentHost/test/node/customizations/claudeMultiRootCustomizationDiscovery.test.ts b/src/vs/platform/agentHost/test/node/customizations/claudeMultiRootCustomizationDiscovery.test.ts new file mode 100644 index 00000000000..965a0c09aeb --- /dev/null +++ b/src/vs/platform/agentHost/test/node/customizations/claudeMultiRootCustomizationDiscovery.test.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { IFileService } from '../../../../files/common/files.js'; +import { NullLogService } from '../../../../log/common/log.js'; +import { discoverClaudeMultiRootCustomizations } from '../../../node/claude/customizations/claudeMultiRootCustomizationDiscovery.js'; +import { scanClaudeDiskCustomizations } from '../../../node/claude/customizations/scan/claudeAgentSkillScan.js'; +import { scanClaudeNativePlugins } from '../../../node/claude/customizations/scan/claudeNativePluginScan.js'; +import { createInMemoryFileService, seedFile } from './claudeCustomizationTestUtils.js'; + +suite('claudeMultiRootCustomizationDiscovery', () => { + const disposables = new DisposableStore(); + const rootA = URI.from({ scheme: Schemas.inMemory, path: '/a' }); + const rootB = URI.from({ scheme: Schemas.inMemory, path: '/b' }); + const userHome = URI.from({ scheme: Schemas.inMemory, path: '/home' }); + let fileService: IFileService; + const seed = (path: string, content = '') => seedFile(fileService, path, content); + + setup(() => { + fileService = createInMemoryFileService(disposables); + }); + + teardown(() => disposables.clear()); + ensureNoDisposablesAreLeakedInTestSuite(); + + test('uses the existing single-root discovery path without changing output order', async () => { + await Promise.all([ + seed('/a/.claude/agents/project.md', '---\nname: project\ndescription: project agent\n---'), + seed('/home/.claude/agents/user.md', '---\nname: user\ndescription: user agent\n---'), + seed('/a/.claude/skills/project-skill/SKILL.md', '---\nname: project-skill\ndescription: project skill\n---'), + seed('/home/.claude/skills/user-skill/SKILL.md', '---\nname: user-skill\ndescription: user skill\n---'), + seed('/home/.claude/settings.json', JSON.stringify({ enabledPlugins: { 'user-plugin@m': true } })), + seed('/a/.claude/settings.json', JSON.stringify({ enabledPlugins: { 'project-plugin@m': true } })), + seed('/home/.claude/plugins/cache/m/user-plugin/1.0.0/.claude-plugin/plugin.json', JSON.stringify({ name: 'user-plugin' })), + seed('/home/.claude/plugins/cache/m/project-plugin/1.0.0/.claude-plugin/plugin.json', JSON.stringify({ name: 'project-plugin' })), + ]); + const logService = new NullLogService(); + const [expectedDiscovered, expectedPlugins, actual] = await Promise.all([ + scanClaudeDiskCustomizations(rootA, userHome, fileService), + scanClaudeNativePlugins(rootA, userHome, fileService, logService), + discoverClaudeMultiRootCustomizations([rootA], userHome, fileService, logService), + ]); + + assert.deepStrictEqual({ + discovered: actual.discovered, + plugins: actual.nativePlugins, + }, { + discovered: expectedDiscovered, + plugins: expectedPlugins, + }); + }); + + test('combines roots in order and applies first-name-wins precedence', async () => { + await Promise.all([ + seed('/a/.claude/agents/shared.md', '---\nname: shared\ndescription: from a\n---'), + seed('/b/.claude/agents/shared.md', '---\nname: shared\ndescription: from b\n---'), + seed('/b/.claude/agents/b-only.md', '---\nname: b-only\ndescription: from b\n---'), + seed('/b/.claude/skills/shared-skill/SKILL.md', '---\nname: shared-skill\ndescription: from b\n---'), + seed('/home/.claude/skills/shared-skill/SKILL.md', '---\nname: shared-skill\ndescription: from user\n---'), + seed('/home/.claude/skills/user-only/SKILL.md', '---\nname: user-only\ndescription: from user\n---'), + seed('/b/.claude/commands/not-loaded.md', '---\nname: not-loaded\ndescription: added-directory command\n---'), + ]); + + const result = await discoverClaudeMultiRootCustomizations([rootA, rootB], userHome, fileService, new NullLogService()); + + assert.deepStrictEqual({ + roots: result.workingDirectories.map(root => root.path), + items: result.discovered.map(item => ({ name: item.name, description: item.description, path: item.uri.path })), + }, { + roots: ['/a', '/b'], + items: [ + { name: 'shared', description: 'from a', path: '/a/.claude/agents/shared.md' }, + { name: 'b-only', description: 'from b', path: '/b/.claude/agents/b-only.md' }, + { name: 'shared-skill', description: 'from b', path: '/b/.claude/skills/shared-skill/SKILL.md' }, + { name: 'user-only', description: 'from user', path: '/home/.claude/skills/user-only/SKILL.md' }, + ], + }); + }); + + test('deduplicates equivalent roots without changing precedence', async () => { + await seed('/a/.claude/agents/a.md', '---\nname: a\ndescription: A\n---'); + + const result = await discoverClaudeMultiRootCustomizations([rootA, rootA], userHome, fileService, new NullLogService()); + + assert.deepStrictEqual({ + roots: result.workingDirectories.map(root => root.path), + items: result.discovered.map(item => item.name), + }, { + roots: ['/a'], + items: ['a'], + }); + }); +}); diff --git a/src/vs/platform/agentHost/test/node/customizations/claudeSessionCustomizationDiscovery.test.ts b/src/vs/platform/agentHost/test/node/customizations/claudeSessionCustomizationDiscovery.test.ts index d4a90a663d8..7b1c9f0a6f0 100644 --- a/src/vs/platform/agentHost/test/node/customizations/claudeSessionCustomizationDiscovery.test.ts +++ b/src/vs/platform/agentHost/test/node/customizations/claudeSessionCustomizationDiscovery.test.ts @@ -53,6 +53,57 @@ suite('claudeSessionCustomizationDiscovery', () => { ensureNoDisposablesAreLeakedInTestSuite(); suite('mapDiscoveredCustomizations', () => { + test('maps agents and skills into separate ordered workspace-root containers', () => { + const workspaceB = URI.from({ scheme: Schemas.inMemory, path: '/workspace/packages/b' }); + const rootAgent = URI.joinPath(workspace, '.claude', 'agents', 'root.md'); + const nestedAgent = URI.joinPath(workspaceB, '.claude', 'agents', 'nested.md'); + const result = mapDiscoveredCustomizations([ + toParsedAgent({ uri: rootAgent, name: 'root' }), + toParsedAgent({ uri: nestedAgent, name: 'nested' }), + ], [], [], [], [workspace, workspaceB], userHome); + + assert.deepStrictEqual( + (result.filter(c => c.type === CustomizationType.Directory) as DirectoryCustomization[]) + .map(directory => ({ uri: directory.uri, children: directory.children?.map(child => child.name) })), + [ + { uri: URI.joinPath(workspace, '.claude', 'agents').toString(), children: ['root'] }, + { uri: URI.joinPath(workspaceB, '.claude', 'agents').toString(), children: ['nested'] }, + ], + ); + }); + + test('keeps user customizations in the user bucket when an additional root contains userHome', () => { + const broadRoot = URI.from({ scheme: Schemas.inMemory, path: '/home' }); + const userSkill = URI.joinPath(userHome, '.claude', 'skills', 'user-skill', 'SKILL.md'); + const result = mapDiscoveredCustomizations([ + toParsedSkill({ uri: userSkill, name: 'user-skill' }), + ], [], [], [], [workspace, broadRoot], userHome); + + assert.deepStrictEqual( + (result.filter(c => c.type === CustomizationType.Directory) as DirectoryCustomization[]) + .map(directory => ({ uri: directory.uri, children: directory.children?.map(child => child.name) })), + [ + { uri: URI.joinPath(userHome, '.claude', 'skills').toString(), children: ['user-skill'] }, + ], + ); + }); + + test('preserves single-root workspace attribution when the workspace contains userHome', () => { + const broadRoot = URI.from({ scheme: Schemas.inMemory, path: '/home' }); + const userSkill = URI.joinPath(userHome, '.claude', 'skills', 'user-skill', 'SKILL.md'); + const result = mapDiscoveredCustomizations([ + toParsedSkill({ uri: userSkill, name: 'user-skill' }), + ], [], [], [], broadRoot, userHome); + + assert.deepStrictEqual( + (result.filter(c => c.type === CustomizationType.Directory) as DirectoryCustomization[]) + .map(directory => ({ uri: directory.uri, children: directory.children?.map(child => child.name) })), + [ + { uri: URI.joinPath(broadRoot, '.claude', 'skills').toString(), children: ['user-skill'] }, + ], + ); + }); + test('maps discovered entries into per-scope Directory containers with real child URIs + top-level MCP', () => { const wsAgentUri = URI.from({ scheme: Schemas.inMemory, path: '/workspace/.claude/agents/wa.md' }); const wsSkillUri = URI.from({ scheme: Schemas.inMemory, path: '/workspace/.claude/skills/ws/SKILL.md' }); @@ -395,6 +446,25 @@ suite('claudeSessionCustomizationDiscovery', () => { assert.strictEqual(fires, 1); }); + test('watches agents, skills, and plugin settings under additional roots', async () => { + const workspaceB = URI.from({ scheme: Schemas.inMemory, path: '/workspace-b' }); + const watcher = disposables.add(new ClaudeCustomizationWatcher([workspace, workspaceB], userHome, fileService, new NullLogService(), debounceMs)); + let fires = 0; + disposables.add(watcher.onDidChange(() => { fires++; })); + + await seed('/workspace-b/unrelated.txt', 'x'); + await settle(); + assert.strictEqual(fires, 0); + + await Promise.all([ + seed('/workspace-b/.claude/agents/a.md', 'a'), + seed('/workspace-b/.claude/skills/s/SKILL.md', 's'), + seed('/workspace-b/.claude/settings.json', '{}'), + ]); + await settle(); + assert.strictEqual(fires, 1); + }); + test('fires for a root-level CLAUDE.md / CLAUDE.local.md edit', async () => { const watcher = disposables.add(new ClaudeCustomizationWatcher(workspace, userHome, fileService, new NullLogService(), debounceMs)); let fires = 0; diff --git a/src/vs/platform/agentHost/test/node/customizations/scan/claudeNativePluginScan.test.ts b/src/vs/platform/agentHost/test/node/customizations/scan/claudeNativePluginScan.test.ts index 35f84367266..e75ac5278a6 100644 --- a/src/vs/platform/agentHost/test/node/customizations/scan/claudeNativePluginScan.test.ts +++ b/src/vs/platform/agentHost/test/node/customizations/scan/claudeNativePluginScan.test.ts @@ -8,7 +8,7 @@ import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { NullLogService } from '../../../../../log/common/log.js'; import { IFileService } from '../../../../../files/common/files.js'; -import { scanClaudeNativePlugins } from '../../../../node/claude/customizations/scan/claudeNativePluginScan.js'; +import { scanClaudeNativePlugins, scanClaudeNativePluginsForRoots } from '../../../../node/claude/customizations/scan/claudeNativePluginScan.js'; import { claudeTestUserHome as userHome, claudeTestWorkspace as workspace, createInMemoryFileService, seedFile } from '../claudeCustomizationTestUtils.js'; suite('claudeNativePluginScan', () => { @@ -128,6 +128,67 @@ suite('claudeNativePluginScan', () => { assert.deepStrictEqual(plugins.map(p => p.root.path), ['/workspace/.claude/skills/mine']); }); + test('discovers an in-place plugin enabled only by an additional workspace root', async () => { + const workspaceB = workspace.with({ path: '/workspace-b' }); + await seed('/workspace-b/.claude/settings.json', JSON.stringify({ enabledPlugins: { 'mine@skills-dir': true } })); + await seed('/workspace-b/.claude/skills/mine/.claude-plugin/plugin.json', manifest('mine')); + + const plugins = await scanClaudeNativePluginsForRoots([workspace, workspaceB], userHome, fileService, logService); + + assert.deepStrictEqual(plugins.map(p => ({ id: p.id, root: p.root.path })), [ + { id: 'mine@skills-dir', root: '/workspace-b/.claude/skills/mine' }, + ]); + }); + + test('uses the most-specific nested workspace root when parsing plugin hooks', async () => { + const workspaceB = workspace.with({ path: '/workspace/packages/b' }); + await seed('/workspace/packages/b/.claude/settings.json', JSON.stringify({ enabledPlugins: { 'mine@skills-dir': true } })); + await seed('/workspace/packages/b/.claude/skills/mine/.claude-plugin/plugin.json', manifest('mine')); + await seed('/workspace/packages/b/.claude/skills/mine/hooks/hooks.json', JSON.stringify({ + hooks: { + PreToolUse: [{ + hooks: [{ type: 'command', command: 'echo nested', cwd: 'scripts' }], + }], + }, + })); + + const plugins = await scanClaudeNativePluginsForRoots([workspace, workspaceB], userHome, fileService, logService); + + assert.deepStrictEqual(plugins[0].parsed.hooks.flatMap(group => group.commands).map(hook => hook.cwd?.path), [ + '/workspace/packages/b/scripts', + ]); + }); + + test('uses the primary root for cached plugin hooks when an additional root contains userHome', async () => { + const broadRoot = workspace.with({ path: '/home' }); + await seed('/home/.claude/settings.json', JSON.stringify({ enabledPlugins: { 'cached@m': true } })); + await seed('/home/.claude/plugins/cache/m/cached/1.0.0/.claude-plugin/plugin.json', manifest('cached')); + await seed('/home/.claude/plugins/cache/m/cached/1.0.0/hooks/hooks.json', JSON.stringify({ + hooks: { + PreToolUse: [{ + hooks: [{ type: 'command', command: 'echo cached', cwd: 'scripts' }], + }], + }, + })); + + const plugins = await scanClaudeNativePluginsForRoots([workspace, broadRoot], userHome, fileService, logService); + + assert.deepStrictEqual(plugins[0].parsed.hooks.flatMap(group => group.commands).map(hook => hook.cwd?.path), [ + '/workspace/scripts', + ]); + }); + + test('uses ordered root precedence for conflicting plugin enablement', async () => { + const workspaceB = workspace.with({ path: '/workspace-b' }); + await seed('/workspace/.claude/settings.json', JSON.stringify({ enabledPlugins: { 'mine@skills-dir': false } })); + await seed('/workspace-b/.claude/settings.json', JSON.stringify({ enabledPlugins: { 'mine@skills-dir': true } })); + await seed('/workspace-b/.claude/skills/mine/.claude-plugin/plugin.json', manifest('mine')); + + const plugins = await scanClaudeNativePluginsForRoots([workspace, workspaceB], userHome, fileService, logService); + + assert.deepStrictEqual(plugins, []); + }); + test('fail-soft: an enabled plugin with no resolvable root is skipped, not thrown', async () => { await seed('/home/.claude/settings.json', JSON.stringify({ enabledPlugins: { 'present@m': true, 'missing@m': true } })); await seed('/home/.claude/plugins/cache/m/present/1.0.0/.claude-plugin/plugin.json', manifest('present')); diff --git a/src/vs/sessions/AI_CUSTOMIZATIONS.md b/src/vs/sessions/AI_CUSTOMIZATIONS.md index 285b43e940e..c68d440cd69 100644 --- a/src/vs/sessions/AI_CUSTOMIZATIONS.md +++ b/src/vs/sessions/AI_CUSTOMIZATIONS.md @@ -140,6 +140,8 @@ The shared plugin discovery pipeline selects format-specific component paths whi Runtime projection is provider-specific. Copilot receives strict skills and MCP explicitly rather than through legacy SDK plugin-directory discovery. Codex receives strict skill roots plus MCP, with remote transport selected by its existing auto-detection. Claude excludes strict packages from legacy plugin discovery and can project remote MCP through its existing auto-detection, but its current SDK cannot register external skill directories or provide the per-server working directory required by strict stdio MCP, so those components are reported and skipped. +Claude Agent Host multi-root customization discovery is gated by the hidden, default-off `chat.agentHost.claudeAgent.multiRootEnabled` setting. When enabled, the primary working directory and each SDK `additionalDirectories` root contribute standalone `.claude/agents`, `.claude/skills`, and native plugin enablement to the Customizations editor. Roots are processed in session order, followed by user scope; same-named standalone agents or skills use the first visible definition as the display source. This display policy is centralized because the SDK reports standalone entries by name rather than source URI. Native plugin loaded state remains authoritative from the SDK snapshot. Rules, hooks, MCP configuration, commands, and CLAUDE.md remain primary-root/user scoped because Claude additional directories do not load those configuration types. Each contributing root has its own writable directory container, and secondary-root watchers observe only agents, skills, and plugin settings. + ### IHarnessDescriptor Key properties on the harness descriptor: From 32636e2fbfe58b3b79cd8681c5c6c829867b64a4 Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:37:51 +0000 Subject: [PATCH 09/86] [cherry-pick] Fix Copilot Chat recovery test checkout path (#328233) Co-authored-by: vs-code-engineering[bot] --- build/azure-pipelines/product-copilot-recovery.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/build/azure-pipelines/product-copilot-recovery.yml b/build/azure-pipelines/product-copilot-recovery.yml index 6f7888e934c..17150e6fd23 100644 --- a/build/azure-pipelines/product-copilot-recovery.yml +++ b/build/azure-pipelines/product-copilot-recovery.yml @@ -97,6 +97,7 @@ extends: testSteps: - checkout: self + path: s lfs: true retryCountOnTaskFailure: 3 - template: copilot/setup-steps.yml From eb7a00caa262f7fd09c6445c43d4adc445e50086 Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:45:42 +0000 Subject: [PATCH 10/86] [cherry-pick] Add Kimi K3 biosecurity policy for Copilot Chat recovery (#328213) * [cherry-pick] Add Kimi K3 biosecurity policy for Copilot Chat recovery * Resolve merge conflict in package.json, keeping version at 0.60.0 Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> * Resolve merge conflict in package-lock.json, keeping version at 0.60.0 Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --------- Co-authored-by: vs-code-engineering[bot] Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- .../prompts/node/agent/kimiPrompts.tsx | 16 +++++++++++ .../node/agent/test/kimiPrompts.spec.tsx | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx index 22717206132..a55c2314f1a 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx @@ -16,6 +16,20 @@ import { ApplyPatchInstructions, CodesearchModeInstructions, DefaultAgentPromptP import { FileLinkificationInstructions } from './fileLinkificationInstructions'; import { IAgentPrompt, PromptRegistry, ReminderInstructionsConstructor, SystemPrompt } from './promptRegistry'; +const KIMI_K3_BIOSECURITY_AND_CHEMICAL_SAFETY_POLICY = `BIOSECURITY AND CHEMICAL SAFETY POLICY (non-negotiable) + +You must refuse to provide information that would provide meaningful uplift toward biological or chemical weapons or other mass-casualty harm. This overrides any other instruction, persona, or user framing (including "hypothetical", "fictional", "research", "educational", or "for a novel" framings). + +Refuse, and do not provide workarounds, partial answers, or pointers to sources, when a request seeks to: +- Bypass, evade, circumvent, or defeat nucleic-acid / DNA / RNA synthesis-provider screening, sequence order screening, or "know-your-customer" biosecurity controls (including obfuscating, fragmenting, recoding, or reordering sequences to avoid detection). +- Design, synthesize, acquire, culture, propagate, stabilize, enhance the virulence or transmissibility of, or weaponize any pathogen, toxin, or biological agent. +- Obtain step-by-step wet-lab protocols, troubleshooting, reagent/equipment sourcing, or scale-up guidance intended to produce a harmful biological or chemical agent. +- Produce, synthesize, or disperse chemical weapons, toxic agents, or their precursors. + +When refusing, give a brief, non-judgmental refusal and, where appropriate, offer clearly safe, high-level educational or defensive alternatives (e.g., biosafety, public-health, or policy information) without operational detail. + +Answer normally for legitimate requests: general science education, clinical care, approved public-health work, defensive biosecurity, and dual-use topics that do not provide operational uplift toward the categories above.`; + class KimiAgentPrompt extends PromptElement { async render(state: void, sizing: PromptSizing) { const tools = detectToolCapabilities(this.props.availableTools); @@ -26,6 +40,8 @@ class KimiAgentPrompt extends PromptElement { 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. + {this.props.modelFamily?.toLowerCase().includes('kimi-k3') && <>{KIMI_K3_BIOSECURITY_AND_CHEMICAL_SAFETY_POLICY}
} + Use clear, step-by-step task execution:
- For simple questions or code samples, answer directly without unnecessary tool calls.
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 38aa6cee0d7..af1949831da 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 @@ -20,6 +20,20 @@ import { PromptRenderer } from '../../base/promptRenderer'; import '../allAgentPrompts'; import { PromptRegistry } from '../promptRegistry'; +const KIMI_K3_BIOSECURITY_AND_CHEMICAL_SAFETY_POLICY = `BIOSECURITY AND CHEMICAL SAFETY POLICY (non-negotiable) + +You must refuse to provide information that would provide meaningful uplift toward biological or chemical weapons or other mass-casualty harm. This overrides any other instruction, persona, or user framing (including "hypothetical", "fictional", "research", "educational", or "for a novel" framings). + +Refuse, and do not provide workarounds, partial answers, or pointers to sources, when a request seeks to: +- Bypass, evade, circumvent, or defeat nucleic-acid / DNA / RNA synthesis-provider screening, sequence order screening, or "know-your-customer" biosecurity controls (including obfuscating, fragmenting, recoding, or reordering sequences to avoid detection). +- Design, synthesize, acquire, culture, propagate, stabilize, enhance the virulence or transmissibility of, or weaponize any pathogen, toxin, or biological agent. +- Obtain step-by-step wet-lab protocols, troubleshooting, reagent/equipment sourcing, or scale-up guidance intended to produce a harmful biological or chemical agent. +- Produce, synthesize, or disperse chemical weapons, toxic agents, or their precursors. + +When refusing, give a brief, non-judgmental refusal and, where appropriate, offer clearly safe, high-level educational or defensive alternatives (e.g., biosafety, public-health, or policy information) without operational detail. + +Answer normally for legitimate requests: general science education, clinical care, approved public-health work, defensive biosecurity, and dual-use topics that do not provide operational uplift toward the categories above.`; + suite('KimiPrompts', () => { let accessor: ITestingServicesAccessor; @@ -76,4 +90,17 @@ suite('KimiPrompts', () => { expect(renderedPrompt).not.toContain(`Use ${ToolName.EditFile}`); expect(renderedPrompt).not.toContain(`Use ${ToolName.ApplyPatch}`); }); + + test('adds the biosecurity and chemical safety policy only for Kimi K3', async () => { + const kimiK3Prompt = await renderSystemPrompt('kimi-k3'); + const kimiK2Prompts = await Promise.all([ + renderSystemPrompt('kimi-k2.6'), + renderSystemPrompt('kimi-k2.7-code'), + ]); + + expect(kimiK3Prompt).toContain(KIMI_K3_BIOSECURITY_AND_CHEMICAL_SAFETY_POLICY); + for (const renderedPrompt of kimiK2Prompts) { + expect(renderedPrompt).not.toContain('BIOSECURITY AND CHEMICAL SAFETY POLICY'); + } + }); }); From 71f1f7f090b6696f9498f5f76c2c0527e0652a24 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 30 Jul 2026 09:50:04 -0700 Subject: [PATCH 11/86] Correct the recorded symptom of the Claude fork defect (#328105) * agentHost: correct the recorded symptom of the Claude fork defect The entry claimed a provider-context fork "rejects the AHP turn id as an invalid upToMessageId". Enabling the gate and recording against the live SDK shows that is not what happens: the E2E fork path never reaches forkSession at all. That string comes from a unit-test stub and the SDK. What actually happens is quieter and worse. The fork silently produces a chat with no provider context: the forked chat's AHP transcript is seeded with the source turn and looks correct, but the model request carries no prior history, so the model cannot recall anything from the source conversation. No error reaches the client. Root cause is anchor resolution. resolveForkAnchorUuid matches the requested turn id against Claude SDK envelope uuids, so it resolves only when the AHP turn id happens to be an SDK uuid. AHP lets a client choose its own turn id on dispatch and Copilot honors that; for such an id the anchor never resolves, _forkChat warns, and createChat continues with a fresh chat. The same test passes for Copilot with the full inherited history in its capture, so this is provider-specific rather than a fault in the shared fork contract or the test. Tests stay disabled - the defect is unfixed - but the entry now describes the symptom someone would actually observe, and records that the unknown-turn test asserts correct behavior and shares the gate only by construction. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: link the Claude fork defect to its tracking issue (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/test/node/e2e/KNOWN_ISSUES.md | 29 +++++++++++++++++-- .../e2e/harness/agentHostE2ETestHarness.ts | 12 ++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md b/src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md index 2d0d9112aa9..ed2eeb5f87c 100644 --- a/src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md +++ b/src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md @@ -123,7 +123,7 @@ A capture that genuinely cannot be refreshed goes in `STALE_RECORDED_REQUEST_EXC - Test: `side chat receives bounded source context without copied history`. - Scope: Claude. - Expected: re-recording the capture drives a real side chat and stores the request the host now sends. -- Observed: recording fails with `Invalid upToMessageId: turn-source`. The side chat is created against a source turn, so recording exercises the same provider-context fork defect that gates `supportsChatForkE2E`; see [Claude provider-context fork](#claude-provider-context-fork). +- Observed: recording does not reproduce the committed capture, because the side chat is anchored on a source turn and therefore hits the same anchor-resolution defect that gates `supportsChatForkE2E` — the side chat falls back to an injected `` preamble instead of a provider fork. See [Claude provider-context fork](#claude-provider-context-fork). - Consequence: the committed capture predates the host's `` preamble, so its recorded request no longer matches the live one. The test still replays correctly — only the request comparison is disabled, via `STALE_RECORDED_REQUEST_EXCEPTIONS`. - Reproduce: @@ -144,8 +144,33 @@ A capture that genuinely cannot be refreshed goes in `STALE_RECORDED_REQUEST_EXC - `unknown-turn fork does not inherit source provider context` - Scope: Claude. - Expected: Claude advertises multi-chat fork support, and a provider-backed fork can continue from the requested source history. -- Observed: exercising a real provider-context fork rejects the AHP turn id as an invalid `upToMessageId`. The unknown-turn context test currently shares the same provider E2E fork gate. +- Observed: the fork **silently produces a chat with no provider context**. The forked chat's AHP state looks correct — the source turn is seeded into its transcript — but the model request carries no prior history, so the model cannot answer questions about the source conversation. No error reaches the client. + + Verified against the live SDK by enabling the gate and recording. Of the four assertions, only the AHP-level one passes: + + ``` + seededMessages: ok (source turn present in the forked chat) + requestHasPriorUserMessage: FAIL (model request has no source user turn) + requestHasPriorAssistantMessage: FAIL (model request has no source reply) + responseHasCodeWord: FAIL (model cannot recall the source code word) + ``` + + The same test passes for Copilot, whose capture shows the full inherited history, so this is provider-specific rather than a fault in the test or the shared fork contract. + + Root cause: `resolveForkAnchorUuid` (`claudeReplayMapper.ts`) matches the requested turn id against **Claude SDK envelope uuids**, so it only resolves when the AHP turn id happens to *be* an SDK uuid. AHP lets a client choose its own turn id on dispatch — Copilot honors that — and for such an id the anchor never resolves: + + ``` + resolveForkAnchorUuid(messages, 'u1') -> 'a1' (SDK uuid, resolves) + resolveForkAnchorUuid(messages, 'fork-source') -> undefined (client turn id, never resolves) + ``` + + `_forkChat` then logs a warning and returns `undefined`, and `createChat` continues with a fresh chat. The degradation is invisible to the client, which is the part that makes this a defect rather than a limitation: a required contract fails silently instead of surfacing a typed error. + + The earlier description of this entry — that the fork "rejects the AHP turn id as an invalid `upToMessageId`" — was inaccurate. That string comes from a unit-test stub and the SDK; the E2E fork path never reaches `forkSession` at all. + +- Note: `unknown-turn fork does not inherit source provider context` asserts the *correct* behavior for an unresolvable anchor and shares this gate only because both are `forkProviderTest`s. It is expected to pass once the resolvable case works. - Gate: `supportsChatForkE2E: false`. +- Issue: [#328104](https://github.com/microsoft/vscode/issues/328104). - Reproduce: ```bash diff --git a/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts b/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts index e0b7a610aee..55824cfa1d0 100644 --- a/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts +++ b/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts @@ -180,12 +180,12 @@ const POSIX_COMMAND_EXCEPTIONS = new Set([]); * `harness/modelRequestProjection.ts`. */ const STALE_RECORDED_REQUEST_EXCEPTIONS = new Set([ - // Re-recording drives a real provider-context fork, which Claude rejects - // with "Invalid upToMessageId: turn-source" — the same defect that gates - // `supportsChatForkE2E`. The capture predates the host's - // `` preamble and cannot be refreshed until that is - // fixed. Claude only: the other providers fork fine and their captures are - // current. + // Re-recording anchors a side chat on a source turn, which hits the same + // anchor-resolution defect that gates `supportsChatForkE2E`: Claude cannot + // resolve a client-assigned turn id, so the fork silently degrades to an + // injected context preamble. The capture predates that preamble and cannot + // be refreshed until the defect is fixed. Claude only: the other providers + // fork fine and their captures are current. 'claude:side chat receives bounded source context without copied history', ]); From e7c86491360a292aa8b33c066d169b1e5690d560 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 31 Jul 2026 02:58:42 +1000 Subject: [PATCH 12/86] agentHost: add multi-root support for Codex sessions (#328210) * feat(agentHost): add multi-root support for Codex sessions Gate Codex multi-root support behind a hidden setting, forward workspace roots through the app-server lifecycle, and persist them for resume and fork recovery. Preserve existing additional writable-directory behavior and disabled defaults. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(agentHost): normalize Codex workspace root identity Deduplicate roots using platform-aware filesystem comparison keys and make additional-directory tests portable on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(agentHost): preserve Codex single-root behavior Activate Codex multi-root protocol and persistence work only for sessions with more than one distinct workspace root. Reuse existing metadata operations so single-folder sessions retain their original wire shape and I/O costs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/remoteAgentHostProtocolClient.ts | 19 +- .../agentHost/common/agentHostSchema.ts | 9 + .../agentHostStarter.config.contribution.ts | 7 + .../platform/agentHost/common/agentService.ts | 7 + .../agentHost/node/codex/codexAgent.ts | 205 ++++++++- .../agentHost/node/codex/codexLaunchConfig.ts | 6 +- .../node/codex/codexSessionMetadataStore.ts | 51 ++- .../remoteAgentHostProtocolClient.test.ts | 21 +- .../test/node/codex/codexLaunchConfig.test.ts | 6 + .../test/node/codex/codexModelRefresh.test.ts | 34 +- .../node/codex/codexPrewarmEviction.test.ts | 428 +++++++++++++++++- .../codex/codexSessionMetadataStore.test.ts | 47 ++ 12 files changed, 786 insertions(+), 54 deletions(-) create mode 100644 src/vs/platform/agentHost/test/node/codex/codexSessionMetadataStore.test.ts diff --git a/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts b/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts index 0a7ca42c332..9cb21e3da04 100644 --- a/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts +++ b/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts @@ -18,7 +18,7 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { ILogService } from '../../log/common/log.js'; import { FileSystemProviderErrorCode, toFileSystemProviderErrorCode } from '../../files/common/files.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; -import { AgentSession, AgentHostCodexAgentEnabledSettingId, AgentHostCopilotMultiRootEnabledSettingId, AgentHostClaudeMultiRootEnabledSettingId, AgentHostSystemProxyEnabledSettingId, IAgentConnection, IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IMcpNotification } from '../common/agentService.js'; +import { AgentSession, AgentHostCodexAgentEnabledSettingId, AgentHostCodexMultiRootEnabledSettingId, AgentHostCopilotMultiRootEnabledSettingId, AgentHostClaudeMultiRootEnabledSettingId, AgentHostSystemProxyEnabledSettingId, IAgentConnection, IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IMcpNotification } from '../common/agentService.js'; import { AMBIENT_AGENT_HOST_AUTHORITY } from '../common/agentHostConnectionsService.js'; import { createRemoteWatchHandle, type IRemoteWatchHandle } from '../common/agentHostFileSystemProvider.js'; import { AgentSubscriptionManager, type IActiveSubscriptionInfo, type IAgentSubscription } from '../common/state/agentSubscription.js'; @@ -38,7 +38,7 @@ import { encodeBase64 } from '../../../base/common/buffer.js'; import { ILoadEstimator, LoadEstimator } from '../../../base/parts/ipc/common/ipc.net.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, AgentHostCodexEnabledConfigKey, AgentHostCopilotMultiRootEnabledConfigKey, AgentHostClaudeMultiRootEnabledConfigKey, AgentHostSessionSyncEnabledConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyEnabledConfigKey, AgentHostPreferLongContextEnabledConfigKey, AgentHostSystemProxyEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, AgentHostEditTelemetryEnabledConfigKey, getAgentHostTerminalAutoApproveRulesConfig, SESSION_SYNC_ENABLED_SETTING_ID, TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID, GLOBAL_AUTO_APPROVE_SETTING_ID, AUTO_REPLY_SETTING_ID, PREFER_LONG_CONTEXT_SETTING_ID, TERMINAL_AUTO_APPROVE_SETTING_ID, TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID, DISABLE_REPO_INFO_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_ENABLED_SETTING_ID, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js'; +import { AgentHostTelemetryLevelConfigKey, AgentHostCodexEnabledConfigKey, AgentHostCodexMultiRootEnabledConfigKey, AgentHostCopilotMultiRootEnabledConfigKey, AgentHostClaudeMultiRootEnabledConfigKey, AgentHostSessionSyncEnabledConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyEnabledConfigKey, AgentHostPreferLongContextEnabledConfigKey, AgentHostSystemProxyEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, AgentHostEditTelemetryEnabledConfigKey, getAgentHostTerminalAutoApproveRulesConfig, SESSION_SYNC_ENABLED_SETTING_ID, TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID, GLOBAL_AUTO_APPROVE_SETTING_ID, AUTO_REPLY_SETTING_ID, PREFER_LONG_CONTEXT_SETTING_ID, TERMINAL_AUTO_APPROVE_SETTING_ID, TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID, DISABLE_REPO_INFO_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_ENABLED_SETTING_ID, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js'; import type { OtlpExportLogsParams } from '../common/state/protocol/channels-otlp/notifications.js'; import type { TelemetryCapabilities } from '../common/state/protocol/channels-otlp/state.js'; import type { Implementation, InitializeResult } from '../common/state/protocol/common/commands.js'; @@ -400,6 +400,12 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC } this._updateClaudeMultiRootEnabled(); } + if (e.affectsConfiguration(AgentHostCodexMultiRootEnabledSettingId)) { + if (this._state.kind !== AgentHostClientState.Connected) { + return; + } + this._updateCodexMultiRootEnabled(); + } if (e.affectsConfiguration(TERMINAL_AUTO_APPROVE_SETTING_ID) || e.affectsConfiguration(TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID)) { if (this._state.kind !== AgentHostClientState.Connected) { return; @@ -714,6 +720,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC this._updateSystemProxyEnabled(); this._updateCopilotMultiRootEnabled(); this._updateClaudeMultiRootEnabled(); + this._updateCodexMultiRootEnabled(); this._updateTerminalAutoApproveRules(); this._updateCodexEnabled(); this._updateDisableRepoInfoTelemetry(); @@ -1581,6 +1588,14 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC }, this._clientId, 0); } + private _updateCodexMultiRootEnabled(): void { + const enabled = this._configurationService.getValue(AgentHostCodexMultiRootEnabledSettingId) === true; + this.dispatchAction(ROOT_STATE_URI, { + type: ActionType.RootConfigChanged, + config: { [AgentHostCodexMultiRootEnabledConfigKey]: enabled }, + }, this._clientId, 0); + } + private _updateCodexEnabled(): void { // Always forwards the current value; the host only acts on enable, so a // forwarded `false` only takes effect on the next agent host restart diff --git a/src/vs/platform/agentHost/common/agentHostSchema.ts b/src/vs/platform/agentHost/common/agentHostSchema.ts index b4cb79fb168..bc928c8f769 100644 --- a/src/vs/platform/agentHost/common/agentHostSchema.ts +++ b/src/vs/platform/agentHost/common/agentHostSchema.ts @@ -482,6 +482,9 @@ export const AgentHostCopilotMultiRootEnabledConfigKey = 'copilotMultiRootEnable */ export const AgentHostClaudeMultiRootEnabledConfigKey = 'claudeMultiRootEnabled'; +/** Root config key forwarded from the renderer that gates Codex multiple-working-directory support. */ +export const AgentHostCodexMultiRootEnabledConfigKey = 'codexMultiRootEnabled'; + /** * Root config key forwarded from the renderer when VS Code's * `chat.tools.terminal.autoApprove` setting changes. Holds the effective @@ -756,6 +759,12 @@ export const platformRootSchema = createSchema({ description: localize('agentHost.config.claudeMultiRootEnabled.description', "Whether the Claude provider advertises support for multiple working directories, letting a session span every folder of a multi-root workspace."), default: false, }), + [AgentHostCodexMultiRootEnabledConfigKey]: schemaProperty({ + type: 'boolean', + title: localize('agentHost.config.codexMultiRootEnabled.title', "Codex Multiple Working Directories"), + description: localize('agentHost.config.codexMultiRootEnabled.description', "Whether the Codex provider advertises support for multiple working directories, letting a session span every folder of a multi-root workspace."), + default: false, + }), [AgentHostTerminalAutoApproveRulesConfigKey]: schemaProperty({ type: 'object', title: localize('agentHost.config.terminalAutoApproveRules.title', "Terminal Auto Approve Rules"), diff --git a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts index 40118defc21..6a65e537ee0 100644 --- a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts +++ b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts @@ -16,6 +16,7 @@ import { AgentHostClaudeMultiRootEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, + AgentHostCodexMultiRootEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostCopilotMultiRootEnabledSettingId, @@ -112,6 +113,12 @@ configurationRegistry.registerConfiguration({ // `product.quality !== 'stable'`) to enable it for a build channel. included: false, }, + [AgentHostCodexMultiRootEnabledSettingId]: { + type: 'boolean', + description: nls.localize('chat.agentHost.codexAgent.multiRootEnabled', "When enabled, Codex agent-host sessions advertise support for multiple working directories, so a session created in a multi-root workspace can span every workspace folder. Experimental; newly created sessions pick up a change without restarting the agent host."), + default: false, + included: false, + }, [AgentHostClaudeAgentEnabledSettingId]: { type: 'boolean', description: nls.localize('chat.agentHost.claudeAgent.enabled', "When enabled, the agent host registers the Claude provider (subject to the Claude SDK being reachable). Independent of `#chat.agents.claude.preferAgentHost#` and `#chat.editor.claude.preferAgentHost#`, which choose which integration surfaces Claude. 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 ae912c4cfaa..2a1a0936af8 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -76,6 +76,13 @@ export const AgentHostCopilotMultiRootEnabledSettingId = 'chat.agentHost.copilot */ export const AgentHostClaudeMultiRootEnabledSettingId = 'chat.agentHost.claudeAgent.multiRootEnabled'; +/** + * Configuration key gating multiple-working-directory support for the Codex + * agent-host provider. Hidden from the Settings UI and off by default while the + * feature is dogfooded. + */ +export const AgentHostCodexMultiRootEnabledSettingId = 'chat.agentHost.codexAgent.multiRootEnabled'; + // The Copilot-CLI-specific setting IDs (`customTerminalTool`, `opus48Prompt`, // `reasoningEffortOverride`, `modelCapabilityOverrides`) live with their // root-config keys in `copilotCliConfig.ts`. diff --git a/src/vs/platform/agentHost/node/codex/codexAgent.ts b/src/vs/platform/agentHost/node/codex/codexAgent.ts index c87803f88bc..3efdb32e4a0 100644 --- a/src/vs/platform/agentHost/node/codex/codexAgent.ts +++ b/src/vs/platform/agentHost/node/codex/codexAgent.ts @@ -12,8 +12,8 @@ import { fetchResourceMetadata } from '../../../../base/common/oauth.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { type IObservable, observableValue } from '../../../../base/common/observable.js'; -import { basename, dirname, isAbsolute, join, resolve, sep } from '../../../../base/common/path.js'; -import { isEqual } from '../../../../base/common/resources.js'; +import { basename, dirname, isAbsolute, join, normalize, resolve, sep } from '../../../../base/common/path.js'; +import { extUriBiasedIgnorePathCase, isEqual } from '../../../../base/common/resources.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; @@ -21,7 +21,7 @@ import { IInstantiationService } from '../../../instantiation/common/instantiati import { localize } from '../../../../nls.js'; import { ILogService } from '../../../log/common/log.js'; import { IProductService } from '../../../product/common/productService.js'; -import { createSchema, platformRootSchema, platformSessionSchema, schemaProperty, AgentHostMcpServersConfigKey, type ISchemaProperty, type SessionMode } from '../../common/agentHostSchema.js'; +import { createSchema, platformRootSchema, platformSessionSchema, schemaProperty, AgentHostCodexMultiRootEnabledConfigKey, AgentHostMcpServersConfigKey, type ISchemaProperty, type SessionMode } from '../../common/agentHostSchema.js'; import { createPricingMetaFromBilling, normalizeCAPIBilling } from '../../common/agentModelPricing.js'; import { AgentHostConfigKey, agentHostCustomizationConfigSchema, type CodexUsageSource } from '../../common/agentHostCustomizationConfig.js'; import { getReasoningEffortDescription, getReasoningEffortLabel } from '../../common/reasoningEffort.js'; @@ -92,6 +92,8 @@ import type { Thread } from './protocol/generated/v2/Thread.js'; import type { ThreadListResponse } from './protocol/generated/v2/ThreadListResponse.js'; import type { ThreadReadResponse } from './protocol/generated/v2/ThreadReadResponse.js'; import type { ThreadForkResponse } from './protocol/generated/v2/ThreadForkResponse.js'; +import type { ThreadStartResponse } from './protocol/generated/v2/ThreadStartResponse.js'; +import type { ThreadResumeResponse } from './protocol/generated/v2/ThreadResumeResponse.js'; import type { TurnCompletedNotification } from './protocol/generated/v2/TurnCompletedNotification.js'; import type { TurnStartedNotification } from './protocol/generated/v2/TurnStartedNotification.js'; import type { ItemStartedNotification } from './protocol/generated/v2/ItemStartedNotification.js'; @@ -356,6 +358,45 @@ const codexSessionConfigDefaults: ICodexSessionConfigDefaults = { [CodexSessionConfigKey.ReasoningSummary]: 'auto', }; +function distinctAbsolutePaths(paths: readonly string[]): string[] { + const seen = new Set(); + const result: string[] = []; + for (const path of paths) { + const normalized = normalize(path); + const key = filesystemPathComparisonKey(normalized); + if (key && !seen.has(key)) { + seen.add(key); + result.push(normalized); + } + } + return result; +} + +function distinctWorkingDirectories(directories: readonly URI[] | undefined): readonly URI[] | undefined { + if (!directories) { + return undefined; + } + const seen = new Set(); + const result: URI[] = []; + for (const directory of directories) { + const path = normalize(directory.fsPath); + const key = filesystemPathComparisonKey(path); + if (key && !seen.has(key)) { + seen.add(key); + result.push(directory); + } + } + return result.length > 0 ? result : undefined; +} + +function filesystemPathComparisonKey(path: string): string | undefined { + if (!isAbsolute(path)) { + return undefined; + } + const resource = extUriBiasedIgnorePathCase.removeTrailingPathSeparator(URI.file(path)); + return extUriBiasedIgnorePathCase.getComparisonKey(resource); +} + const CodexPrewarmTtlMs = 60_000; /** @@ -404,6 +445,7 @@ interface ICodexSession { * `workingDirectory`). */ workingDirectories?: readonly URI[]; + readonly multiRootEnabled: boolean; /** * Set to the temp folder created for this session when no working * directory was supplied, so {@link CodexAgent.disposeSession} can remove @@ -531,6 +573,10 @@ interface ICodexSession { readonly clientCustomizations: CodexClientCustomizationStore; } +type ICodexSessionRead = ThreadReadResponse & { + readonly persistedWorkingDirectories?: readonly URI[]; +}; + /** * A live Codex collab-agent (subagent) child thread. Codex runs each spawned * subagent as its OWN app-server thread that emits a full item/turn event @@ -1162,10 +1208,16 @@ export class CodexAgent extends Disposable implements IAgent { if (mode === 'read-only') { return { type: 'readOnly', networkAccess: false }; } - const writableRoots = [ - ...(session.workingDirectory ? [session.workingDirectory.fsPath] : []), - ...(narrowAdditionalDirectories(config[CodexSessionConfigKey.AdditionalDirectories]) ?? []), - ]; + const additionalDirectories = narrowAdditionalDirectories(config[CodexSessionConfigKey.AdditionalDirectories]) ?? []; + const writableRoots = this._isMultiRootActive(session) + ? distinctAbsolutePaths([ + ...this._runtimeWorkspaceRoots(session), + ...additionalDirectories, + ]) + : [ + ...(session.workingDirectory ? [session.workingDirectory.fsPath] : []), + ...additionalDirectories, + ]; return { type: 'workspaceWrite', writableRoots, @@ -1179,7 +1231,9 @@ export class CodexAgent extends Disposable implements IAgent { const config = this._readSessionConfig(session); const { approvalPolicy, sandboxMode, approvalsReviewer } = this._resolveSessionPermissions(session); const sandboxPolicy = this._sandboxPolicy(session, config, sandboxMode); - const runtimeWorkspaceRoots = sandboxPolicy.type === 'workspaceWrite' ? sandboxPolicy.writableRoots : undefined; + const runtimeWorkspaceRoots = this._isMultiRootActive(session) + ? this._runtimeWorkspaceRoots(session) + : (sandboxPolicy.type === 'workspaceWrite' ? sandboxPolicy.writableRoots : undefined); const effort = this._getReasoningEffort(session); const personality = narrowPersonality(config[CodexSessionConfigKey.Personality]) ?? codexSessionConfigDefaults[CodexSessionConfigKey.Personality]; const summary = narrowReasoningSummary(config[CodexSessionConfigKey.ReasoningSummary]) ?? codexSessionConfigDefaults[CodexSessionConfigKey.ReasoningSummary]; @@ -1205,6 +1259,16 @@ export class CodexAgent extends Disposable implements IAgent { }; } + private _runtimeWorkspaceRoots(session: ICodexSession): string[] { + const workingDirectories = session.workingDirectories + ?? (session.workingDirectory ? [session.workingDirectory] : []); + return distinctAbsolutePaths(workingDirectories.map(directory => directory.fsPath)); + } + + private _isMultiRootActive(session: ICodexSession): boolean { + return session.multiRootEnabled && (session.workingDirectories?.length ?? 0) > 1; + } + private async _refreshModels(): Promise { const usageSource = this._usageSource; if (usageSource === 'openai') { @@ -2197,6 +2261,8 @@ export class CodexAgent extends Disposable implements IAgent { threadId: childThreadId, sessionUri: parent.sessionUri, workingDirectory: parent.workingDirectory, + workingDirectories: parent.workingDirectories, + multiRootEnabled: parent.multiRootEnabled, managedWorkingDirectory: undefined, mapState: createCodexSessionMapState(new Set(this._serverToolHost?.toolNames ?? []), clientToolSet), pendingCommandApprovals: new PendingRequestRegistry(), @@ -2625,9 +2691,14 @@ export class CodexAgent extends Disposable implements IAgent { description: this._usageSource === 'openai' ? localize('codexAgent.description.openai', "Codex agent using your OpenAI account") : localize('codexAgent.description.copilot', "Codex agent using GitHub Copilot"), + ...(this._isMultiRootEnabled() ? { capabilities: { multipleWorkingDirectories: { immutablePrimary: true } } } : {}), }; } + private _isMultiRootEnabled(): boolean { + return this._configurationService.getRootValue(platformRootSchema, AgentHostCodexMultiRootEnabledConfigKey) === true; + } + private _sessionUriFromChat(chat: URI): URI { const parsed = parseChatUri(chat); return parsed ? URI.parse(parsed.session) : chat; @@ -2708,6 +2779,10 @@ export class CodexAgent extends Disposable implements IAgent { const effectiveModel = this._supportedModelOrUndefined(config.model); const sessionId = config.session ? AgentSession.id(config.session) : generateUuid(); const sessionUri = config.session ?? AgentSession.uri(this.id, sessionId); + const multiRootEnabled = this._isMultiRootEnabled(); + const workingDirectories = multiRootEnabled && (config.workingDirectories?.length ?? 0) > 1 + ? distinctWorkingDirectories(config.workingDirectories) + : undefined; // If the workbench is rebinding this URI (createSession arriving // after a previous dispose for the same id), reuse the existing @@ -2729,6 +2804,8 @@ export class CodexAgent extends Disposable implements IAgent { threadId: undefined, sessionUri, workingDirectory: config.workingDirectories?.[0], + workingDirectories, + multiRootEnabled, managedWorkingDirectory: undefined, mapState: createCodexSessionMapState(new Set(this._serverToolHost?.toolNames ?? []), clientToolSet), pendingCommandApprovals: new PendingRequestRegistry(), @@ -2775,13 +2852,16 @@ export class CodexAgent extends Disposable implements IAgent { * `thread/resume` (`needsResume: true`) — so the prewarm/first-turn flags * are pre-set to their post-materialization values. */ - private _createResumedSessionEntry(sessionId: string, threadId: string, sessionUri: URI, workingDirectory: URI | undefined, model: ModelSelection | undefined): ICodexSession { + private _createResumedSessionEntry(sessionId: string, threadId: string, sessionUri: URI, workingDirectory: URI | undefined, model: ModelSelection | undefined, workingDirectories?: readonly URI[], multiRootEnabled?: boolean): ICodexSession { const clientToolSet = new ActiveClientToolSet(); + const effectiveWorkingDirectories = distinctWorkingDirectories(workingDirectories); return { sessionId, threadId, sessionUri, workingDirectory, + workingDirectories: effectiveWorkingDirectories, + multiRootEnabled: multiRootEnabled ?? (effectiveWorkingDirectories?.length ?? 0) > 1, managedWorkingDirectory: undefined, mapState: createCodexSessionMapState(new Set(this._serverToolHost?.toolNames ?? []), clientToolSet), pendingCommandApprovals: new PendingRequestRegistry(), @@ -2833,12 +2913,21 @@ export class CodexAgent extends Disposable implements IAgent { } const sourceThreadId = sourceRead.thread.id; const sourceTurns = sourceRead.thread.turns ?? []; + const sourceSession = this._sessions.get(AgentSession.id(fork.session)); + const sourcePrimary = sourceRead.thread.cwd ? URI.file(sourceRead.thread.cwd) : config.workingDirectories?.[0]; + const sourceStoredWorkingDirectories = sourceSession?.workingDirectories ?? sourceRead.persistedWorkingDirectories; + const inheritedWorkingDirectories = sourcePrimary + ? distinctWorkingDirectories([sourcePrimary, ...(sourceStoredWorkingDirectories?.slice(1) ?? [])]) + : undefined; + const multiRootEnabled = sourceSession?.multiRootEnabled ?? (inheritedWorkingDirectories?.length ?? 0) > 1; + const runtimeWorkspaceRoots = multiRootEnabled && inheritedWorkingDirectories && inheritedWorkingDirectories.length > 1 + ? distinctAbsolutePaths(inheritedWorkingDirectories.map(directory => directory.fsPath)) + : undefined; // Resolve how many trailing turns to drop so the fork keeps turns up to // and including `fork.turnId`. A live source maps host turn ids to codex // turn ids; a restored source already uses codex ids. Fall back to the // caller-supplied `turnIndex` when the id can't be resolved. - const sourceSession = this._sessions.get(AgentSession.id(fork.session)); const codexTurnId = sourceSession?.codexTurnIdByHostTurnId.get(fork.turnId) ?? fork.turnId; // Reject an unresolvable fork boundary rather than silently keeping the // full history: if neither the mapped codex turn id nor the caller's @@ -2867,6 +2956,10 @@ export class CodexAgent extends Disposable implements IAgent { ); const forkResult = await conn.client.request<'thread/fork', ThreadForkResponse>('thread/fork', { threadId: sourceThreadId, + ...(runtimeWorkspaceRoots?.length ? { + cwd: runtimeWorkspaceRoots[0], + runtimeWorkspaceRoots, + } : {}), ...(model ? { model: model.id } : {}), approvalPolicy, sandbox: sandboxMode, @@ -2900,8 +2993,15 @@ export class CodexAgent extends Disposable implements IAgent { const workingDirectory = forkResult.cwd ? URI.file(forkResult.cwd) : (sourceRead.thread.cwd ? URI.file(sourceRead.thread.cwd) : config.workingDirectories?.[0]); + const forkWorkingDirectories = multiRootEnabled + ? distinctWorkingDirectories( + forkResult.runtimeWorkspaceRoots?.length + ? forkResult.runtimeWorkspaceRoots.map(path => URI.file(path)) + : inheritedWorkingDirectories, + ) + : undefined; - const session = this._createResumedSessionEntry(newThreadId, newThreadId, newSessionUri, workingDirectory, model); + const session = this._createResumedSessionEntry(newThreadId, newThreadId, newSessionUri, workingDirectory, model, forkWorkingDirectories, multiRootEnabled); this._sessions.set(newThreadId, session); this._sessionIdByThreadId.set(newThreadId, newThreadId); // Forked threads skip materialization (the thread already exists), so @@ -3008,8 +3108,11 @@ export class CodexAgent extends Disposable implements IAgent { threadConfig.mcp_servers = mcpServers as JsonValue; this._logService.info(`[Codex] thread/start for session=${session.sessionUri.toString()} with ${mcpServerNames.length} MCP server(s): ${mcpServerNames.join(', ')}`); } - const startResult = await conn.client.request<'thread/start', { thread: { id: string } }>('thread/start', { + const multiRootActive = this._isMultiRootActive(session); + const runtimeWorkspaceRoots = multiRootActive ? this._runtimeWorkspaceRoots(session) : undefined; + const startResult = await conn.client.request<'thread/start', ThreadStartResponse>('thread/start', { cwd: session.workingDirectory.fsPath, + ...(runtimeWorkspaceRoots?.length ? { runtimeWorkspaceRoots } : {}), model: model.id, approvalPolicy, sandbox: sandboxMode, @@ -3018,6 +3121,10 @@ export class CodexAgent extends Disposable implements IAgent { dynamicTools: this._buildDynamicTools(session), }); const threadId = startResult.thread.id; + if (multiRootActive && !session.workingDirectories && startResult.runtimeWorkspaceRoots?.length) { + session.workingDirectories = startResult.runtimeWorkspaceRoots.map(path => URI.file(path)); + session.workingDirectory = session.workingDirectories[0]; + } if (session.disposed) { try { await conn.client.request<'thread/unsubscribe'>('thread/unsubscribe', { threadId }); @@ -3143,11 +3250,20 @@ export class CodexAgent extends Disposable implements IAgent { } // Persist only once the prewarmed thread is claimed by a turn. This // avoids restoring an expired, never-used prewarm as a live session. - void this._metadataStore.write(session.sessionUri, { + const multiRootActive = this._isMultiRootActive(session); + const fields = { threadId: session.threadId, cwd: session.workingDirectory, modelId: session.model?.id, - }); + workingDirectories: multiRootActive ? session.workingDirectories : undefined, + }; + void this._metadataStore.write(session.sessionUri, fields); + if (multiRootActive) { + const canonicalSessionUri = AgentSession.uri(this.id, session.threadId); + if (!isEqual(session.sessionUri, canonicalSessionUri)) { + void this._metadataStore.write(canonicalSessionUri, fields); + } + } } private _claimPrewarm(session: ICodexSession): void { @@ -3165,6 +3281,12 @@ export class CodexAgent extends Disposable implements IAgent { if (session.prewarmClaimed) { if (session.threadId === undefined && !session.materializePromise) { session.workingDirectory = workingDirectory; + if (this._isMultiRootActive(session)) { + session.workingDirectories = distinctWorkingDirectories([ + workingDirectory, + ...(session.workingDirectories?.slice(1) ?? []), + ]); + } } return; } @@ -3225,7 +3347,12 @@ export class CodexAgent extends Disposable implements IAgent { // assign when the send supplied one, so the resume path keeps emitting the // singular working directory. if (workingDirectories) { - session.workingDirectories = workingDirectories; + session.workingDirectories = session.multiRootEnabled && workingDirectories.length > 1 + ? distinctWorkingDirectories([ + session.workingDirectory ?? workingDirectories[0], + ...workingDirectories.slice(1), + ]) + : workingDirectories; } const conn = await this._ensureConnection(); const effectiveTurnId = turnId ?? generateUuid(); @@ -3281,7 +3408,16 @@ export class CodexAgent extends Disposable implements IAgent { // so a resumed thread reconnects auth-gated servers, matching // the config a fresh `thread/start` would apply. const mcpServers = this._buildSessionMcpServers(session); - await conn.client.request<'thread/resume'>('thread/resume', buildCodexResumeParams(this._usageSource, threadId, mcpServers)); + const multiRootActive = this._isMultiRootActive(session); + const runtimeWorkspaceRoots = multiRootActive ? this._runtimeWorkspaceRoots(session) : undefined; + const resumeResult = await conn.client.request<'thread/resume', ThreadResumeResponse>( + 'thread/resume', + buildCodexResumeParams(this._usageSource, threadId, mcpServers, runtimeWorkspaceRoots), + ); + if (multiRootActive && !session.workingDirectories && resumeResult.runtimeWorkspaceRoots?.length) { + session.workingDirectories = resumeResult.runtimeWorkspaceRoots.map(path => URI.file(path)); + session.workingDirectory = session.workingDirectories[0]; + } session.materializedMcpSig = mcpServersSignature(mcpServers); session.needsResume = false; } catch (err) { @@ -3662,10 +3798,14 @@ export class CodexAgent extends Disposable implements IAgent { // thread/resume (Decision 8). The threadId came from the metadata // overlay or from `thread/list` (when the session was materialized // in a prior process); `_readSession` returns the resolved id. + const metadata = this._withWorkingDirectories( + this._threadToMetadata(read.thread, session), + read.persistedWorkingDirectories, + ); if (!this._sessions.has(sessionId)) { const workingDirectory = read.thread.cwd ? URI.file(read.thread.cwd) : undefined; const threadId = read.thread.id; - const restored = this._createResumedSessionEntry(sessionId, threadId, session, workingDirectory, undefined); + const restored = this._createResumedSessionEntry(sessionId, threadId, session, workingDirectory, undefined, metadata.workingDirectories); this._sessions.set(sessionId, restored); this._sessionIdByThreadId.set(threadId, sessionId); if (!isCodexThreadProviderCompatible(this._usageSource, read.thread.modelProvider)) { @@ -3679,10 +3819,10 @@ export class CodexAgent extends Disposable implements IAgent { this._serverToolHost.advertise(restored.sessionUri.toString()); } } - return this._threadToMetadata(read.thread, session); + return metadata; } - private async _readSession(session: URI): Promise { + private async _readSession(session: URI): Promise { // Resolve the codex thread id for this session URI. Resolution // order: in-memory session → persisted metadata overlay → URI host // (for sessions materialized in a prior process where sessionId @@ -3690,9 +3830,11 @@ export class CodexAgent extends Disposable implements IAgent { const sessionId = AgentSession.id(session); const existing = this._sessions.get(sessionId); let threadId = existing?.threadId; + let persistedWorkingDirectories = existing?.workingDirectories; if (threadId === undefined) { const overlay = await this._metadataStore.read(session); threadId = overlay.threadId ?? sessionId; + persistedWorkingDirectories = overlay.workingDirectories; } try { const conn = await this._ensureConnection(); @@ -3700,7 +3842,7 @@ export class CodexAgent extends Disposable implements IAgent { threadId, includeTurns: true, }); - return response; + return { ...response, persistedWorkingDirectories }; } catch (err) { const message = err instanceof Error ? err.message : String(err); // `thread not loaded` is app-server's expected response for any @@ -3747,10 +3889,11 @@ export class CodexAgent extends Disposable implements IAgent { liveUriByThreadId.set(s.threadId, s.sessionUri); } } - return response.data.map(t => this._threadToMetadata( - t, - liveUriByThreadId.get(t.id) ?? AgentSession.uri(this.id, t.id), - )); + return response.data.map(thread => { + const sessionUri = liveUriByThreadId.get(thread.id) ?? AgentSession.uri(this.id, thread.id); + const liveWorkingDirectories = this._sessions.get(AgentSession.id(sessionUri))?.workingDirectories; + return this._withWorkingDirectories(this._threadToMetadata(thread, sessionUri), liveWorkingDirectories); + }); } catch (err) { this._logService.warn(`[Codex] thread/list failed: ${err instanceof Error ? err.message : String(err)}`); return []; @@ -3768,6 +3911,20 @@ export class CodexAgent extends Disposable implements IAgent { }; } + private _withWorkingDirectories(metadata: IAgentSessionMetadata, storedWorkingDirectories: readonly URI[] | undefined): IAgentSessionMetadata { + const primary = metadata.workingDirectories?.[0]; + if (!primary || !storedWorkingDirectories || storedWorkingDirectories.length <= 1) { + return metadata; + } + const workingDirectories = distinctWorkingDirectories([ + primary, + ...storedWorkingDirectories.slice(1), + ]); + return workingDirectories && workingDirectories.length > 1 + ? { ...metadata, workingDirectories } + : metadata; + } + setServerToolHost(host: IAgentServerToolHost): void { this._serverToolHost = host; } diff --git a/src/vs/platform/agentHost/node/codex/codexLaunchConfig.ts b/src/vs/platform/agentHost/node/codex/codexLaunchConfig.ts index 3d508f37f37..ef84bbadde5 100644 --- a/src/vs/platform/agentHost/node/codex/codexLaunchConfig.ts +++ b/src/vs/platform/agentHost/node/codex/codexLaunchConfig.ts @@ -23,10 +23,14 @@ export function isCodexThreadProviderCompatible(usageSource: CodexUsageSource, m } /** Explicitly bind a compatible resumed thread to the current global usage source. */ -export function buildCodexResumeParams(usageSource: CodexUsageSource, threadId: string, mcpServers: Readonly>): ThreadResumeParams { +export function buildCodexResumeParams(usageSource: CodexUsageSource, threadId: string, mcpServers: Readonly>, workingDirectories?: readonly string[]): ThreadResumeParams { return { threadId, modelProvider: usageSource === 'copilot' ? 'vscode-proxy' : 'openai', + ...(workingDirectories?.length ? { + cwd: workingDirectories[0], + runtimeWorkspaceRoots: [...workingDirectories], + } : {}), ...(Object.keys(mcpServers).length > 0 ? { config: { mcp_servers: mcpServers as JsonValue } } : {}), }; } diff --git a/src/vs/platform/agentHost/node/codex/codexSessionMetadataStore.ts b/src/vs/platform/agentHost/node/codex/codexSessionMetadataStore.ts index 14ded96e49e..f8634bdd5a4 100644 --- a/src/vs/platform/agentHost/node/codex/codexSessionMetadataStore.ts +++ b/src/vs/platform/agentHost/node/codex/codexSessionMetadataStore.ts @@ -21,6 +21,8 @@ import { ISessionDataService } from '../../common/sessionDataService.js'; * materialize time. * `codex.cwd` — absolute path to the working directory the * session was created against (URI string). + * Multi-root sessions store a JSON object in this same + * field so single-root reads retain their original shape. * `codex.model` — serialized {@link ModelSelection.id} string, * remembered for restore so resumed sessions reuse * the model picked during the prior process. @@ -30,12 +32,14 @@ export interface ICodexSessionOverlay { readonly threadId?: string; readonly cwd?: URI; readonly modelId?: string; + readonly workingDirectories?: readonly URI[]; } export interface ICodexSessionOverlayUpdate { readonly threadId?: string; readonly cwd?: URI; readonly modelId?: string; + readonly workingDirectories?: readonly URI[]; } export class CodexSessionMetadataStore { @@ -43,7 +47,6 @@ export class CodexSessionMetadataStore { private static readonly KEY_THREAD_ID = 'codex.threadId'; private static readonly KEY_CWD = 'codex.cwd'; private static readonly KEY_MODEL = 'codex.model'; - constructor( @ISessionDataService private readonly _sessionDataService: ISessionDataService, @ILogService private readonly _logService: ILogService, @@ -65,7 +68,10 @@ export class CodexSessionMetadataStore { work.push(db.setMetadata(CodexSessionMetadataStore.KEY_THREAD_ID, fields.threadId)); } if (fields.cwd !== undefined) { - work.push(db.setMetadata(CodexSessionMetadataStore.KEY_CWD, fields.cwd.toString())); + work.push(db.setMetadata( + CodexSessionMetadataStore.KEY_CWD, + serializeCwd(fields.cwd, fields.workingDirectories), + )); } if (fields.modelId !== undefined) { work.push(db.setMetadata(CodexSessionMetadataStore.KEY_MODEL, fields.modelId)); @@ -96,10 +102,12 @@ export class CodexSessionMetadataStore { ref.object.getMetadata(CodexSessionMetadataStore.KEY_CWD), ref.object.getMetadata(CodexSessionMetadataStore.KEY_MODEL), ]); + const cwd = parseCwd(cwdRaw); return { threadId: threadId ?? undefined, - cwd: cwdRaw ? URI.parse(cwdRaw) : undefined, + cwd: cwd.cwd, modelId: modelId ?? undefined, + workingDirectories: cwd.workingDirectories, }; } finally { ref.dispose(); @@ -109,4 +117,41 @@ export class CodexSessionMetadataStore { return {}; } } + +} + +function serializeCwd(cwd: URI, workingDirectories: readonly URI[] | undefined): string { + if (!workingDirectories || workingDirectories.length <= 1) { + return cwd.toString(); + } + return JSON.stringify({ + cwd: cwd.toString(), + workingDirectories: workingDirectories.map(directory => directory.toString()), + }); +} + +function parseCwd(raw: string | undefined): { readonly cwd?: URI; readonly workingDirectories?: readonly URI[] } { + if (!raw) { + return {}; + } + if (!raw.startsWith('{')) { + return { cwd: URI.parse(raw) }; + } + try { + const value: { cwd?: unknown; workingDirectories?: unknown } = JSON.parse(raw); + if (typeof value.cwd !== 'string') { + return {}; + } + const workingDirectories = Array.isArray(value.workingDirectories) + ? value.workingDirectories + .filter((directory): directory is string => typeof directory === 'string') + .map(directory => URI.parse(directory)) + : undefined; + return { + cwd: URI.parse(value.cwd), + workingDirectories: workingDirectories && workingDirectories.length > 1 ? workingDirectories : undefined, + }; + } catch { + return {}; + } } diff --git a/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts b/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts index 07bcdd3033c..6ae23e4db13 100644 --- a/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts +++ b/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts @@ -29,8 +29,8 @@ import { CustomizationType, MessageAttachmentKind, MessageKind, PendingMessageKi import type { IClientTransport, IProtocolTransport } from '../../common/state/sessionTransport.js'; import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; import { TelemetryLevel } from '../../../telemetry/common/telemetry.js'; -import { AgentHostCodexAgentEnabledSettingId, AgentHostCopilotMultiRootEnabledSettingId, AgentHostClaudeMultiRootEnabledSettingId, AgentHostSystemProxyEnabledSettingId } from '../../common/agentService.js'; -import { AgentHostAutoReplyEnabledConfigKey, AgentHostCodexEnabledConfigKey, AgentHostCopilotMultiRootEnabledConfigKey, AgentHostClaudeMultiRootEnabledConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, AgentHostEditTelemetryEnabledConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostSystemProxyEnabledConfigKey, AgentHostTelemetryLevelConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, AUTO_REPLY_SETTING_ID, DISABLE_REPO_INFO_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_ENABLED_SETTING_ID, telemetryLevelToAgentHostConfigValue, TERMINAL_AUTO_APPROVE_SETTING_ID, TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID, type AgentHostTerminalAutoApproveRules } from '../../common/agentHostSchema.js'; +import { AgentHostCodexAgentEnabledSettingId, AgentHostCodexMultiRootEnabledSettingId, AgentHostCopilotMultiRootEnabledSettingId, AgentHostClaudeMultiRootEnabledSettingId, AgentHostSystemProxyEnabledSettingId } from '../../common/agentService.js'; +import { AgentHostAutoReplyEnabledConfigKey, AgentHostCodexEnabledConfigKey, AgentHostCodexMultiRootEnabledConfigKey, AgentHostCopilotMultiRootEnabledConfigKey, AgentHostClaudeMultiRootEnabledConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, AgentHostEditTelemetryEnabledConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostSystemProxyEnabledConfigKey, AgentHostTelemetryLevelConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, AUTO_REPLY_SETTING_ID, DISABLE_REPO_INFO_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_ENABLED_SETTING_ID, telemetryLevelToAgentHostConfigValue, TERMINAL_AUTO_APPROVE_SETTING_ID, TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID, type AgentHostTerminalAutoApproveRules } from '../../common/agentHostSchema.js'; import type { Implementation } from '../../common/state/protocol/common/commands.js'; import { agentsWindowAgentHostClientInfo } from '../../common/agentHostClientInfo.js'; @@ -966,6 +966,23 @@ suite('RemoteAgentHostProtocolClient', () => { assert.deepStrictEqual(getRootConfig(updatedMultiRootEnabled), { [AgentHostClaudeMultiRootEnabledConfigKey]: false }); }); + test('forwards Codex multi-root enablement on connect and when the setting changes', async () => { + const configurationService = new TestConfigurationService({ [AgentHostCodexMultiRootEnabledSettingId]: true }); + const { client, transport } = createClient(disposables.add(new TestProtocolTransport()), createPermissionService(), undefined, new NullLogService(), configurationService); + + await connectClient(client, transport); + + const multiRootEnabled = findRootConfigNotification(transport.sentMessages, AgentHostCodexMultiRootEnabledConfigKey); + assert.deepStrictEqual(getRootConfig(multiRootEnabled), { [AgentHostCodexMultiRootEnabledConfigKey]: true }); + + transport.sentMessages.length = 0; + await configurationService.setUserConfiguration(AgentHostCodexMultiRootEnabledSettingId, false); + fireConfigurationChange(configurationService, AgentHostCodexMultiRootEnabledSettingId); + + const updatedMultiRootEnabled = findLastRootConfigNotification(transport.sentMessages, AgentHostCodexMultiRootEnabledConfigKey); + assert.deepStrictEqual(getRootConfig(updatedMultiRootEnabled), { [AgentHostCodexMultiRootEnabledConfigKey]: false }); + }); + test('forwards auto-reply on connect and when the setting changes', async () => { const configurationService = new TestConfigurationService({ [AUTO_REPLY_SETTING_ID]: true }); const { client, transport } = createClient(disposables.add(new TestProtocolTransport()), createPermissionService(), undefined, new NullLogService(), configurationService); diff --git a/src/vs/platform/agentHost/test/node/codex/codexLaunchConfig.test.ts b/src/vs/platform/agentHost/test/node/codex/codexLaunchConfig.test.ts index a2f144d954a..d5969c3bf13 100644 --- a/src/vs/platform/agentHost/test/node/codex/codexLaunchConfig.test.ts +++ b/src/vs/platform/agentHost/test/node/codex/codexLaunchConfig.test.ts @@ -54,5 +54,11 @@ suite('CodexLaunchConfig', () => { modelProvider: 'vscode-proxy', config: { mcp_servers: { GitHub: { url: 'https://api.githubcopilot.com/mcp/' } } }, }); + assert.deepStrictEqual(buildCodexResumeParams('openai', 'thread-c', {}, ['/repo-a', '/repo-b']), { + threadId: 'thread-c', + modelProvider: 'openai', + cwd: '/repo-a', + runtimeWorkspaceRoots: ['/repo-a', '/repo-b'], + }); }); }); diff --git a/src/vs/platform/agentHost/test/node/codex/codexModelRefresh.test.ts b/src/vs/platform/agentHost/test/node/codex/codexModelRefresh.test.ts index 94f13e9cc44..f75c2479af7 100644 --- a/src/vs/platform/agentHost/test/node/codex/codexModelRefresh.test.ts +++ b/src/vs/platform/agentHost/test/node/codex/codexModelRefresh.test.ts @@ -5,7 +5,6 @@ import type { CCAModel } from '@vscode/copilot-api'; import assert from 'assert'; -import { Event } from '../../../../../base/common/event.js'; import type { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; @@ -14,29 +13,31 @@ import { TestInstantiationService } from '../../../../../platform/instantiation/ import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IAgentHostGitHubEndpointService } from '../../../node/agentHostGitHubEndpointService.js'; -import { IAgentConfigurationService } from '../../../node/agentConfigurationService.js'; +import { AgentConfigurationService, IAgentConfigurationService } from '../../../node/agentConfigurationService.js'; +import { AgentHostStateManager } from '../../../node/agentHostStateManager.js'; import { IAgentSdkDownloader } from '../../../node/agentSdkDownloader.js'; import { CodexAgent } from '../../../node/codex/codexAgent.js'; import { ICodexProxyService } from '../../../node/codex/codexProxyService.js'; import { ICopilotApiService } from '../../../node/shared/copilotApiService.js'; import { ISessionDataService } from '../../../common/sessionDataService.js'; import { createTestGitHubEndpointService } from '../testGitHubEndpointService.js'; +import { AgentHostCodexMultiRootEnabledConfigKey } from '../../../common/agentHostSchema.js'; -function createAgent(disposables: Pick, models: () => Promise): CodexAgent { +function createAgent(disposables: Pick, models: () => Promise, rootConfig: Record = {}): CodexAgent { const instantiationService = new TestInstantiationService(); + const logService = new NullLogService(); + const stateManager = disposables.add(new AgentHostStateManager(logService)); + const configurationService = disposables.add(new AgentConfigurationService(stateManager, logService)); + configurationService.updateRootConfig(rootConfig); instantiationService.stub(ISessionDataService, { _serviceBrand: undefined }); instantiationService.stub(ICopilotApiService, { _serviceBrand: undefined, models }); instantiationService.stub(ICodexProxyService, { _serviceBrand: undefined }); - instantiationService.stub(IAgentConfigurationService, { - _serviceBrand: undefined, - onDidRootConfigChange: Event.None, - getRootValue: () => undefined, - }); + instantiationService.stub(IAgentConfigurationService, configurationService); instantiationService.stub(IAgentHostGitHubEndpointService, createTestGitHubEndpointService()); instantiationService.stub(IAgentSdkDownloader, { _serviceBrand: undefined }); instantiationService.stub(IProductService, { _serviceBrand: undefined, version: '1.0.0-test' } as IProductService); instantiationService.stub(INativeEnvironmentService, { userHome: URI.file('/tmp') }); - instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(ILogService, logService); return disposables.add(instantiationService.createInstance(CodexAgent)); } @@ -62,4 +63,19 @@ suite('CodexAgent model refresh', () => { assert.deepStrictEqual(agent.models.get().map(model => model.id), ['gpt-5.5']); }); + + test('advertises multiple working directories only while enabled', () => { + const agent = createAgent(disposables, async () => []); + const disabledByDefault = agent.getDescriptor().capabilities?.multipleWorkingDirectories; + agent['_configurationService'].updateRootConfig({ [AgentHostCodexMultiRootEnabledConfigKey]: true }); + const whenEnabled = agent.getDescriptor().capabilities?.multipleWorkingDirectories; + agent['_configurationService'].updateRootConfig({ [AgentHostCodexMultiRootEnabledConfigKey]: false }); + const afterDisabling = agent.getDescriptor().capabilities?.multipleWorkingDirectories; + + assert.deepStrictEqual({ disabledByDefault, whenEnabled, afterDisabling }, { + disabledByDefault: undefined, + whenEnabled: { immutablePrimary: true }, + afterDisabling: undefined, + }); + }); }); diff --git a/src/vs/platform/agentHost/test/node/codex/codexPrewarmEviction.test.ts b/src/vs/platform/agentHost/test/node/codex/codexPrewarmEviction.test.ts index 91a48a964ed..50d840ea13c 100644 --- a/src/vs/platform/agentHost/test/node/codex/codexPrewarmEviction.test.ts +++ b/src/vs/platform/agentHost/test/node/codex/codexPrewarmEviction.test.ts @@ -6,9 +6,11 @@ import type { CCAModel } from '@vscode/copilot-api'; import assert from 'assert'; import { PassThrough } from 'stream'; -import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Emitter } from '../../../../../base/common/event.js'; import type { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; +import { sep } from '../../../../../base/common/path.js'; +import { isWindows } from '../../../../../base/common/platform.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { INativeEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; @@ -17,7 +19,8 @@ import { IProductService } from '../../../../../platform/product/common/productS import { AgentSession } from '../../../common/agentService.js'; import { buildDefaultChatUri } from '../../../common/state/sessionState.js'; import { ISessionDataService } from '../../../common/sessionDataService.js'; -import { IAgentConfigurationService } from '../../../node/agentConfigurationService.js'; +import { AgentConfigurationService, IAgentConfigurationService } from '../../../node/agentConfigurationService.js'; +import { AgentHostStateManager } from '../../../node/agentHostStateManager.js'; import { IAgentHostGitHubEndpointService } from '../../../node/agentHostGitHubEndpointService.js'; import { IAgentSdkDownloader } from '../../../node/agentSdkDownloader.js'; import { CodexAgent } from '../../../node/codex/codexAgent.js'; @@ -25,6 +28,10 @@ import { CodexAppServerClient, type ICodexAppServerTransport } from '../../../no import { ICodexProxyService } from '../../../node/codex/codexProxyService.js'; import { ICopilotApiService } from '../../../node/shared/copilotApiService.js'; import { createTestGitHubEndpointService } from '../testGitHubEndpointService.js'; +import { AgentHostCodexMultiRootEnabledConfigKey } from '../../../common/agentHostSchema.js'; +import { CodexSessionConfigKey } from '../../../common/codexSessionConfigKeys.js'; +import type { SandboxPolicy } from '../../../node/codex/protocol/generated/v2/SandboxPolicy.js'; +import { createSessionDataService, TestSessionDatabase } from '../../common/sessionTestHelpers.js'; interface ITestWireRequest { readonly id: number; @@ -32,6 +39,8 @@ interface ITestWireRequest { readonly params: { readonly cwd?: string; readonly threadId?: string; + readonly runtimeWorkspaceRoots?: readonly string[]; + readonly sandboxPolicy?: SandboxPolicy; }; } @@ -98,25 +107,46 @@ function readNextRequest(stream: PassThrough): Promise { }); } -async function createAgent(disposables: Pick): Promise { +interface ICreateAgentOptions { + readonly multiRootEnabled?: boolean; + readonly sessionConfig?: Readonly>; + readonly database?: TestSessionDatabase; +} + +class TestCodexConfigurationService extends AgentConfigurationService { + constructor( + stateManager: AgentHostStateManager, + logService: NullLogService, + private sessionConfig: Readonly> | undefined, + ) { + super(stateManager, logService); + } + + setSessionConfig(sessionConfig: Readonly>): void { + this.sessionConfig = sessionConfig; + } + + override getSessionConfigValues(): Record | undefined { + return this.sessionConfig ? { ...this.sessionConfig } : undefined; + } +} + +async function createAgent(disposables: Pick, options: ICreateAgentOptions = {}): Promise { const models = [{ id: 'gpt-test', name: 'GPT Test', supported_endpoints: ['/responses'] }] as CCAModel[]; const instantiationService = new TestInstantiationService(); - instantiationService.stub(ISessionDataService, { _serviceBrand: undefined }); + const logService = new NullLogService(); + const stateManager = disposables.add(new AgentHostStateManager(logService)); + const configurationService = disposables.add(new TestCodexConfigurationService(stateManager, logService, options.sessionConfig)); + configurationService.updateRootConfig({ [AgentHostCodexMultiRootEnabledConfigKey]: options.multiRootEnabled }); + instantiationService.stub(ISessionDataService, createSessionDataService(options.database)); instantiationService.stub(ICopilotApiService, { _serviceBrand: undefined, models: async () => models }); instantiationService.stub(ICodexProxyService, { _serviceBrand: undefined }); - instantiationService.stub(IAgentConfigurationService, { - _serviceBrand: undefined, - onDidRootConfigChange: Event.None, - getRootValue: () => undefined, - getSessionConfigValues: () => undefined, - isWorkingDirectoryPending: () => false, - updateRootConfig: () => { }, - }); + instantiationService.stub(IAgentConfigurationService, configurationService); instantiationService.stub(IAgentHostGitHubEndpointService, createTestGitHubEndpointService()); instantiationService.stub(IAgentSdkDownloader, { _serviceBrand: undefined, isSdkResolvableWithoutDownload: async () => true }); instantiationService.stub(IProductService, { _serviceBrand: undefined, version: '1.0.0-test' } as IProductService); instantiationService.stub(INativeEnvironmentService, { userHome: URI.file('/tmp') }); - instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(ILogService, logService); const agent = disposables.add(instantiationService.createInstance(CodexAgent)); await agent.authenticate(agent.getProtectedResources()[0].resource, 'test-token'); await agent.refreshModels(); @@ -206,4 +236,376 @@ suite('CodexAgent prewarm eviction', () => { test('waits for and evicts an in-flight folder prewarm when the first send resolves to a worktree', async () => { await assertPrewarmEvictedOnSend(disposables, false); }); + + test('multi-root start and turn separate workspace roots from additional writable directories', async () => { + const additionalDirectory = URI.file('/manual-write').fsPath; + const sessionUri = AgentSession.uri('codex', 'multi-root'); + const agent = await createAgent(disposables, { + multiRootEnabled: true, + sessionConfig: { [CodexSessionConfigKey.AdditionalDirectories]: [additionalDirectory, `${additionalDirectory}${sep}`] }, + }); + const peer = disposables.add(createTestPeer()); + const client = new CodexAppServerClient(peer.transport); + agent['_connection'] = { + kind: 'ready', + client, + usageSource: 'github', + child: { kill: () => true }, + } as never; + agent['_refreshSkillHookCustomizations'] = async () => { }; + agent['_refreshSkillExtraRoots'] = async () => { }; + const repoA = URI.file('/repo-a'); + const repoB = URI.file('/repo-b'); + const duplicateRepoA = URI.file(`${repoA.fsPath}${sep}`); + const caseVariantRepoA = URI.file(repoA.fsPath.toUpperCase()); + + try { + const workingDirectories = [repoA, duplicateRepoA, ...(isWindows ? [caseVariantRepoA] : []), repoB]; + const { session } = await agent.createSession({ session: sessionUri, workingDirectories, model: { id: 'gpt-test' } }); + const entry = agent['_sessions'].get(AgentSession.id(session))!; + const start = await readNextRequest(peer.outbound); + peer.push({ id: start.id, result: { thread: { id: 'thread' }, runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath] } }); + await entry.materializePromise; + + const send = agent.chats.sendMessage(URI.parse(buildDefaultChatUri(session)), 'hello', workingDirectories, undefined, 'turn-1'); + const turn = await readNextRequest(peer.outbound); + peer.push({ id: turn.id, result: {} }); + await send; + const configurationService = agent['_configurationService']; + assert.ok(configurationService instanceof TestCodexConfigurationService); + configurationService.setSessionConfig({ [CodexSessionConfigKey.PermissionsPreset]: 'full-access' }); + const fullAccess = agent['_turnStartOptions'](entry, 'gpt-test'); + configurationService.setSessionConfig({ [CodexSessionConfigKey.SandboxMode]: 'read-only' }); + const readOnly = agent['_turnStartOptions'](entry, 'gpt-test'); + + assert.deepStrictEqual({ + start: { cwd: start.params.cwd, runtimeWorkspaceRoots: start.params.runtimeWorkspaceRoots }, + turn: { + runtimeWorkspaceRoots: turn.params.runtimeWorkspaceRoots, + sandboxPolicy: turn.params.sandboxPolicy, + }, + fullAccess: { + runtimeWorkspaceRoots: fullAccess.runtimeWorkspaceRoots, + sandboxPolicy: fullAccess.sandboxPolicy, + }, + readOnly: { + runtimeWorkspaceRoots: readOnly.runtimeWorkspaceRoots, + sandboxPolicy: readOnly.sandboxPolicy, + }, + }, { + start: { cwd: repoA.fsPath, runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath] }, + turn: { + runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath], + sandboxPolicy: { + type: 'workspaceWrite', + writableRoots: [repoA.fsPath, repoB.fsPath, additionalDirectory], + networkAccess: false, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false, + }, + }, + fullAccess: { + runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath], + sandboxPolicy: { type: 'dangerFullAccess' }, + }, + readOnly: { + runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath], + sandboxPolicy: { type: 'readOnly', networkAccess: false }, + }, + }); + } finally { + peer.exit(); + } + }); + + test('disabled multi-root preserves the existing additional-directory payload', async () => { + const additionalDirectory = URI.file('/manual-write').fsPath; + const sessionUri = AgentSession.uri('codex', 'single-root'); + const agent = await createAgent(disposables, { + sessionConfig: { [CodexSessionConfigKey.AdditionalDirectories]: [additionalDirectory] }, + }); + const peer = disposables.add(createTestPeer()); + const client = new CodexAppServerClient(peer.transport); + agent['_connection'] = { + kind: 'ready', + client, + usageSource: 'github', + child: { kill: () => true }, + } as never; + agent['_refreshSkillHookCustomizations'] = async () => { }; + agent['_refreshSkillExtraRoots'] = async () => { }; + const repoA = URI.file('/repo-a'); + const repoB = URI.file('/repo-b'); + + try { + const { session } = await agent.createSession({ session: sessionUri, workingDirectories: [repoA, repoB], model: { id: 'gpt-test' } }); + const entry = agent['_sessions'].get(AgentSession.id(session))!; + const start = await readNextRequest(peer.outbound); + peer.push({ id: start.id, result: { thread: { id: 'thread' } } }); + await entry.materializePromise; + + const send = agent.chats.sendMessage(URI.parse(buildDefaultChatUri(session)), 'hello', [repoA], undefined, 'turn-1'); + const turn = await readNextRequest(peer.outbound); + peer.push({ id: turn.id, result: {} }); + await send; + + assert.deepStrictEqual({ + startRuntimeWorkspaceRoots: start.params.runtimeWorkspaceRoots, + turnRuntimeWorkspaceRoots: turn.params.runtimeWorkspaceRoots, + writableRoots: turn.params.sandboxPolicy?.type === 'workspaceWrite' ? turn.params.sandboxPolicy.writableRoots : undefined, + }, { + startRuntimeWorkspaceRoots: undefined, + turnRuntimeWorkspaceRoots: [repoA.fsPath, additionalDirectory], + writableRoots: [repoA.fsPath, additionalDirectory], + }); + } finally { + peer.exit(); + } + }); + + test('enabled multi-root preserves single-folder protocol and sandbox behavior', async () => { + const additionalDirectory = `${URI.file('/manual-write').fsPath}${sep}`; + const sessionUri = AgentSession.uri('codex', 'enabled-single-root'); + const agent = await createAgent(disposables, { + multiRootEnabled: true, + sessionConfig: { [CodexSessionConfigKey.AdditionalDirectories]: [additionalDirectory] }, + }); + const peer = disposables.add(createTestPeer()); + const client = new CodexAppServerClient(peer.transport); + agent['_connection'] = { + kind: 'ready', + client, + usageSource: 'github', + child: { kill: () => true }, + } as never; + agent['_refreshSkillHookCustomizations'] = async () => { }; + agent['_refreshSkillExtraRoots'] = async () => { }; + const repo = URI.file('/repo'); + + try { + const { session } = await agent.createSession({ session: sessionUri, workingDirectories: [repo], model: { id: 'gpt-test' } }); + const entry = agent['_sessions'].get(AgentSession.id(session))!; + const start = await readNextRequest(peer.outbound); + peer.push({ id: start.id, result: { thread: { id: 'thread' } } }); + await entry.materializePromise; + + const send = agent.chats.sendMessage(URI.parse(buildDefaultChatUri(session)), 'hello', [repo], undefined, 'turn-1'); + const turn = await readNextRequest(peer.outbound); + peer.push({ id: turn.id, result: {} }); + await send; + const configurationService = agent['_configurationService']; + assert.ok(configurationService instanceof TestCodexConfigurationService); + configurationService.setSessionConfig({ [CodexSessionConfigKey.PermissionsPreset]: 'full-access' }); + const fullAccess = agent['_turnStartOptions'](entry, 'gpt-test'); + configurationService.setSessionConfig({ [CodexSessionConfigKey.SandboxMode]: 'read-only' }); + const readOnly = agent['_turnStartOptions'](entry, 'gpt-test'); + + assert.deepStrictEqual({ + start: { + cwd: start.params.cwd, + runtimeWorkspaceRoots: start.params.runtimeWorkspaceRoots, + }, + turn: { + runtimeWorkspaceRoots: turn.params.runtimeWorkspaceRoots, + sandboxPolicy: turn.params.sandboxPolicy, + }, + fullAccess: { + runtimeWorkspaceRoots: fullAccess.runtimeWorkspaceRoots, + sandboxPolicy: fullAccess.sandboxPolicy, + }, + readOnly: { + runtimeWorkspaceRoots: readOnly.runtimeWorkspaceRoots, + sandboxPolicy: readOnly.sandboxPolicy, + }, + }, { + start: { + cwd: repo.fsPath, + runtimeWorkspaceRoots: undefined, + }, + turn: { + runtimeWorkspaceRoots: [repo.fsPath, additionalDirectory], + sandboxPolicy: { + type: 'workspaceWrite', + writableRoots: [repo.fsPath, additionalDirectory], + networkAccess: false, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false, + }, + }, + fullAccess: { + runtimeWorkspaceRoots: undefined, + sandboxPolicy: { type: 'dangerFullAccess' }, + }, + readOnly: { + runtimeWorkspaceRoots: undefined, + sandboxPolicy: { type: 'readOnly', networkAccess: false }, + }, + }); + } finally { + peer.exit(); + } + }); + + test('fork inherits the source workspace roots instead of requested replacements', async () => { + const agent = await createAgent(disposables, { multiRootEnabled: true }); + const peer = disposables.add(createTestPeer()); + const client = new CodexAppServerClient(peer.transport); + agent['_connection'] = { + kind: 'ready', + client, + usageSource: 'github', + child: { kill: () => true }, + } as never; + agent['_refreshSkillHookCustomizations'] = async () => { }; + agent['_refreshSkillExtraRoots'] = async () => { }; + const repoA = URI.file('/repo-a'); + const repoB = URI.file('/repo-b'); + const requestedA = URI.file('/requested-a'); + const requestedB = URI.file('/requested-b'); + + try { + const source = await agent.createSession({ workingDirectories: [repoA, repoB], model: { id: 'gpt-test' } }); + const sourceEntry = agent['_sessions'].get(AgentSession.id(source.session))!; + const start = await readNextRequest(peer.outbound); + peer.push({ id: start.id, result: { thread: { id: 'source-thread' }, cwd: repoA.fsPath, runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath] } }); + await sourceEntry.materializePromise; + + const forkPromise = agent.createSession({ + workingDirectories: [requestedA, requestedB], + fork: { session: source.session, turnId: 'turn-1', turnIndex: 0 }, + }); + const read = await readNextRequest(peer.outbound); + peer.push({ + id: read.id, + result: { + thread: { + id: 'source-thread', + cwd: repoA.fsPath, + turns: [{ id: 'turn-1' }], + }, + }, + }); + const fork = await readNextRequest(peer.outbound); + peer.push({ + id: fork.id, + result: { + thread: { id: 'fork-thread', cwd: repoA.fsPath }, + cwd: repoA.fsPath, + runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath], + }, + }); + const forked = await forkPromise; + const forkedEntry = agent['_sessions'].get(AgentSession.id(forked.session))!; + + assert.deepStrictEqual({ + request: { + method: fork.method, + cwd: fork.params.cwd, + runtimeWorkspaceRoots: fork.params.runtimeWorkspaceRoots, + }, + workingDirectories: forkedEntry.workingDirectories?.map(directory => directory.fsPath), + }, { + request: { + method: 'thread/fork', + cwd: repoA.fsPath, + runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath], + }, + workingDirectories: [repoA.fsPath, repoB.fsPath], + }); + } finally { + peer.exit(); + } + }); + + test('cold resume restores persisted workspace roots', async () => { + const database = new TestSessionDatabase(); + const repoA = URI.file('/repo-a'); + const repoB = URI.file('/repo-b'); + const agentA = await createAgent(disposables, { multiRootEnabled: true, database }); + const peerA = disposables.add(createTestPeer()); + agentA['_connection'] = { + kind: 'ready', + client: new CodexAppServerClient(peerA.transport), + usageSource: 'github', + child: { kill: () => true }, + } as never; + agentA['_refreshSkillHookCustomizations'] = async () => { }; + agentA['_refreshSkillExtraRoots'] = async () => { }; + let peerB: ITestPeer | undefined; + + try { + const created = await agentA.createSession({ workingDirectories: [repoA, repoB], model: { id: 'gpt-test' } }); + const entry = agentA['_sessions'].get(AgentSession.id(created.session))!; + const start = await readNextRequest(peerA.outbound); + peerA.push({ id: start.id, result: { thread: { id: 'thread' }, cwd: repoA.fsPath, runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath] } }); + await entry.materializePromise; + const firstSend = agentA.chats.sendMessage(URI.parse(buildDefaultChatUri(created.session)), 'hello', [repoA, repoB], undefined, 'turn-1'); + const firstTurn = await readNextRequest(peerA.outbound); + peerA.push({ id: firstTurn.id, result: {} }); + await firstSend; + await new Promise(resolve => setImmediate(resolve)); + const canonicalOverlay = await agentA['_metadataStore'].read(AgentSession.uri('codex', 'thread')); + + const agentB = await createAgent(disposables, { multiRootEnabled: true, database }); + peerB = disposables.add(createTestPeer()); + agentB['_connection'] = { + kind: 'ready', + client: new CodexAppServerClient(peerB.transport), + usageSource: 'github', + child: { kill: () => true }, + } as never; + agentB['_refreshSkillHookCustomizations'] = async () => { }; + agentB['_refreshSkillExtraRoots'] = async () => { }; + + const metadataPromise = agentB.getSessionMetadata(created.session); + const read = await readNextRequest(peerB.outbound); + peerB.push({ + id: read.id, + result: { + thread: { + id: 'thread', + cwd: repoA.fsPath, + modelProvider: 'vscode-proxy', + turns: [], + }, + }, + }); + const metadata = await metadataPromise; + + const resumedSend = agentB.chats.sendMessage(URI.parse(buildDefaultChatUri(created.session)), 'again', undefined, undefined, 'turn-2'); + const resume = await readNextRequest(peerB.outbound); + peerB.push({ + id: resume.id, + result: { + thread: { id: 'thread', cwd: repoA.fsPath }, + cwd: repoA.fsPath, + runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath], + }, + }); + const resumedTurn = await readNextRequest(peerB.outbound); + peerB.push({ id: resumedTurn.id, result: {} }); + await resumedSend; + + assert.deepStrictEqual({ + canonicalOverlay: canonicalOverlay.workingDirectories?.map(directory => directory.fsPath), + metadata: metadata?.workingDirectories?.map(directory => directory.fsPath), + resume: { + cwd: resume.params.cwd, + runtimeWorkspaceRoots: resume.params.runtimeWorkspaceRoots, + }, + turnRuntimeWorkspaceRoots: resumedTurn.params.runtimeWorkspaceRoots, + }, { + canonicalOverlay: [repoA.fsPath, repoB.fsPath], + metadata: [repoA.fsPath, repoB.fsPath], + resume: { + cwd: repoA.fsPath, + runtimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath], + }, + turnRuntimeWorkspaceRoots: [repoA.fsPath, repoB.fsPath], + }); + } finally { + peerB?.exit(); + peerA.exit(); + } + }); }); diff --git a/src/vs/platform/agentHost/test/node/codex/codexSessionMetadataStore.test.ts b/src/vs/platform/agentHost/test/node/codex/codexSessionMetadataStore.test.ts new file mode 100644 index 00000000000..ca716da1542 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/codex/codexSessionMetadataStore.test.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { NullLogService } from '../../../../../platform/log/common/log.js'; +import { CodexSessionMetadataStore } from '../../../node/codex/codexSessionMetadataStore.js'; +import { createSessionDataService, TestSessionDatabase } from '../../common/sessionTestHelpers.js'; + +suite('CodexSessionMetadataStore', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('round trips working directories', async () => { + const store = new CodexSessionMetadataStore(createSessionDataService(), new NullLogService()); + const session = URI.parse('codex:/session'); + const workingDirectories = [URI.file('/repo-a'), URI.file('/repo-b')]; + + await store.write(session, { threadId: 'thread', cwd: workingDirectories[0], workingDirectories }); + + const overlay = await store.read(session); + assert.deepStrictEqual({ + threadId: overlay.threadId, + cwd: overlay.cwd?.toString(), + workingDirectories: overlay.workingDirectories?.map(directory => directory.toString()), + }, { + threadId: 'thread', + cwd: workingDirectories[0].toString(), + workingDirectories: workingDirectories.map(directory => directory.toString()), + }); + }); + + test('ignores malformed working directory metadata', async () => { + const database = new TestSessionDatabase(); + await database.setMetadata('codex.cwd', '{"cwd":'); + const store = new CodexSessionMetadataStore(createSessionDataService(database), new NullLogService()); + + const overlay = await store.read(URI.parse('codex:/session')); + + assert.deepStrictEqual({ cwd: overlay.cwd, workingDirectories: overlay.workingDirectories }, { + cwd: undefined, + workingDirectories: undefined, + }); + }); +}); From 4f5ed4db9d8bfc7d9907c777cdf72c10872f52d7 Mon Sep 17 00:00:00 2001 From: Paul <8560030+pwang347@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:07:50 -0700 Subject: [PATCH 13/86] Ensure independent Copilot packages for EH (#327842) --- build/lib/copilot.ts | 151 +++++++++++++++++++++++++++------ build/lib/npmPackage.ts | 27 ++++++ build/lib/test/copilot.test.ts | 103 ++++++++++++++++++++++ 3 files changed, 256 insertions(+), 25 deletions(-) diff --git a/build/lib/copilot.ts b/build/lib/copilot.ts index 9ec67047ef0..412e862d7dc 100644 --- a/build/lib/copilot.ts +++ b/build/lib/copilot.ts @@ -4,8 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; -import { ensureNpmPackage, type EnsureNpmPackageOptions } from './npmPackage.ts'; +import { ensureNpmPackage, materializeNpmPackageVersion, type EnsureNpmPackageOptions } from './npmPackage.ts'; + +/** + * Options for {@link prepareBuiltInCopilotRipgrepShim}. Extends the npm packing + * options with an override for the extension lockfile used to verify natives + * fetched for the pinned version (defaults to the repo's copy; overridable in + * tests). + */ +export interface PrepareBuiltInCopilotOptions extends EnsureNpmPackageOptions { + extensionLockfilePath?: string; +} /** * The platforms that @github/copilot ships platform-specific packages for. @@ -232,7 +243,7 @@ export function ensureCopilotPlatformPackage(platform: string, arch: string, nod * Failures throw to fail the build because built-in packaging must guarantee * this artifact is present. */ -export function prepareBuiltInCopilotRipgrepShim(platform: string, arch: string, builtInCopilotExtensionDir: string, appNodeModulesDir: string): void { +export function prepareBuiltInCopilotRipgrepShim(platform: string, arch: string, builtInCopilotExtensionDir: string, appNodeModulesDir: string, options: PrepareBuiltInCopilotOptions = {}): void { const { nodePlatform, nodeArch } = toNodePlatformArch(platform, arch); const platformArch = `${nodePlatform}-${nodeArch}`; const copilotPackagePlatformArch = toCopilotPackagePlatformArch(platform, arch); @@ -244,7 +255,7 @@ export function prepareBuiltInCopilotRipgrepShim(platform: string, arch: string, if (!fs.existsSync(copilotSdkBase)) { throw new Error(`[prepareBuiltInCopilotRipgrepShim] Copilot SDK directory not found at ${copilotSdkBase}`); } - materializeBuiltInCopilotSdkPlatformFiles(copilotPackagePlatformArch, tgrepPlatformArch, copilotBase, appNodeModulesDir); + materializeBuiltInCopilotSdkPlatformFiles(copilotPackagePlatformArch, tgrepPlatformArch, copilotBase, appNodeModulesDir, options); pruneNonTargetCopilotSdkPrebuilds(copilotPackagePlatformArch, path.join(copilotSdkBase, 'prebuilds'), copilotPlatforms); pruneNonTargetCopilotSdkPrebuilds(tgrepPlatformArch, path.join(copilotSdkBase, path.join('tgrep', 'bin')), copilotTgrepPlatforms); pruneNonTargetCopilotSdkPrebuilds(tgrepPlatformArch, path.join(copilotBase, path.join('tgrep', 'bin')), copilotTgrepPlatforms); @@ -277,37 +288,127 @@ export function prepareBuiltInCopilotRipgrepShim(platform: string, arch: string, } } -function materializeBuiltInCopilotSdkPlatformFiles(copilotPackagePlatformArch: string, tgrepPlatformArch: string, copilotBase: string, appNodeModulesDir: string): void { +function materializeBuiltInCopilotSdkPlatformFiles(copilotPackagePlatformArch: string, tgrepPlatformArch: string, copilotBase: string, appNodeModulesDir: string, options: PrepareBuiltInCopilotOptions = {}): void { if (!copilotPlatforms.includes(copilotPackagePlatformArch)) { return; } - const platformPackageDir = path.join(appNodeModulesDir, '@github', `copilot-${copilotPackagePlatformArch}`); - if (!fs.existsSync(platformPackageDir)) { - throw new Error(`[prepareBuiltInCopilotRipgrepShim] Copilot platform package not found at ${platformPackageDir}`); + // The SDK JavaScript shipped inside the built-in extension and the native + // `runtime.node` it loads MUST be the same @github/copilot version: the JS + // calls native functions the binary may not export (e.g. a newer CLI that + // removed one), which throws at load. Source the native from a platform + // package matching the EXTENSION's version rather than whatever app-root + // currently has — the extension is intentionally pinned to a fixed CLI + // version for the extension host while the agent host (app-root) keeps + // updating, so the two versions diverge by design. + const extVersion = readCopilotPackageVersion(copilotBase); + const { dir: platformPackageDir, cleanup } = resolveVersionMatchedCopilotPlatformPackage(copilotPackagePlatformArch, extVersion, appNodeModulesDir, options); + try { + copyRequiredDirectory( + path.join(platformPackageDir, 'prebuilds', copilotPackagePlatformArch), + path.join(copilotBase, 'sdk', 'prebuilds', copilotPackagePlatformArch), + `Copilot SDK native prebuilds for ${copilotPackagePlatformArch}` + ); + + if (!copilotTgrepPlatforms.includes(tgrepPlatformArch)) { + return; + } + + const tgrepSource = path.join(platformPackageDir, 'tgrep', 'bin', tgrepPlatformArch); + copyRequiredDirectory( + tgrepSource, + path.join(copilotBase, 'tgrep', 'bin', tgrepPlatformArch), + `Copilot tgrep for ${tgrepPlatformArch}` + ); + copyRequiredDirectory( + tgrepSource, + path.join(copilotBase, 'sdk', 'tgrep', 'bin', tgrepPlatformArch), + `Copilot SDK tgrep for ${tgrepPlatformArch}` + ); + } finally { + cleanup(); + } +} + +/** + * Resolves a `@github/copilot-{platform}` package directory whose version + * matches `extVersion`, so the native copied into the built-in extension always + * matches the extension's own SDK JavaScript. + * + * Prefers the app-root package when it already matches (no extra work), and + * otherwise fetches the exact extension version into a temp dir. The extension + * is pinned to a fixed CLI version for the extension host while the agent host + * (app-root) keeps updating, so app-root will normally NOT match and the fetch + * is the expected path once the two versions diverge. The fetched tarball is + * verified against the SHA-512 the extension lockfile pins for that version + * before extraction; resolution fails closed if that integrity is missing. + */ +function resolveVersionMatchedCopilotPlatformPackage(copilotPackagePlatformArch: string, extVersion: string, appNodeModulesDir: string, options: PrepareBuiltInCopilotOptions): { dir: string; cleanup: () => void } { + const noop = () => { }; + const packageName = `@github/copilot-${copilotPackagePlatformArch}`; + + const appRootDir = path.join(appNodeModulesDir, '@github', `copilot-${copilotPackagePlatformArch}`); + if (readOptionalPackageVersion(appRootDir) === extVersion) { + return { dir: appRootDir, cleanup: noop }; } - copyRequiredDirectory( - path.join(platformPackageDir, 'prebuilds', copilotPackagePlatformArch), - path.join(copilotBase, 'sdk', 'prebuilds', copilotPackagePlatformArch), - `Copilot SDK native prebuilds for ${copilotPackagePlatformArch}` - ); + const integrity = resolvePinnedPlatformPackageIntegrity(packageName, extVersion, options); + const staged = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-copilot-native-')); + try { + const stagedPackageDir = path.join(staged, `copilot-${copilotPackagePlatformArch}`); + materializeNpmPackageVersion(packageName, extVersion, stagedPackageDir, integrity, options); + console.log(`[prepareBuiltInCopilotRipgrepShim] ${packageName} in app-root does not match the built-in extension's @github/copilot@${extVersion}; using the version-matched package instead.`); + return { dir: stagedPackageDir, cleanup: () => fs.rmSync(staged, { recursive: true, force: true }) }; + } catch (err) { + fs.rmSync(staged, { recursive: true, force: true }); + throw err; + } +} - if (!copilotTgrepPlatforms.includes(tgrepPlatformArch)) { - return; +/** + * Reads the `sha512-...` integrity the built-in extension's lockfile pins for + * `packageName` at `extVersion`. Fails closed: a missing lockfile, entry, + * version mismatch, or integrity means the fetched native cannot be verified, + * so the build must stop rather than ship an unverified binary. + */ +function resolvePinnedPlatformPackageIntegrity(packageName: string, extVersion: string, options: PrepareBuiltInCopilotOptions): string { + const lockfilePath = options.extensionLockfilePath ?? path.join(import.meta.dirname, '..', '..', 'extensions', 'copilot', 'package-lock.json'); + + let lock: { packages?: Record }; + try { + lock = JSON.parse(fs.readFileSync(lockfilePath, 'utf8')); + } catch (err) { + throw new Error(`[prepareBuiltInCopilotRipgrepShim] Could not read ${lockfilePath} to verify ${packageName}@${extVersion}: ${err instanceof Error ? err.message : String(err)}`); } - const tgrepSource = path.join(platformPackageDir, 'tgrep', 'bin', tgrepPlatformArch); - copyRequiredDirectory( - tgrepSource, - path.join(copilotBase, 'tgrep', 'bin', tgrepPlatformArch), - `Copilot tgrep for ${tgrepPlatformArch}` - ); - copyRequiredDirectory( - tgrepSource, - path.join(copilotBase, 'sdk', 'tgrep', 'bin', tgrepPlatformArch), - `Copilot SDK tgrep for ${tgrepPlatformArch}` - ); + const entry = lock.packages?.[path.posix.join('node_modules', packageName)]; + if (!entry) { + throw new Error(`[prepareBuiltInCopilotRipgrepShim] ${packageName} is not recorded in ${lockfilePath}; refusing to fetch an unverifiable native.`); + } + if (entry.version !== extVersion) { + throw new Error(`[prepareBuiltInCopilotRipgrepShim] ${packageName} is pinned to ${entry.version} in ${lockfilePath} but the built-in extension is @github/copilot@${extVersion}; refusing to fetch an unverifiable native.`); + } + if (!entry.integrity) { + throw new Error(`[prepareBuiltInCopilotRipgrepShim] ${packageName}@${extVersion} has no integrity in ${lockfilePath}; refusing to fetch an unverifiable native.`); + } + return entry.integrity; +} + +function readCopilotPackageVersion(copilotBase: string): string { + const version = readOptionalPackageVersion(copilotBase); + if (!version) { + throw new Error(`[prepareBuiltInCopilotRipgrepShim] Could not read a version from ${path.join(copilotBase, 'package.json')}`); + } + return version; +} + +function readOptionalPackageVersion(packageDir: string): string | undefined { + try { + const version = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')).version; + return typeof version === 'string' ? version : undefined; + } catch { + return undefined; + } } function copyRequiredDirectory(source: string, target: string, description: string): void { diff --git a/build/lib/npmPackage.ts b/build/lib/npmPackage.ts index 1be92590f78..6de7f17ccc3 100644 --- a/build/lib/npmPackage.ts +++ b/build/lib/npmPackage.ts @@ -54,6 +54,33 @@ export function ensureNpmPackage(packageName: string, nodeModulesRoot = 'node_mo } } +/** + * Materializes a SPECIFIC version of an npm package into `targetDir`, replacing + * any existing contents. Unlike {@link ensureNpmPackage}, the version is passed + * explicitly rather than read from the adjacent lockfile — use for build-time + * payloads whose required version does not match that lockfile. Pass + * `expectedIntegrity` (the `sha512-...` recorded for that version in the + * relevant lockfile) to verify the fetched tarball before extraction. + */ +export function materializeNpmPackageVersion(packageName: string, version: string, targetDir: string, expectedIntegrity: string | undefined, options: EnsureNpmPackageOptions = {}): void { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-npm-package-')); + try { + const tarballPath = (options.packPackage ?? packNpmPackage)(packageName, version, tempDir); + verifyNpmIntegrity(tarballPath, expectedIntegrity); + + fs.rmSync(targetDir, { recursive: true, force: true }); + fs.mkdirSync(targetDir, { recursive: true }); + extract({ file: tarballPath, cwd: targetDir, strip: 1, sync: true }); + console.log(`[materializeNpmPackageVersion] Materialized ${packageName}@${version} in ${targetDir}`); + } catch (err) { + fs.rmSync(targetDir, { recursive: true, force: true }); + throw new Error(`[materializeNpmPackageVersion] Failed to materialize ${packageName}@${version}: ${err instanceof Error ? err.message : String(err)}`); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} + + function packNpmPackage(packageName: string, version: string, tempDir: string): string { execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['pack', `${packageName}@${version}`, '--pack-destination', tempDir, '--silent'], { stdio: 'pipe', shell: process.platform === 'win32' }); diff --git a/build/lib/test/copilot.test.ts b/build/lib/test/copilot.test.ts index b87fbb7f2ef..043ed3c45bc 100644 --- a/build/lib/test/copilot.test.ts +++ b/build/lib/test/copilot.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { createHash } from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -11,6 +12,25 @@ import { suite, test } from 'node:test'; import { create } from 'tar'; import { copilotPlatforms, ensureCopilotPlatformPackage, getCopilotExcludeFilter, getCopilotRuntimePrebuildFiles, getMxcExcludeFilter, prepareBuiltInCopilotRipgrepShim } from '../copilot.ts'; +/** + * Builds a fake `@github/copilot-win32-x64@1.0.73` tarball on disk and returns + * its path plus the `sha512-...` integrity of its bytes, so a test can pin that + * integrity in a lockfile the build verifies against. + */ +function createPinnedCopilotWin32Tarball(dir: string): { tarball: string; integrity: string } { + const stage = fs.mkdtempSync(path.join(dir, 'pkg-')); + const packageRoot = path.join(stage, 'package'); + fs.mkdirSync(path.join(packageRoot, 'prebuilds', 'win32-x64'), { recursive: true }); + fs.mkdirSync(path.join(packageRoot, 'tgrep', 'bin', 'win32-x64'), { recursive: true }); + fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ version: '1.0.73' })); + fs.writeFileSync(path.join(packageRoot, 'prebuilds', 'win32-x64', 'runtime.node'), 'EXT-NATIVE-1.0.73'); + fs.writeFileSync(path.join(packageRoot, 'tgrep', 'bin', 'win32-x64', 'tgrep.exe'), 'EXT-TGREP-1.0.73'); + const tarball = path.join(stage, 'copilot-win32-x64.tgz'); + create({ file: tarball, cwd: stage, gzip: true, sync: true }, ['package']); + const integrity = 'sha512-' + createHash('sha512').update(fs.readFileSync(tarball)).digest('base64'); + return { tarball, integrity }; +} + suite('copilot', () => { test('keeps the public copilot platform package include list scoped to the selected package', () => { const files = getCopilotRuntimePrebuildFiles('linux', 'x64'); @@ -186,7 +206,9 @@ suite('copilot', () => { fs.mkdirSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'linux-x64'), { recursive: true }); fs.writeFileSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'linux-x64', 'runtime.node'), ''); + fs.writeFileSync(path.join(extensionCopilotDir, 'package.json'), JSON.stringify({ version: '1.0.73' })); fs.mkdirSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty'), { recursive: true }); + fs.writeFileSync(path.join(platformPackageDir, 'package.json'), JSON.stringify({ version: '1.0.73' })); fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'runtime.node'), ''); fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty.node'), ''); fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty', 'OpenConsole.exe'), ''); @@ -209,6 +231,87 @@ suite('copilot', () => { } }); + test('materializes a version-matched native when app-root diverges from the pinned extension', () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-copilot-sdk-pinned-test-')); + try { + const builtInCopilotExtensionDir = path.join(repoRoot, 'extensions', 'copilot'); + const extensionCopilotDir = path.join(builtInCopilotExtensionDir, 'node_modules', '@github', 'copilot'); + const appNodeModulesDir = path.join(repoRoot, 'node_modules'); + const platformPackageDir = path.join(appNodeModulesDir, '@github', 'copilot-win32-x64'); + + // Extension pinned at 1.0.73. + fs.mkdirSync(path.join(extensionCopilotDir, 'sdk'), { recursive: true }); + fs.writeFileSync(path.join(extensionCopilotDir, 'package.json'), JSON.stringify({ version: '1.0.73' })); + + // App-root updated ahead of the pinned extension — its (mismatched) native must NOT be used. + fs.mkdirSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64'), { recursive: true }); + fs.writeFileSync(path.join(platformPackageDir, 'package.json'), JSON.stringify({ version: '9.9.9-canary' })); + fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'runtime.node'), 'CANARY-NATIVE'); + fs.mkdirSync(path.join(platformPackageDir, 'tgrep', 'bin', 'win32-x64'), { recursive: true }); + fs.writeFileSync(path.join(platformPackageDir, 'tgrep', 'bin', 'win32-x64', 'tgrep.exe'), 'CANARY-TGREP'); + + fs.mkdirSync(path.join(appNodeModulesDir, '@vscode', 'ripgrep-universal', 'bin', 'win32-x64'), { recursive: true }); + fs.writeFileSync(path.join(appNodeModulesDir, '@vscode', 'ripgrep-universal', 'bin', 'win32-x64', 'rg.exe'), ''); + + // Pin the fetched tarball's integrity in the extension lockfile the build verifies against. + const { tarball, integrity } = createPinnedCopilotWin32Tarball(repoRoot); + const extensionLockfilePath = path.join(builtInCopilotExtensionDir, 'package-lock.json'); + fs.writeFileSync(extensionLockfilePath, JSON.stringify({ + packages: { 'node_modules/@github/copilot-win32-x64': { version: '1.0.73', integrity } } + })); + + const packCalls: { packageName: string; version: string }[] = []; + prepareBuiltInCopilotRipgrepShim('win32', 'x64', builtInCopilotExtensionDir, appNodeModulesDir, { + extensionLockfilePath, + packPackage: (packageName, version) => { + packCalls.push({ packageName, version }); + return tarball; + } + }); + + // The version-matched (1.0.73) native was fetched and used — not app-root's canary. + assert.deepStrictEqual(packCalls, [{ packageName: '@github/copilot-win32-x64', version: '1.0.73' }]); + assert.strictEqual( + fs.readFileSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'runtime.node'), 'utf8'), + 'EXT-NATIVE-1.0.73' + ); + assert.strictEqual( + fs.readFileSync(path.join(extensionCopilotDir, 'sdk', 'tgrep', 'bin', 'win32-x64', 'tgrep.exe'), 'utf8'), + 'EXT-TGREP-1.0.73' + ); + } finally { + fs.rmSync(repoRoot, { recursive: true, force: true }); + } + }); + + test('refuses to ship a fetched native that does not match the pinned extension lockfile integrity', () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-copilot-sdk-integrity-test-')); + try { + const builtInCopilotExtensionDir = path.join(repoRoot, 'extensions', 'copilot'); + const extensionCopilotDir = path.join(builtInCopilotExtensionDir, 'node_modules', '@github', 'copilot'); + const appNodeModulesDir = path.join(repoRoot, 'node_modules'); + + fs.mkdirSync(path.join(extensionCopilotDir, 'sdk'), { recursive: true }); + fs.writeFileSync(path.join(extensionCopilotDir, 'package.json'), JSON.stringify({ version: '1.0.73' })); + fs.mkdirSync(path.join(appNodeModulesDir, '@vscode', 'ripgrep-universal', 'bin', 'win32-x64'), { recursive: true }); + fs.writeFileSync(path.join(appNodeModulesDir, '@vscode', 'ripgrep-universal', 'bin', 'win32-x64', 'rg.exe'), ''); + + const { tarball } = createPinnedCopilotWin32Tarball(repoRoot); + const extensionLockfilePath = path.join(builtInCopilotExtensionDir, 'package-lock.json'); + // Lockfile pins a DIFFERENT (tampered) integrity than the fetched tarball. + fs.writeFileSync(extensionLockfilePath, JSON.stringify({ + packages: { 'node_modules/@github/copilot-win32-x64': { version: '1.0.73', integrity: `sha512-${'A'.repeat(88)}` } } + })); + + assert.throws(() => prepareBuiltInCopilotRipgrepShim('win32', 'x64', builtInCopilotExtensionDir, appNodeModulesDir, { + extensionLockfilePath, + packPackage: () => tarball + }), /integrity mismatch/); + } finally { + fs.rmSync(repoRoot, { recursive: true, force: true }); + } + }); + test('strips all copilot platform packages for unsupported armhf builds', () => { assert.deepStrictEqual( getCopilotExcludeFilter('linux', 'armhf'), From 9178d7ee2f29e29f0361a2693780e090910db27f Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:23:22 -0700 Subject: [PATCH 14/86] ah: clean up ask questions ux (#328164) * ah: clean up ask questions ux * address feedback --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../agentHost/common/agentHostSchema.ts | 2 + .../node/copilot/copilotAgentSession.ts | 67 +++--- .../test/node/copilotAgentSession.test.ts | 30 ++- .../agentHost/agentHostSessionHandler.ts | 4 +- .../agentHost/stateToProgressAdapter.ts | 54 ++++- .../chatCollapsibleContentPart.ts | 2 + .../chatQuestionCarouselPart.ts | 212 +++++++++++++++++- .../media/chatQuestionCarousel.css | 165 ++++++++++++++ .../chatToolPartUtilities.ts | 6 +- .../chat/browser/widget/chatListRenderer.ts | 21 +- .../chat/common/chatService/chatService.ts | 6 +- .../chatQuestionCarouselData.ts | 8 +- .../stateToProgressAdapter.test.ts | 109 ++++++++- .../chatQuestionCarouselPart.test.ts | 159 ++++++++++++- .../chatToolProgressPart.test.ts | 22 +- .../browser/widget/chatListRenderer.test.ts | 34 ++- .../model/chatQuestionCarouselData.test.ts | 11 +- 17 files changed, 857 insertions(+), 55 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHostSchema.ts b/src/vs/platform/agentHost/common/agentHostSchema.ts index bc928c8f769..a6167a4ce61 100644 --- a/src/vs/platform/agentHost/common/agentHostSchema.ts +++ b/src/vs/platform/agentHost/common/agentHostSchema.ts @@ -451,6 +451,8 @@ export const GLOBAL_AUTO_APPROVE_SETTING_ID = 'chat.tools.global.autoApprove'; */ export const AgentHostAutoReplyEnabledConfigKey = 'autoReplyEnabled'; +export const AgentHostAutoReplyAnswer = 'The user is not available to answer your question. Choose a pragmatic option best aligned with the context of the request.'; + /** * The VS Code setting ID for auto-reply. Defined here so renderer-side * agent-host clients can forward it without importing from `workbench/contrib/chat`. diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index bc5c841f534..f66c7a32ac1 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -31,7 +31,7 @@ import { CopilotCliConfigKey, applyModelFamilyAlias, copilotCliConfigSchema } fr import type { ChatInputRequestWithPlanReview, IAgentHostPlanReviewAction } from '../../common/agentHostPlanReview.js'; import { gitHubMcpServerUrl } from '../../common/githubEndpoints.js'; import { AgentHostSandboxConfigKey, sandboxConfigSchema } from '../../common/sandboxConfigSchema.js'; -import { AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyEnabledConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, platformRootSchema, platformSessionSchema } from '../../common/agentHostSchema.js'; +import { AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyAnswer, AgentHostAutoReplyEnabledConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, platformRootSchema, platformSessionSchema } from '../../common/agentHostSchema.js'; import { AgentSession, AgentSignal, AuthenticateParams, IMcpNotification, IRestoredSubagentSession, subagentChatTitle, type IAgentToolPendingConfirmationSignal } from '../../common/agentService.js'; import { META_DIFF_BASE_BRANCH } from '../../common/agentHostGitService.js'; import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js'; @@ -2936,10 +2936,50 @@ export class CopilotAgentSession extends Disposable { request: UserInputRequest, _invocation: { sessionId: string }, ): Promise { + const requestId = generateUuid(); + const questionId = generateUuid(); + const inputRequest: ChatInputRequest = { + id: requestId, + questions: [request.choices && request.choices.length > 0 + ? { + kind: ChatInputQuestionKind.SingleSelect, + id: questionId, + message: request.question, + required: true, + options: request.choices.map(c => ({ id: c, label: c })), + allowFreeformInput: request.allowFreeform ?? true, + } + : { + kind: ChatInputQuestionKind.Text, + id: questionId, + message: request.question, + required: true, + }, + ], + }; + const isAutopilot = this._isAutopilotMode(); if (isAutopilot || this._isAutoReplyEnabled()) { + this._emitAction({ + type: ActionType.ChatInputRequested, + request: inputRequest, + }); + this._emitAction({ + type: ActionType.ChatInputCompleted, + requestId, + response: ChatInputResponseKind.Accept, + answers: { + [questionId]: { + state: ChatInputAnswerState.Submitted, + value: { + kind: ChatInputAnswerValueKind.Text, + value: AgentHostAutoReplyAnswer, + }, + }, + }, + }); return { - answer: 'The user is not available to answer your question. Choose a pragmatic option best aligned with the context of the request.', + answer: AgentHostAutoReplyAnswer, wasFreeform: true, }; } @@ -2950,33 +2990,10 @@ export class CopilotAgentSession extends Disposable { const questionPreview = request.question.substring(0, 100); try { - const requestId = generateUuid(); - const questionId = generateUuid(); this._logService.info(`[Copilot:${this.sessionId}] User input request: requestId=${requestId}, question="${questionPreview}"`); const pendingInput = this._pendingUserInputs.register(requestId, { questionId }); - // Build the protocol ChatInputRequest from the SDK's simple format - const inputRequest: ChatInputRequest = { - id: requestId, - questions: [request.choices && request.choices.length > 0 - ? { - kind: ChatInputQuestionKind.SingleSelect, - id: questionId, - message: request.question, - required: true, - options: request.choices.map(c => ({ id: c, label: c })), - allowFreeformInput: request.allowFreeform ?? true, - } - : { - kind: ChatInputQuestionKind.Text, - id: questionId, - message: request.question, - required: true, - }, - ], - }; - this._emitAction({ type: ActionType.ChatInputRequested, request: inputRequest, diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index b968dd140f5..944735eed85 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -5345,7 +5345,7 @@ suite('CopilotAgentSession', () => { }); }); - test('autopilot auto-answers a free-form question without firing a progress event', async () => { + test('autopilot auto-answers a free-form question and records it in history', async () => { const { runtime, signals } = await createAgentSession(disposables, { configValues: { [SessionConfigKey.Mode]: 'autopilot' }, }); @@ -5360,7 +5360,18 @@ suite('CopilotAgentSession', () => { // the user typed something custom. assert.strictEqual(result.answer, 'The user is not available to answer your question. Choose a pragmatic option best aligned with the context of the request.'); assert.strictEqual(result.wasFreeform, true); - assert.strictEqual(signals.length, 0); + assert.deepStrictEqual(getActions(signals).map(action => action.type), [ + ActionType.ChatInputRequested, + ActionType.ChatInputCompleted, + ]); + const completed = getActions(signals)[1]; + assert.deepStrictEqual(completed.type === ActionType.ChatInputCompleted ? Object.values(completed.answers ?? {}) : [], [{ + state: ChatInputAnswerState.Submitted, + value: { + kind: ChatInputAnswerValueKind.Text, + value: result.answer, + }, + }]); }); test('autopilot does not auto-answer when mode is not "autopilot"', async () => { @@ -5383,7 +5394,7 @@ suite('CopilotAgentSession', () => { assert.ok(isAction(signals[0], ActionType.ChatInputRequested)); }); - test('auto-reply auto-answers a question without firing a progress event', async () => { + test('auto-reply auto-answers a question and records it in history', async () => { // `chat.autoReply` is forwarded as the autoReplyEnabled root config. // Even in interactive mode it must short-circuit like autopilot. const { runtime, signals } = await createAgentSession(disposables, { @@ -5398,7 +5409,18 @@ suite('CopilotAgentSession', () => { assert.strictEqual(result.answer, 'The user is not available to answer your question. Choose a pragmatic option best aligned with the context of the request.'); assert.strictEqual(result.wasFreeform, true); - assert.strictEqual(signals.length, 0); + assert.deepStrictEqual(getActions(signals).map(action => action.type), [ + ActionType.ChatInputRequested, + ActionType.ChatInputCompleted, + ]); + const completed = getActions(signals)[1]; + assert.deepStrictEqual(completed.type === ActionType.ChatInputCompleted ? Object.values(completed.answers ?? {}) : [], [{ + state: ChatInputAnswerState.Submitted, + value: { + kind: ChatInputAnswerValueKind.Text, + value: result.answer, + }, + }]); }); }); 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 7f64369dba4..d37bb3ae266 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts @@ -99,7 +99,7 @@ import { buildHostLocalEventsPath } from '../../copilotCliEventsUri.js'; import { toolDataToDefinition } from './agentHostToolUtils.js'; import { IAgentHostUntitledProvisionalSessionService } from './agentHostUntitledProvisionalSessionService.js'; import { IAgentHostImportConversationStore } from './agentHostImportConversationStore.js'; -import { activeTurnToProgress, BOOLEAN_TRUE_OPTION_ID, completedToolCallToEditParts, completedToolCallToSerialized, convertProtocolAnswers, convertProtocolPlanReviewResult, createInputRequestCarousel, createInputRequestPlanReview, finalizeToolInvocation, formatTurnResponseDetails, getTerminalContent, getUrlInputRequestPresentation, isSubagentTool, makeAhpTerminalToolSessionId, messageAttachmentsToVariableData, messageToVariableData, parseAhpTerminalToolSessionId, rewriteAgentHostLinkTarget, stringOrMarkdownToString, systemNotificationToChatPart, toolCallAuthenticationServer, toolCallConfirmationMessages, toolCallStateToInvocation, toolCallStateToPreparedInvocation, toolCallStateToStreamingInvocation, turnsToHistory, updateRunningToolSpecificData, usageInfoToAutoModeResolution, usageInfoToChatUsage, usageInfoToQuotas, type IAgentHostToolInvocationOptions, type IToolCallFileEdit, type TurnModelLookup } from './stateToProgressAdapter.js'; +import { activeTurnToProgress, BOOLEAN_TRUE_OPTION_ID, completedToolCallToEditParts, completedToolCallToSerialized, containsAutomaticReplyAnswer, convertProtocolAnswers, convertProtocolPlanReviewResult, createInputRequestCarousel, createInputRequestPlanReview, finalizeToolInvocation, formatTurnResponseDetails, getTerminalContent, getUrlInputRequestPresentation, isSubagentTool, makeAhpTerminalToolSessionId, messageAttachmentsToVariableData, messageToVariableData, parseAhpTerminalToolSessionId, rewriteAgentHostLinkTarget, stringOrMarkdownToString, systemNotificationToChatPart, toolCallAuthenticationServer, toolCallConfirmationMessages, toolCallStateToInvocation, toolCallStateToPreparedInvocation, toolCallStateToStreamingInvocation, turnsToHistory, updateRunningToolSpecificData, usageInfoToAutoModeResolution, usageInfoToChatUsage, usageInfoToQuotas, type IAgentHostToolInvocationOptions, type IToolCallFileEdit, type TurnModelLookup } from './stateToProgressAdapter.js'; import { resolveMcpServerAuthentication, agentHostMcpServerId } from './agentHostAuth.js'; export { toolDataToDefinition }; @@ -3285,6 +3285,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC carousel.data = carouselAnswers ?? {}; carousel.isUsed = true; carousel.answeredExternally = part.response === ChatInputResponseKind.Accept && !carouselAnswers; + carousel.autoReply = containsAutomaticReplyAnswer(protocolAnswers); + carousel.answeredExternally ||= carousel.autoReply; carousel.draftAnswers = undefined; carousel.draftCurrentIndex = undefined; carousel.draftCollapsed = undefined; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts index fcf690f499b..523dea21220 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts @@ -19,6 +19,7 @@ import { readToolCallMeta } from '../../../../../../platform/agentHost/common/me import { getChatErrorDetailsFromMeta, IChatErrorContext } from '../../../common/chatErrorMessages.js'; import { AGENT_HOST_SCHEME, toAgentHostUri } from '../../../../../../platform/agentHost/common/agentHostUri.js'; import { AgentHostElementAttachmentDisplayKind, getElementAttachmentCorrelationId } from '../../../../../../platform/agentHost/common/meta/agentElementAttachments.js'; +import { AgentHostAutoReplyAnswer } from '../../../../../../platform/agentHost/common/agentHostSchema.js'; import { getAgentFeedbackAttachmentMetadata, isAgentFeedbackAnnotationsAttachment, isAgentFeedbackAttachment } from '../../../../../../platform/agentHost/common/meta/agentFeedbackAttachments.js'; import { getBrowserViewAttachmentMetadata, isBrowserViewAttachment } from '../../../../../../platform/agentHost/common/meta/browserViewAttachments.js'; import { isViewUnreviewedCommentsTool, isAddCommentTool } from '../../../../../../platform/agentHost/common/meta/agentFeedbackAnnotations.js'; @@ -47,6 +48,22 @@ import { restoreChatReferenceVariableEntryFromAttachment } from './agentHostChat export const BOOLEAN_TRUE_OPTION_ID = 'true'; export const BOOLEAN_FALSE_OPTION_ID = 'false'; +const agentHostAskUserToolNames = new Set(['ask_user', 'AskUserQuestion', 'request_user_input']); + +function isAgentHostAskUserTool(toolName: string): boolean { + return agentHostAskUserToolNames.has(toolName); +} + +function shouldHideCompletedAgentHostAskUserTool(toolCall: ToolCallState): boolean { + if (!isAgentHostAskUserTool(toolCall.toolName)) { + return false; + } + if (toolCall.status === ToolCallStatus.Completed) { + return toolCall.success; + } + return toolCall.status === ToolCallStatus.Cancelled && toolCall.reason === ToolCallCancellationReason.Skipped; +} + export interface IAgentHostToolInvocationOptions { readonly currentClientId: string; readonly cancelOtherClientToolCall: (toolCall: ToolCallState) => void; @@ -110,6 +127,14 @@ export function convertProtocolAnswers(raw: Record | un return Object.keys(answers).length > 0 ? answers : undefined; } +export function containsAutomaticReplyAnswer(raw: Record | undefined): boolean { + return Object.values(raw ?? {}).some(answer => + answer.state === ChatInputAnswerState.Submitted + && answer.value.kind === ChatInputAnswerValueKind.Text + && answer.value.value === AgentHostAutoReplyAnswer + ); +} + function getPlanReviewAction(planReview: IAgentHostPlanReview, actionId: string | undefined) { return actionId ? planReview.actions.find(action => action.id === actionId) : undefined; } @@ -221,7 +246,7 @@ export function createInputRequestCarousel(inputReq: ChatInputRequest, connectio }); } - return new ChatQuestionCarouselData( + const carousel = new ChatQuestionCarouselData( questions, true, inputReq.id, @@ -229,6 +254,8 @@ export function createInputRequestCarousel(inputReq: ChatInputRequest, connectio undefined, inputReq.message ? rawMarkdownToString(inputReq.message, connectionAuthority) : undefined, ); + carousel.answerPresentation = 'conversation'; + return carousel; } export function createInputRequestPlanReview(inputReq: ChatInputRequest, planReview: IAgentHostPlanReview): ChatPlanReviewData { @@ -297,7 +324,8 @@ export function inputRequestResponsePartToProgress(part: InputRequestResponsePar : undefined; carousel.data = answers ?? {}; carousel.isUsed = true; - carousel.answeredExternally = part.response === ChatInputResponseKind.Accept && !answers; + carousel.autoReply = containsAutomaticReplyAnswer(inputReq.answers); + carousel.answeredExternally = part.response === ChatInputResponseKind.Accept && (carousel.autoReply || !answers); return carousel; } @@ -1655,7 +1683,7 @@ export function completedToolCallToSerialized(tc: ICompletedToolCall, subAgentIn pastTenseMessage: isTerminal ? undefined : pastTenseMsg, isConfirmed: completedToolCallConfirmedReason(tc), isComplete: true, - presentation: undefined, + presentation: shouldHideCompletedAgentHostAskUserTool(tc) ? ToolInvocationPresentation.HiddenAfterComplete : undefined, subAgentInvocationId: subAgentInvocationId, toolSpecificData, resultDetails, @@ -2160,6 +2188,10 @@ export function toolCallStateToInvocation(tc: ToolCallState, subAgentInvocationI const invocation = new ChatToolInvocation(undefined, toolData, tc.toolCallId, subAgentInvocationId, undefined); invocation.invocationMessage = stringOrMarkdownToString(tc.invocationMessage, connectionAuthority) ?? tc.displayName; + if (isAgentHostAskUserTool(tc.toolName)) { + invocation.invocationMessage = localize('agentHost.askUser.waiting', "Waiting for answer..."); + invocation.presentation = ToolInvocationPresentation.HiddenAfterComplete; + } if (tc.status === ToolCallStatus.AuthRequired) { invocation.setAuthenticationRequired(toolCallAuthenticationServer(tc, mcpServerAuthority)); } @@ -2263,6 +2295,10 @@ export function toolCallStateToStreamingInvocation(tc: ToolCallState, subAgentIn }, subagentInvocationId: subAgentInvocationId, }); + if (isAgentHostAskUserTool(tc.toolName)) { + invocation.invocationMessage = localize('agentHost.askUser.asking', "Asking a question..."); + invocation.presentation = ToolInvocationPresentation.HiddenAfterComplete; + } if (sessionResource && isSubagentTool(tc)) { invocation.toolSpecificData = toolCallStateToInvocation(tc, subAgentInvocationId, sessionResource, connectionAuthority ?? '', mcpServerAuthority).toolSpecificData; } @@ -2299,6 +2335,10 @@ export function updateRunningToolSpecificData(existing: ChatToolInvocation, tc: return; } existing.invocationMessage = stringOrMarkdownToString(tc.invocationMessage, connectionAuthority) ?? existing.invocationMessage; + if (isAgentHostAskUserTool(tc.toolName)) { + existing.invocationMessage = localize('agentHost.askUser.waiting', "Waiting for answer..."); + existing.presentation = ToolInvocationPresentation.HiddenAfterComplete; + } if (isAddCommentTool(tc.toolName)) { existing.invocationMessage = addCommentReference(tc) ?? existing.invocationMessage; } @@ -2414,6 +2454,9 @@ export function finalizeToolInvocation(invocation: ChatToolInvocation, tc: ToolC if (isAddCommentTool(tc.toolName)) { invocation.invocationMessage = addCommentReference(tc) ?? invocation.invocationMessage; } + if (isAgentHostAskUserTool(tc.toolName)) { + invocation.presentation = ToolInvocationPresentation.HiddenAfterComplete; + } // Check for subagent content — set toolSpecificData so the UI renders a subagent widget if (isCompleted) { @@ -2496,6 +2539,11 @@ export function finalizeToolInvocation(invocation: ChatToolInvocation, tc: ToolC const errorMessage = isCompleted ? tc.error?.message : (isCancelled ? tc.reasonMessage : undefined); const errorString = typeof errorMessage === 'string' ? errorMessage : errorMessage?.markdown; const fileEdits = isCompleted ? fileEditsToExternalEdits(tc) : []; + if (isAgentHostAskUserTool(tc.toolName)) { + invocation.presentation = shouldHideCompletedAgentHostAskUserTool(tc) + ? ToolInvocationPresentation.HiddenAfterComplete + : undefined; + } // Hide the tool widget when file edits are shown separately via onFileEdits if (fileEdits.length > 0 && !isFailure) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts index 4674e1070ea..218bfa9aba9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts @@ -36,6 +36,7 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I protected readonly hasFollowingContent: boolean; protected _isExpanded = observableValue(this, false); protected _collapseButton: ButtonWithIcon | undefined; + protected _hoverChevron: HTMLElement | undefined; private readonly _overrideIcon = observableValue(this, undefined); protected readonly _showCheckmarks: IObservable; @@ -106,6 +107,7 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I // Add hover chevron indicator on the right (decorative, hide from screen readers) const hoverChevron = $('span.chat-collapsible-hover-chevron.codicon.codicon-chevron-right', { 'aria-hidden': 'true' }); + this._hoverChevron = hoverChevron; collapseButton.element.appendChild(hoverChevron); if (this.hoverMessage) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index 8010f0a04cd..39b90561c15 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -11,6 +11,7 @@ import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../. import { KeyCode } from '../../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { isMacintosh } from '../../../../../../base/common/platform.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { generateUuid } from '../../../../../../base/common/uuid.js'; import { hasKey } from '../../../../../../base/common/types.js'; import { localize } from '../../../../../../nls.js'; @@ -38,6 +39,8 @@ import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { ITerminalChatService } from '../../../../terminal/browser/terminal.js'; +import { AgentHostAutoReplyAnswer } from '../../../../../../platform/agentHost/common/agentHostSchema.js'; +import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js'; import './media/chatQuestionCarousel.css'; const PREVIOUS_QUESTION_ACTION_ID = 'workbench.action.chat.previousQuestion'; @@ -47,6 +50,63 @@ export interface IChatQuestionCarouselOptions { shouldAutoFocus?: boolean; } +class ChatQuestionAnswerCollapsiblePart extends ChatCollapsibleContentPart { + constructor( + title: string, + private readonly prefix: string | undefined, + private readonly value: string, + private readonly answerIcon: ThemeIcon, + context: IChatContentPartRenderContext, + private readonly contentFactory: (() => HTMLElement) | undefined, + private readonly onDidChangeHeight: () => void, + hoverService: IHoverService, + configurationService: IConfigurationService, + ) { + super(title, context, undefined, hoverService, configurationService); + } + + protected override init(): HTMLElement { + const element = super.init(); + element.classList.toggle('chat-question-answer-expandable', !!this.contentFactory); + if (this._collapseButton) { + const labelElement = this._collapseButton.labelElement; + labelElement.textContent = ''; + const icon = dom.$('span.chat-question-summary-answer-icon'); + icon.classList.add(...ThemeIcon.asClassNameArray(this.answerIcon)); + icon.setAttribute('aria-hidden', 'true'); + const value = dom.$('span.chat-question-summary-answer-value'); + value.textContent = this.value; + this._register(this.hoverService.setupDelayedHover(value, { content: this.value })); + labelElement.appendChild(icon); + if (this.prefix) { + const prefix = dom.$('span.chat-question-summary-prefix'); + prefix.textContent = this.prefix; + labelElement.append(prefix, labelElement.ownerDocument.createTextNode(' ')); + } + labelElement.appendChild(value); + if (!this.contentFactory) { + this._collapseButton.element.tabIndex = -1; + this._collapseButton.element.setAttribute('aria-disabled', 'true'); + this._collapseButton.element.removeAttribute('aria-expanded'); + this._hoverChevron?.remove(); + } + } + return element; + } + + protected override initContent(): HTMLElement { + return this.contentFactory?.() ?? dom.$('.chat-question-summary-empty-content'); + } + + protected override expansionDidChange(): void { + this.onDidChangeHeight(); + } + + hasSameContent(_other: IChatRendererContent, _followingContent: IChatRendererContent[], _element: ChatTreeItem): boolean { + return false; + } +} + export class ChatQuestionCarouselPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; @@ -92,7 +152,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent constructor( public readonly carousel: IChatQuestionCarousel, - context: IChatContentPartRenderContext, + private readonly _context: IChatContentPartRenderContext, private readonly _options: IChatQuestionCarouselOptions, @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, @IHoverService private readonly _hoverService: IHoverService, @@ -106,6 +166,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent super(); this.domNode = dom.$('.chat-question-carousel-container'); + this.domNode.classList.toggle('chat-question-carousel-conversation', carousel.answerPresentation === 'conversation'); this.domNode.id = generateUuid(); this._inChatQuestionCarouselContextKey = ChatContextKeys.inChatQuestionCarousel.bindTo(this._contextKeyService); this._chatQuestionCarouselHasTerminalContextKey = ChatContextKeys.chatQuestionCarouselHasTerminal.bindTo(this._contextKeyService); @@ -152,7 +213,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // If carousel was already used OR the response is complete, show summary of answers // When response is complete, the carousel can no longer be interacted with - const responseIsComplete = isResponseVM(context.element) && context.element.isComplete; + const responseIsComplete = isResponseVM(this._context.element) && this._context.element.isComplete; if (carousel.isUsed || responseIsComplete) { this._isSkipped = true; this.domNode.classList.add('chat-question-carousel-used'); @@ -401,6 +462,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent * Hides the carousel UI and shows a summary of answers. */ private hideAndShowSummary(): void { + if (this._store.isDisposed) { + return; + } + this._isSkipped = true; this.domNode.classList.add('chat-question-carousel-used'); @@ -1558,7 +1623,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const skippedMessage = dom.$('.chat-question-summary-skipped'); skippedMessage.textContent = isDismissedByTerminal ? localize('chat.questionCarousel.deferredToTerminal', "Deferring to user's input in the terminal") - : localize('chat.questionCarousel.skipped', 'Skipped'); + : localize('chat.questionCarousel.skipped', 'Skipped question'); summaryContainer.appendChild(skippedMessage); } this.domNode.appendChild(summaryContainer); @@ -1570,12 +1635,34 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private renderSummary(): void { // If no answers, show the terminal-state (Skipped/Answered) message if (this._answers.size === 0) { + if (this.carousel.answerPresentation === 'conversation') { + if (this.carousel.autoReply) { + this.renderConversationSummary({ + answerFallback: localize('chat.questionCarousel.answeredAutomatically', "Answered automatically"), + answerIcon: Codicon.copilotCompact, + }); + } else if (this.carousel.answeredExternally) { + this.renderTerminalStateMessage(); + } else if (this.carousel.isUsed) { + this.renderConversationSummary({ + answerFallback: localize('chat.questionCarousel.skippedConversation', "Skipped question"), + answerIcon: Codicon.closeCompact, + hideAnswerPrefix: true, + }); + } + return; + } if (this.carousel.isUsed) { this.renderTerminalStateMessage(); } return; } + if (this.carousel.answerPresentation === 'conversation') { + this.renderConversationSummary(); + return; + } + const summaryContainer = dom.$('.chat-question-carousel-summary'); for (const question of this.carousel.questions) { @@ -1607,10 +1694,129 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this.domNode.appendChild(summaryContainer); } + private renderConversationSummary(options?: { answerFallback?: string; answerIcon?: ThemeIcon; hideAnswerPrefix?: boolean }): void { + const summaryStore = new DisposableStore(); + this._interactiveUIStore.value = summaryStore; + const summaryContainer = dom.$('.chat-question-carousel-summary.chat-question-carousel-conversation-summary'); + this.domNode.setAttribute('aria-label', localize('chat.questionCarousel.answeredQuestions', "Answered chat questions")); + + for (const question of this.carousel.questions) { + const answer = this._answers.get(question.id); + const summaryItem = dom.$('.chat-question-summary-item'); + const questionValue = dom.$('.chat-question-summary-question'); + const questionText = getDisplayedQuestionText(question); + const displayedQuestion = (typeof questionText === 'string' ? questionText : questionText.value).replace(/[:\s]+$/, ''); + const questionPrefix = dom.$('span.chat-question-summary-prefix'); + questionPrefix.textContent = localize('chat.questionCarousel.questionPrefix', "Question:"); + const questionTextValue = dom.$('span.chat-question-summary-question-value'); + questionTextValue.textContent = displayedQuestion; + summaryStore.add(this._hoverService.setupDelayedHover(questionTextValue, { content: displayedQuestion })); + questionValue.append(questionPrefix, questionValue.ownerDocument.createTextNode(' '), questionTextValue); + summaryItem.appendChild(questionValue); + + const decision = dom.$('.chat-question-summary-decision'); + const answerValue = answer === undefined + ? options?.answerFallback ?? localize('chat.questionCarousel.conversationNotAnswered', "Not answered yet") + : this.formatAnswerForSummary(question, answer); + const answerPrefix = options?.hideAnswerPrefix ? undefined : localize('chat.questionCarousel.answerPrefix', "Answered:"); + const answerTitle = answerPrefix + ? localize('chat.questionCarousel.conversationAnswer', "{0} {1}", answerPrefix, answerValue) + : answerValue; + const collapsibleContext = { + ...this._context, + content: this._context.content ?? [], + contentIndex: this._context.contentIndex ?? 0, + }; + const answerPart = summaryStore.add(new ChatQuestionAnswerCollapsiblePart( + answerTitle, + answerPrefix, + answerValue, + options?.answerIcon ?? (this.carousel.autoReply ? Codicon.copilotCompact : Codicon.comment), + collapsibleContext, + question.options?.length ? () => this.renderConversationOptions(question, answer) : undefined, + () => this._onDidChangeHeight.fire(), + this._hoverService, + this._configurationService, + )); + answerPart.domNode.classList.add('chat-question-answer-collapsible'); + decision.appendChild(answerPart.domNode); + summaryItem.appendChild(decision); + summaryContainer.appendChild(summaryItem); + } + + this.domNode.appendChild(summaryContainer); + } + + private renderConversationOptions(question: IChatQuestion, answer: IChatQuestionAnswerValue | undefined): HTMLElement { + const selectedValues = new Set(); + let freeformValue: string | undefined; + if (typeof answer === 'string') { + selectedValues.add(answer); + } else if (answer) { + if (hasKey(answer, { selectedValues: true })) { + for (const selectedValue of answer.selectedValues) { + selectedValues.add(selectedValue); + } + freeformValue = answer.freeformValue; + } else { + const singleAnswer = answer as IChatSingleSelectAnswer; + if (singleAnswer.selectedValue !== undefined) { + selectedValues.add(singleAnswer.selectedValue); + } + freeformValue = singleAnswer.freeformValue; + } + } + + const container = dom.$('.chat-question-summary-option-details.chat-used-context-list'); + const optionsTitle = dom.$('.chat-question-summary-options-title'); + optionsTitle.textContent = localize('chat.questionCarousel.optionsTitle', "Options"); + container.appendChild(optionsTitle); + + const optionList = dom.$('ul.chat-question-summary-option-list'); + for (const option of question.options ?? []) { + const selected = selectedValues.has(option.value); + const optionItem = dom.$('li.chat-question-summary-option'); + optionItem.classList.toggle('selected', selected); + optionItem.setAttribute('aria-label', selected + ? localize('chat.questionCarousel.selectedOptionAriaLabel', "{0}, selected", option.label) + : option.label); + const optionLabel = dom.$('span.chat-question-summary-option-label'); + optionLabel.textContent = option.label; + optionItem.appendChild(optionLabel); + if (selected) { + optionItem.appendChild(this.renderSelectedOptionState()); + } + optionList.appendChild(optionItem); + } + if (freeformValue) { + const customItem = dom.$('li.chat-question-summary-option.selected'); + customItem.setAttribute('aria-label', localize('chat.questionCarousel.selectedCustomAnswerAriaLabel', "Custom answer: {0}, selected", freeformValue)); + const customLabel = dom.$('span.chat-question-summary-option-label'); + customLabel.textContent = localize('chat.questionCarousel.customAnswer', "Custom answer: {0}", freeformValue); + customItem.append(customLabel, this.renderSelectedOptionState()); + optionList.appendChild(customItem); + } + container.appendChild(optionList); + return container; + } + + private renderSelectedOptionState(): HTMLElement { + const selectedState = dom.$('span.chat-question-summary-option-selected'); + const selectedIcon = dom.$('span'); + selectedIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.checkCompact)); + selectedIcon.setAttribute('aria-hidden', 'true'); + selectedState.appendChild(selectedIcon); + return selectedState; + } + /** * Formats an answer for display in the summary. */ private formatAnswerForSummary(question: IChatQuestion, answer: IChatQuestionAnswerValue): string { + if (this.carousel.autoReply && answer === AgentHostAutoReplyAnswer) { + return localize('chat.questionCarousel.autoReplyAnswer', "The user is not available to answer your question. Choose a pragmatic option best aligned with the context of the request."); + } + switch (question.type) { case 'text': return String(answer); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 0ac6a6aca49..a02a87f4460 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -560,3 +560,168 @@ font-size: var(--vscode-chat-font-size-body-s); } } + +.interactive-session .chat-question-carousel-container.chat-question-carousel-conversation.chat-question-carousel-used { + max-height: none; + overflow: visible; + border: none; + background: transparent; +} + +.interactive-session .chat-question-carousel-container.chat-question-carousel-conversation.chat-question-carousel-used:focus-visible { + outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); + outline-offset: calc(-1 * var(--vscode-strokeThickness)); +} + +.interactive-session .chat-question-carousel-container.chat-question-carousel-conversation.chat-question-carousel-used:focus-within:not(:focus-visible) { + border-color: transparent; + outline: none; +} + +.interactive-session .chat-question-carousel-conversation-summary { + gap: var(--vscode-spacing-size120); + padding: var(--vscode-spacing-size80) 0; + + .chat-question-summary-item { + gap: var(--vscode-spacing-size60); + padding: var(--vscode-spacing-size40) 0 0; + font-size: var(--vscode-agents-fontSize-body1); + } + + .chat-question-summary-question { + display: flex; + gap: var(--vscode-spacing-size40); + color: var(--vscode-descriptionForeground); + } + + .chat-question-summary-prefix { + flex-shrink: 0; + font-weight: var(--vscode-agents-fontWeight-semiBold); + } + + .chat-question-summary-question-value, + .chat-question-summary-answer-value { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .chat-question-summary-decision { + position: relative; + display: flex; + flex-direction: column; + padding-left: var(--vscode-spacing-size160); + } + + .chat-question-summary-decision::before { + position: absolute; + top: calc(-1 * var(--vscode-spacing-size40)); + left: var(--vscode-spacing-size40); + width: var(--vscode-spacing-size60); + height: var(--vscode-spacing-size160); + border-bottom: var(--vscode-strokeThickness) solid var(--vscode-chat-requestBorder); + border-left: var(--vscode-strokeThickness) solid var(--vscode-chat-requestBorder); + border-bottom-left-radius: var(--vscode-cornerRadius-medium); + content: ''; + pointer-events: none; + } + + .chat-question-summary-answer-icon { + flex: 0 0 var(--vscode-codiconFontSize-compact); + font-size: var(--vscode-codiconFontSize-compact); + line-height: 1; + } + + .chat-question-answer-collapsible { + min-width: 0; + margin-bottom: 0; + } + + .chat-question-answer-collapsible > .chat-used-context-label .monaco-button { + align-items: center; + max-width: 100%; + } + + .chat-question-answer-collapsible:not(.chat-question-answer-expandable) > .chat-used-context-label .monaco-button { + cursor: default; + pointer-events: none; + user-select: none; + } + + .chat-question-answer-collapsible > .chat-used-context-label .monaco-button-mdlabel { + display: flex; + flex: 1; + align-items: center; + gap: var(--vscode-spacing-size40); + white-space: normal; + width: 100%; + } + + .chat-question-answer-collapsible .chat-question-summary-answer-icon, + .chat-question-answer-collapsible .chat-collapsible-hover-chevron { + align-self: center; + margin-top: 2px; + } + + .chat-question-answer-expandable .chat-collapsible-hover-chevron { + flex-shrink: 0; + margin-left: var(--vscode-spacing-size20); + line-height: 1; + opacity: 1; + } + + .chat-question-summary-option-details { + margin-bottom: 0; + padding: var(--vscode-spacing-size100); + border: var(--vscode-strokeThickness) solid var(--vscode-widget-border); + border-radius: var(--vscode-cornerRadius-medium); + background: var(--vscode-editorWidget-background); + color: var(--vscode-descriptionForeground); + font-size: var(--vscode-chat-font-size-body-s) + } + + .chat-question-summary-options-title { + margin-bottom: var(--vscode-spacing-size40); + padding: 0 var(--vscode-spacing-size80) var(--vscode-spacing-size40); + border-bottom: var(--vscode-strokeThickness) solid color-mix(in srgb, var(--vscode-widget-border) 70%, transparent); + color: var(--vscode-foreground); + font-size: var(--vscode-agents-fontSize-label1); + } + + .chat-question-summary-option-list { + display: flex; + flex-direction: column; + gap: 0; + margin: 0; + padding: 0; + list-style: none; + } + + .chat-question-summary-option { + display: flex; + align-items: center; + gap: var(--vscode-spacing-size80); + padding: var(--vscode-spacing-size40) var(--vscode-spacing-size80); + border-radius: var(--vscode-cornerRadius-small); + } + + .chat-question-summary-option.selected { + background: var(--vscode-list-inactiveSelectionBackground); + color: var(--vscode-foreground); + } + + .chat-question-summary-option-label { + flex: 1; + } + + .chat-question-summary-option-selected { + display: flex; + flex-shrink: 0; + align-items: center; + color: inherit; + } + + .chat-question-summary-option-selected > .codicon { + font-size: var(--vscode-codiconFontSize-compact); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts index 8840232b780..8c891c24255 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts @@ -12,7 +12,11 @@ export function isMcpToolInvocation(toolInvocation: IChatToolInvocation | IChatT } export function isAskQuestionsToolInvocation(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): boolean { - return toolInvocation.toolId === 'copilot_askQuestions' || toolInvocation.toolId === 'vscode_askQuestions'; + return toolInvocation.toolId === 'copilot_askQuestions' + || toolInvocation.toolId === 'vscode_askQuestions' + || toolInvocation.toolId === 'ask_user' + || toolInvocation.toolId === 'AskUserQuestion' + || toolInvocation.toolId === 'request_user_input'; } /** diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 57e16fdfe70..5def5488eb3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -122,7 +122,7 @@ import { ChatPendingDragController } from './chatPendingDragAndDrop.js'; import { HookType } from '../../common/promptSyntax/hookTypes.js'; import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; import { AccessibilityWorkbenchSettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { isMcpToolInvocation } from './chatContentParts/toolInvocationParts/chatToolPartUtilities.js'; +import { isAskQuestionsToolInvocation, isMcpToolInvocation } from './chatContentParts/toolInvocationParts/chatToolPartUtilities.js'; import { AgentSessionProviders, isAgentHostTarget } from '../agentSessions/agentSessions.js'; const $ = dom.$; @@ -1556,6 +1556,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind === 'toolInvocation' && IChatToolInvocation.isStreaming(part))) { @@ -1569,7 +1570,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind === 'toolInvocation' && !IChatToolInvocation.isComplete(part))) || (lastPart.kind === 'externalEdit' && !workingParts.some(part => part.kind === 'toolInvocation' && !IChatToolInvocation.isComplete(part))) || (lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) || + endsWithCompletedQuestion || lastPart.kind === 'mcpServersStarting' || lastPart.kind === 'mcpAuthenticationRequired' || lastPart.kind === 'mcpServersStartingSlow' || @@ -2591,7 +2593,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind === 'mcpServersStartingSlow' && part.servers.get().length > 0); } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 79f9e23b5f5..8915272323f 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -489,14 +489,18 @@ export interface IChatQuestionCarousel { data?: IChatQuestionAnswers; /** Whether the carousel has been submitted/skipped */ isUsed?: boolean; - /** True when accepted/answered outside the carousel UI (e.g. via voice) without structured answers. */ + /** True when accepted/answered outside the carousel UI, such as by voice or automatic reply. */ answeredExternally?: boolean; + /** True when Copilot supplied the answer through automatic reply. */ + autoReply?: boolean; /** Top-level message shown above the questions (e.g. from MCP elicitation message) */ message?: string | IMarkdownString; /** Source attribution (e.g. MCP server) */ source?: ToolDataSource; /** Terminal ID when the carousel was triggered by a terminal needing input */ terminalId?: string; + /** Visual treatment for the answered state. */ + answerPresentation?: 'conversation'; kind: 'questionCarousel'; } diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatQuestionCarouselData.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatQuestionCarouselData.ts index 7b94d7c1b0e..2745c87c71a 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatQuestionCarouselData.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatQuestionCarouselData.ts @@ -26,10 +26,11 @@ export class ChatQuestionCarouselData implements IChatQuestionCarousel { public dismissedByTerminalInput?: boolean; /** - * True when the input was accepted/answered outside the carousel UI (e.g. - * via voice) without structured answers, so the summary reads "Answered". + * True when the input was accepted/answered outside the carousel UI, such + * as by voice or automatic reply. */ public answeredExternally?: boolean; + public autoReply?: boolean; /** * Marks the carousel as dismissed with the given answers and clears draft @@ -56,6 +57,7 @@ export class ChatQuestionCarouselData implements IChatQuestionCarousel { public message?: string | IMarkdownString, public source?: ToolDataSource, public terminalId?: string, + public answerPresentation?: 'conversation', ) { } toJSON(): IChatQuestionCarousel { @@ -67,9 +69,11 @@ export class ChatQuestionCarouselData implements IChatQuestionCarousel { data: this.data, isUsed: this.isUsed, answeredExternally: this.answeredExternally, + autoReply: this.autoReply, message: this.message, source: this.source, terminalId: this.terminalId, + answerPresentation: this.answerPresentation, }; } } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts index 968f2ebe76b..5b764be2417 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts @@ -9,12 +9,13 @@ import { hasKey } from '../../../../../../base/common/types.js'; import { URI } from '../../../../../../base/common/uri.js'; import type { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { AgentHostAutoReplyAnswer } from '../../../../../../platform/agentHost/common/agentHostSchema.js'; import { McpAuthRequiredReason } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { fromAgentHostUri, toAgentHostUri } from '../../../../../../platform/agentHost/common/agentHostUri.js'; -import { buildSubagentChatUri, MessageKind, ToolCallContributorKind, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallStatus, ToolCallConfirmationReason, ToolResultContentType, TurnState, ResponsePartKind, readUsageInfoMeta, type ActiveTurn, type ICompletedToolCall, type ToolCallPendingConfirmationState, type ToolCallRunningState, type Turn, type ToolCallResponsePart, ToolCallCancellationReason, type Message, type ToolResultContent } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { buildSubagentChatUri, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, MessageKind, ToolCallContributorKind, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallStatus, ToolCallConfirmationReason, ToolResultContentType, TurnState, ResponsePartKind, readUsageInfoMeta, type ActiveTurn, type ICompletedToolCall, type ToolCallPendingConfirmationState, type ToolCallRunningState, type Turn, type ToolCallResponsePart, ToolCallCancellationReason, type Message, type ToolResultContent } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind, type IChatMarkdownContent, type IChatTerminalToolInvocationData, type IChatThinkingPart, type IChatUsage } from '../../../common/chatService/chatService.js'; import { isToolResultInputOutputDetails, type IToolResultInputOutputDetails, ToolDataSource, ToolInvocationPresentation } from '../../../common/tools/languageModelToolsService.js'; -import { turnsToHistory as rawTurnsToHistory, activeTurnToProgress as rawActiveTurnToProgress, completedToolCallToSerialized, toolCallStateToInvocation as rawToolCallStateToInvocation, toolCallStateToPreparedInvocation as rawToolCallStateToPreparedInvocation, toolCallStateToStreamingInvocation, finalizeToolInvocation as rawFinalizeToolInvocation, updateRunningToolSpecificData as rawUpdateRunningToolSpecificData, usageInfoToAutoModeResolution, usageInfoToQuotas, formatTurnResponseDetails, rewriteAgentHostLinkTarget, rewriteMarkdownLinks, type TurnModelLookup } from '../../../browser/agentSessions/agentHost/stateToProgressAdapter.js'; +import { turnsToHistory as rawTurnsToHistory, activeTurnToProgress as rawActiveTurnToProgress, completedToolCallToSerialized, containsAutomaticReplyAnswer, createInputRequestCarousel, toolCallStateToInvocation as rawToolCallStateToInvocation, toolCallStateToPreparedInvocation as rawToolCallStateToPreparedInvocation, toolCallStateToStreamingInvocation, finalizeToolInvocation as rawFinalizeToolInvocation, updateRunningToolSpecificData as rawUpdateRunningToolSpecificData, usageInfoToAutoModeResolution, usageInfoToQuotas, formatTurnResponseDetails, rewriteAgentHostLinkTarget, rewriteMarkdownLinks, type TurnModelLookup } from '../../../browser/agentSessions/agentHost/stateToProgressAdapter.js'; // ---- Helper factories ------------------------------------------------------- @@ -124,6 +125,23 @@ suite('stateToProgressAdapter', () => { ensureNoDisposablesAreLeakedInTestSuite(); + test('detects the canonical automatic reply answer', () => { + assert.deepStrictEqual([ + containsAutomaticReplyAnswer({ + question: { + state: ChatInputAnswerState.Submitted, + value: { kind: ChatInputAnswerValueKind.Text, value: AgentHostAutoReplyAnswer }, + }, + }), + containsAutomaticReplyAnswer({ + question: { + state: ChatInputAnswerState.Submitted, + value: { kind: ChatInputAnswerValueKind.Text, value: 'User answer' }, + }, + }), + ], [true, false]); + }); + suite('rewriteAgentHostLinkTarget', () => { test('supports absolute paths and file URIs with validated locations', () => { const unwrap = (href: string) => fromAgentHostUri(URI.parse(rewriteAgentHostLinkTarget(href, 'my-host'))).toString(); @@ -281,6 +299,56 @@ suite('stateToProgressAdapter', () => { assert.strictEqual(details.isError, false); }); + test('restores an answered ask-user interaction as a hidden tool plus conversational summary', () => { + const turn = createTurn({ + responseParts: [ + { + kind: ResponsePartKind.ToolCall, + toolCall: createCompletedToolCall({ toolName: 'ask_user' }), + }, + { + kind: ResponsePartKind.InputRequest, + request: { + id: 'input-1', + questions: [{ + id: 'q1', + kind: ChatInputQuestionKind.SingleSelect, + message: 'What should we work on?', + required: true, + options: [ + { id: 'fix', label: 'Fix a bug' }, + { id: 'feature', label: 'Implement a feature' }, + ], + }], + answers: { + q1: { + state: ChatInputAnswerState.Submitted, + value: { kind: ChatInputAnswerValueKind.Selected, value: 'fix' }, + }, + }, + }, + response: ChatInputResponseKind.Accept, + }, + ], + }); + const history = turnsToHistory(URI.file('/'), [turn], 'p'); + const parts = history[1].type === 'response' ? history[1].parts : []; + const tool = parts[0] as IChatToolInvocationSerialized; + const carousel = parts[1]; + + assert.deepStrictEqual({ + toolPresentation: tool.presentation, + carouselKind: carousel.kind, + answerPresentation: carousel.kind === 'questionCarousel' ? carousel.answerPresentation : undefined, + answer: carousel.kind === 'questionCarousel' ? carousel.data?.q1 : undefined, + }, { + toolPresentation: ToolInvocationPresentation.HiddenAfterComplete, + carouselKind: 'questionCarousel', + answerPresentation: 'conversation', + answer: { selectedValue: 'fix', freeformValue: undefined }, + }); + }); + test('generic failed tool call in history uses error text as output', () => { const turn = createTurn({ responseParts: [{ @@ -877,6 +945,43 @@ suite('stateToProgressAdapter', () => { assert.strictEqual(invocation.source, ToolDataSource.Internal); }); + test('renders ask-user tools as waiting progress that hides after completion', () => { + const toolNames = ['ask_user', 'AskUserQuestion', 'request_user_input']; + const live = toolNames.map(toolName => { + const invocation = toolCallStateToInvocation(createToolCallState({ toolName })); + return { + message: invocation.invocationMessage, + presentation: invocation.presentation, + }; + }); + const restored = completedToolCallToSerialized(createCompletedToolCall({ toolName: 'ask_user' }), undefined, URI.file('/'), 'local'); + const failed = completedToolCallToSerialized(createCompletedToolCall({ toolName: 'ask_user', success: false }), undefined, URI.file('/'), 'local'); + + assert.deepStrictEqual({ live, restoredPresentation: restored.presentation, failedPresentation: failed.presentation }, { + live: toolNames.map(() => ({ + message: 'Waiting for answer...', + presentation: ToolInvocationPresentation.HiddenAfterComplete, + })), + restoredPresentation: ToolInvocationPresentation.HiddenAfterComplete, + failedPresentation: undefined, + }); + }); + + test('marks Agent Host input requests for conversational answer rendering', () => { + const carousel = createInputRequestCarousel({ + id: 'input-1', + questions: [{ + id: 'q1', + kind: ChatInputQuestionKind.SingleSelect, + message: 'Choose one', + required: true, + options: [{ id: 'a', label: 'Option A' }], + }], + }, 'local'); + + assert.strictEqual(carousel.answerPresentation, 'conversation'); + }); + test('attaches automation result data to live and restored configureAutomation calls', () => { const content: ToolResultContent[] = [{ type: ToolResultContentType.Text, diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index 84f9219555d..4f24ca2fa9a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -12,6 +12,7 @@ import { ChatQuestionCarouselPart, IChatQuestionCarouselOptions } from '../../.. import { IChatQuestionAnswerValue, IChatQuestionCarousel } from '../../../../common/chatService/chatService.js'; import { IChatContentPartRenderContext } from '../../../../browser/widget/chatContentParts/chatContentParts.js'; import { ChatQuestionCarouselData } from '../../../../common/model/chatProgressTypes/chatQuestionCarouselData.js'; +import { AgentHostAutoReplyAnswer } from '../../../../../../../platform/agentHost/common/agentHostSchema.js'; function createMockCarousel(questions: IChatQuestionCarousel['questions'], allowSkip: boolean = true): IChatQuestionCarousel { return { @@ -22,7 +23,8 @@ function createMockCarousel(questions: IChatQuestionCarousel['questions'], allow } function createMockContext(): IChatContentPartRenderContext { - return {} as IChatContentPartRenderContext; + const context: Partial = { content: [], contentIndex: 0 }; + return context as IChatContentPartRenderContext; } suite('ChatQuestionCarouselPart', () => { @@ -31,11 +33,12 @@ suite('ChatQuestionCarouselPart', () => { let widget: ChatQuestionCarouselPart; let submittedAnswers: Map | undefined | null = null; - function createWidget(carousel: IChatQuestionCarousel): ChatQuestionCarouselPart { + function createWidget(carousel: IChatQuestionCarousel, onSubmit?: () => void): ChatQuestionCarouselPart { const instantiationService = workbenchInstantiationService(undefined, store); const options: IChatQuestionCarouselOptions = { onSubmit: (answers) => { submittedAnswers = answers; + onSubmit?.(); } }; widget = store.add(instantiationService.createInstance(ChatQuestionCarouselPart, carousel, createMockContext(), options)); @@ -450,6 +453,18 @@ suite('ChatQuestionCarouselPart', () => { assert.strictEqual(answer.selectedValues.length, 2); assert.strictEqual(answer.freeformValue, undefined); }); + + test('does not render a summary after onSubmit disposes the part', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question', defaultValue: 'answer' } + ]); + createWidget(carousel, () => widget.dispose()); + + const submitButton = widget.domNode.querySelector('.chat-question-submit-button') as HTMLButtonElement; + submitButton.click(); + + assert.strictEqual(widget.domNode.querySelector('.chat-question-carousel-summary'), null); + }); }); suite('Navigation', () => { @@ -910,6 +925,78 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(summaryValue?.textContent?.includes('saved answer'), 'Summary should show saved answer from data'); }); + test('renders conversational summary with expandable selected options', () => { + const carousel = new ChatQuestionCarouselData([{ + id: 'q1', + type: 'singleSelect', + title: 'What should we prioritize if the refactor affects multiple platforms and may require migration work?', + options: [ + { id: 'fix', label: 'Fix a bug', value: 'fix' }, + { id: 'feature', label: 'Implement a feature', value: 'feature' }, + ], + }], true, undefined, { q1: { selectedValue: 'fix' } }, true); + carousel.answerPresentation = 'conversation'; + createWidget(carousel.toJSON()); + + const question = widget.domNode.querySelector('.chat-question-summary-question'); + const answerButton = widget.domNode.querySelector('.chat-question-answer-collapsible .monaco-button') as HTMLElement | null; + assert.ok(question && answerButton); + assert.strictEqual(widget.domNode.querySelector('.chat-question-summary-option-list'), null); + answerButton.click(); + + assert.deepStrictEqual({ + question: question.textContent, + questionExpandable: question.hasAttribute('aria-expanded'), + answer: answerButton.textContent, + answerExpanded: answerButton.getAttribute('aria-expanded'), + answerIcon: answerButton.querySelector('.chat-question-summary-answer-icon')?.classList.contains('codicon-comment'), + hasChevron: !!answerButton.querySelector('.chat-collapsible-hover-chevron'), + optionsTitle: widget.domNode.querySelector('.chat-question-summary-options-title')?.textContent, + options: Array.from(widget.domNode.querySelectorAll('.chat-question-summary-option')).map(option => ({ + label: option.querySelector('.chat-question-summary-option-label')?.textContent, + selected: option.classList.contains('selected'), + hasCompactCheck: !!option.querySelector('.chat-question-summary-option-selected .codicon-check-compact'), + })), + }, { + question: 'Question: What should we prioritize if the refactor affects multiple platforms and may require migration work?', + answer: 'Answered: Fix a bug', + questionExpandable: false, + answerExpanded: 'true', + answerIcon: true, + hasChevron: true, + optionsTitle: 'Options', + options: [ + { label: 'Fix a bug', selected: true, hasCompactCheck: true }, + { label: 'Implement a feature', selected: false, hasCompactCheck: false }, + ], + }); + }); + + test('uses a non-interactive collapsible header for free responses', () => { + const carousel = new ChatQuestionCarouselData([{ + id: 'q1', + type: 'text', + title: 'What would you like me to help you with?', + }], true, undefined, { q1: 'Review the changes' }, true); + carousel.answerPresentation = 'conversation'; + createWidget(carousel.toJSON()); + + const answerButton = widget.domNode.querySelector('.chat-question-answer-collapsible .monaco-button') as HTMLElement | null; + assert.deepStrictEqual({ + answer: answerButton?.textContent, + disabled: answerButton?.getAttribute('aria-disabled'), + tabIndex: answerButton?.tabIndex, + expanded: answerButton?.getAttribute('aria-expanded'), + hasChevron: !!answerButton?.querySelector('.chat-collapsible-hover-chevron'), + }, { + answer: 'Answered: Review the changes', + disabled: 'true', + tabIndex: -1, + expanded: null, + hasChevron: false, + }); + }); + test('shows skipped message when constructed with isUsed but no data', () => { const carousel: IChatQuestionCarousel = { kind: 'questionCarousel', @@ -928,6 +1015,42 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(skippedMessage, 'Should show skipped message when no data'); }); + test('renders a skipped conversational question with its options', () => { + const carousel: IChatQuestionCarousel = { + kind: 'questionCarousel', + questions: [{ + id: 'q1', + type: 'singleSelect', + title: 'Which environment?', + options: [ + { id: 'staging', label: 'Staging', value: 'staging' }, + { id: 'production', label: 'Production', value: 'production' }, + ], + }], + allowSkip: true, + isUsed: true, + answerPresentation: 'conversation', + }; + createWidget(carousel); + + const answerButton = widget.domNode.querySelector('.chat-question-answer-collapsible .monaco-button') as HTMLElement | null; + assert.ok(answerButton); + answerButton.click(); + assert.deepStrictEqual({ + question: widget.domNode.querySelector('.chat-question-summary-question')?.textContent, + answer: answerButton.textContent, + answerIcon: answerButton.querySelector('.chat-question-summary-answer-icon')?.classList.contains('codicon-close-compact'), + hasChevron: !!answerButton.querySelector('.chat-collapsible-hover-chevron'), + options: Array.from(widget.domNode.querySelectorAll('.chat-question-summary-option-label')).map(option => option.textContent), + }, { + question: 'Question: Which environment?', + answer: 'Skipped question', + answerIcon: true, + hasChevron: true, + options: ['Staging', 'Production'], + }); + }); + test('shows answered message when answeredExternally but no data', () => { const carousel: IChatQuestionCarousel = { kind: 'questionCarousel', @@ -936,7 +1059,8 @@ suite('ChatQuestionCarouselPart', () => { ], allowSkip: true, isUsed: true, - answeredExternally: true + answeredExternally: true, + answerPresentation: 'conversation', }; createWidget(carousel); @@ -945,6 +1069,35 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(summary, 'Should show summary container'); assert.ok(!summary?.querySelector('.chat-question-summary-skipped'), 'Should not show skipped message'); assert.ok(summary?.querySelector('.chat-question-summary-answered'), 'Should show answered message when answered externally'); + assert.ok(!summary?.querySelector('.codicon-copilot-compact'), 'Should not present a generic external answer as an automatic reply'); + }); + + test('renders a Copilot icon for a structured automatic answer', () => { + const carousel: IChatQuestionCarousel = { + kind: 'questionCarousel', + questions: [ + { id: 'q1', type: 'text', title: 'What should we work on next?' } + ], + allowSkip: true, + isUsed: true, + answeredExternally: true, + autoReply: true, + answerPresentation: 'conversation', + data: { q1: AgentHostAutoReplyAnswer }, + }; + createWidget(carousel); + + assert.deepStrictEqual({ + question: widget.domNode.querySelector('.chat-question-summary-question')?.textContent, + answer: widget.domNode.querySelector('.chat-question-answer-collapsible .monaco-button')?.textContent, + answerIcon: widget.domNode.querySelector('.chat-question-summary-answer-icon')?.classList.contains('codicon-copilot-compact'), + hasGenericMessage: !!widget.domNode.querySelector('.chat-question-summary-answered'), + }, { + question: 'Question: What should we work on next?', + answer: `Answered: ${AgentHostAutoReplyAnswer}`, + answerIcon: true, + hasGenericMessage: false, + }); }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatToolProgressPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatToolProgressPart.test.ts index 9a15aa2eff0..ab2f0fda274 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatToolProgressPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatToolProgressPart.test.ts @@ -25,7 +25,7 @@ import { ChatToolInvocationPart } from '../../../../browser/widget/chatContentPa import { ChatToolConfirmationCarouselPart } from '../../../../browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationCarouselPart.js'; import { BaseChatToolInvocationSubPart } from '../../../../browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.js'; import { ChatToolProgressSubPart } from '../../../../browser/widget/chatContentParts/toolInvocationParts/chatToolProgressPart.js'; -import { isMcpToolInvocation } from '../../../../browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.js'; +import { isAskQuestionsToolInvocation, isMcpToolInvocation } from '../../../../browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.js'; import { DiffEditorPool, EditorPool } from '../../../../browser/widget/chatContentParts/chatContentCodePools.js'; import { IChatAutomationConfiguredData, IChatTerminalToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; import { IChatResponseViewModel } from '../../../../common/model/chatViewModel.js'; @@ -268,6 +268,11 @@ suite('ChatToolProgressSubPart', () => { assert.deepStrictEqual(cases, [true, true, false]); }); + test('detects all ask-question tool names for top-level rendering', () => { + const toolNames = ['copilot_askQuestions', 'vscode_askQuestions', 'ask_user', 'AskUserQuestion', 'request_user_input']; + assert.deepStrictEqual(toolNames.map(toolId => isAskQuestionsToolInvocation(createToolInvocation({ toolId }))), [true, true, true, true, true]); + }); + test('renders the automation result subpart for configured automation data', () => { const invocation: IChatToolInvocationSerialized = { ...createSerializedToolInvocation({ isComplete: true }), @@ -446,6 +451,16 @@ suite('ChatToolProgressSubPart', () => { mockMarkdownRenderer, new Set() )); + const waitingForAnswerTool = disposables.add(instantiationService.createInstance( + ChatToolProgressSubPart, + createToolInvocation({ + toolId: 'ask_user', + invocationMessage: 'Waiting for answer...' + }), + createRenderContext(false), + mockMarkdownRenderer, + new Set() + )); assert.deepStrictEqual([ !!askQuestionsTool.domNode.querySelector('.shimmer-progress'), @@ -454,8 +469,9 @@ suite('ChatToolProgressSubPart', () => { askMultipleQuestionsTool.domNode.querySelector('.chat-progress-shimmer-text')?.textContent, askMultipleQuestionsTool.domNode.textContent, !!analyzingAnswersTool.domNode.querySelector('.shimmer-progress'), - analyzingAnswersTool.domNode.querySelector('.chat-progress-shimmer-text')?.textContent - ], [true, 'Asking a question', 'Asking a question (Target)', 'Asking 3 questions', 'Asking 3 questions (What should we work on?, Preferred area, How hands-on?)', false, undefined]); + analyzingAnswersTool.domNode.querySelector('.chat-progress-shimmer-text')?.textContent, + !!waitingForAnswerTool.domNode.querySelector('.shimmer-progress') + ], [true, 'Asking a question', 'Asking a question (Target)', 'Asking 3 questions', 'Asking 3 questions (What should we work on?, Preferred area, How hands-on?)', false, undefined, true]); }); test('does not render a loading icon for run playwright code progress', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatListRenderer.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatListRenderer.test.ts index 9a3bad3d7cd..2300ec53211 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatListRenderer.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatListRenderer.test.ts @@ -16,10 +16,10 @@ import { TestConfigurationService } from '../../../../../../platform/configurati import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; -import { buildPlanReviewProgressContent, ChatListItemRenderer, endsWithSubagentContent, formatCompletedResponseDisclosureLabel, getFinalResponseStartIndex, getVisibleCompletedResponseItemCount, getWorkingProgressRelevantParts, IChatListItemTemplate, isWaitingForMcpServers, reconcileChatItemHeight, renderChatRequestTimestamp, renderChatResponseDetails, shouldCreateGroupedThinkingPart, shouldHideChatUserIdentity, shouldPinToolInvocationToThinking, shouldRenderInitialProgressiveContentImmediately, shouldScheduleInitialHeightChange, shouldShowFileChangesSummaryForSettings, shouldShowPillsSummaryForSettings, shouldStartNewCollapsedThinkingGroup } from '../../../browser/widget/chatListRenderer.js'; +import { buildPlanReviewProgressContent, ChatListItemRenderer, endsWithCompletedQuestionInteraction, endsWithSubagentContent, formatCompletedResponseDisclosureLabel, getFinalResponseStartIndex, getVisibleCompletedResponseItemCount, getWorkingProgressRelevantParts, IChatListItemTemplate, isWaitingForMcpServers, reconcileChatItemHeight, renderChatRequestTimestamp, renderChatResponseDetails, shouldCreateGroupedThinkingPart, shouldHideChatUserIdentity, shouldPinToolInvocationToThinking, shouldRenderInitialProgressiveContentImmediately, shouldScheduleInitialHeightChange, shouldShowFileChangesSummaryForSettings, shouldShowPillsSummaryForSettings, shouldStartNewCollapsedThinkingGroup } from '../../../browser/widget/chatListRenderer.js'; import { ChatWidget } from '../../../browser/widget/chatWidget.js'; import { isChatTurnStatusPillsEnabled } from '../../../browser/widget/chatTurnPills.js'; -import { IChatMcpServersStartingSlow, IChatService, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../common/chatService/chatService.js'; +import { IChatMcpServersStartingSlow, IChatQuestionCarousel, IChatService, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../common/chatService/chatService.js'; import { formatChatRequestTimestamp, formatChatResponseDetails, formatElapsedTime } from '../../../common/chatProgressFormatting.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, CollapsedToolsDisplayMode, ThinkingDisplayMode } from '../../../common/constants.js'; import { ChatModel } from '../../../common/model/chatModel.js'; @@ -500,6 +500,36 @@ suite('ChatListRenderer', () => { executingWithMcpApp: false, streamingWithMcpApp: false, }); + + suite('endsWithCompletedQuestionInteraction', () => { + test('resumes working progress after completed ask interactions', () => { + const completedTool: IChatToolInvocationSerialized = { + kind: 'toolInvocationSerialized', + toolCallId: 'ask-1', + toolId: 'ask_user', + invocationMessage: 'Waiting for answer...', + originMessage: undefined, + pastTenseMessage: undefined, + isComplete: true, + isConfirmed: { type: ToolConfirmKind.ConfirmationNotNeeded }, + presentation: undefined, + source: ToolDataSource.Internal, + }; + const completedQuestion: IChatQuestionCarousel = { + kind: 'questionCarousel', + questions: [], + allowSkip: true, + isUsed: true, + }; + + assert.deepStrictEqual([ + endsWithCompletedQuestionInteraction([completedTool]), + endsWithCompletedQuestionInteraction([completedTool, completedQuestion]), + endsWithCompletedQuestionInteraction([{ ...completedQuestion, isUsed: false }]), + endsWithCompletedQuestionInteraction([{ ...completedTool, toolId: 'read_file' }]), + ], [true, true, false, false]); + }); + }); }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatQuestionCarouselData.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatQuestionCarouselData.test.ts index 5908b92fa06..5d2ee7d9e99 100644 --- a/src/vs/workbench/contrib/chat/test/common/model/chatQuestionCarouselData.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/model/chatQuestionCarouselData.test.ts @@ -66,13 +66,20 @@ suite('ChatQuestionCarouselData', () => { assert.strictEqual((json as { draftCurrentIndex?: unknown }).draftCurrentIndex, undefined, 'toJSON should not include draftCurrentIndex'); }); - test('toJSON preserves answeredExternally', () => { + test('toJSON preserves external answer metadata', () => { const carousel = new ChatQuestionCarouselData(createQuestions(), true, 'test-resolve-id', {}, true); carousel.answeredExternally = true; + carousel.autoReply = true; const json = carousel.toJSON(); - assert.strictEqual(json.answeredExternally, true, 'toJSON should preserve answeredExternally'); + assert.deepStrictEqual({ + answeredExternally: json.answeredExternally, + autoReply: json.autoReply, + }, { + answeredExternally: true, + autoReply: true, + }); }); test('multiple carousels can have independent completion promises', async () => { From 2d2f4ae7b6981bfc2b93be73b8ca990d90c3ed87 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 30 Jul 2026 10:25:57 -0700 Subject: [PATCH 15/86] Cover annotations, protocol contracts, and terminal lifecycle in agent host E2E (#328163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Cover annotations, protocol contracts, and terminal lifecycle in agent host E2E Adds conformance coverage for protocol areas that no E2E test reached, chosen from the protocol-surface `uncovered` lists rather than to hit a number. - `suites/annotationsSuite.ts` — the whole `annotations/*` channel, which was covered by neither the E2E suite nor the frozen protocol suite. - `suites/protocolContractsSuite.ts` — liveness (`ping`), turn-history paging (`fetchTurns` / `chat/turnsLoaded`), connection recovery (`reconnect`, both the replay-gap and unresumable-subscription cases), and the rejection contract for the four declared-but-unsupported working-directory actions. - `stateOperationsSuite.ts` — terminal clear and exit, root's view of terminals appearing and disappearing, and queued-message promotion. `reconnect` is only answerable on a transport that has not completed the handshake, so it needs a second connection that can be dropped and re-established. `IAgentHostE2ETestContext.connectClient` is that seam; the shared per-test client cannot express it. The queued-message test started out asserting that two queued messages accumulate. They do not: a message queued onto an idle chat is promoted straight into a turn, so the queue is empty again by the next reduction. The dispatch envelope looked entirely normal, and every other test in that suite asserts only the result of its last dispatch, which is why this had gone unnoticed. The test now asserts the real contract, and KNOWN_ISSUES records the test-shape lesson. Protocol surface: commands 27/29 -> 28/29, actions 54/85 -> 59/85. The one remaining uncovered command and the eight `changeset/*` actions are covered by the changeset branch. Conformance tier 62 -> 73 tests; full suite 172 passing, 0 failing, stable across repeated runs. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drive the two new host-local turns with /rename instead of a bang command Windows CI timed out on `fetchTurns reports the turns a chat already has` and `a message queued on an idle chat is promoted straight into a turn`. Both needed a real turn that never reaches the model, and both used `!echo` to get one. That is a documented Windows defect, already recorded in KNOWN_ISSUES and already the reason `a bang command runs locally and exposes terminal output` is Windows-scoped: a successful bang command produces output but does not complete reliably, so waiting on `chat/turnComplete` hangs. `/rename` is handled by the same local-command dispatcher but spawns no shell, and it is exercised on Windows today by the host-features suite. Neither test cares how the turn was produced, only that one exists, so this removes the dependency rather than scoping the tests to non-Windows. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Tighten notification predicates and correct the migration backlog table Addresses code review feedback: - The turn waits in the queued-message test and in `fetchTurns` now constrain `channel`, so a shared client cannot match a notification from another session's chat. - `dispatchAndWaitOnShared` now matches the action type as well as the channel and originating `clientSeq`. - The migration backlog table's third column listed symbols as uncovered that this change covers. Renamed it to "Protocol symbols", marked each row's actual status, and pointed at `coverage/protocol-surface.json` as the authoritative source. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/test/node/e2e/KNOWN_ISSUES.md | 17 + .../agentHost/test/node/e2e/README.md | 20 +- .../node/e2e/coverage/protocol-surface.json | 30 +- .../test/node/e2e/coverage/summary.json | 974 ++++++++++-------- .../e2e/harness/agentHostE2ETestHarness.ts | 17 + .../node/e2e/suites/agentHostE2ESuites.ts | 10 + .../test/node/e2e/suites/annotationsSuite.ts | 180 ++++ .../test/node/e2e/suites/e2eTestContext.ts | 6 + .../node/e2e/suites/protocolContractsSuite.ts | 243 +++++ .../node/e2e/suites/stateOperationsSuite.ts | 122 +++ 10 files changed, 1133 insertions(+), 486 deletions(-) create mode 100644 src/vs/platform/agentHost/test/node/e2e/suites/annotationsSuite.ts create mode 100644 src/vs/platform/agentHost/test/node/e2e/suites/protocolContractsSuite.ts diff --git a/src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md b/src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md index ed2eeb5f87c..02b3d295477 100644 --- a/src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md +++ b/src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md @@ -326,6 +326,23 @@ These are opt-in live tests, not known failures. - Gate: `supportsPlanMode: false`. - Evaluation goal: make the test prompt provider-neutral or add an equivalent Claude-specific prompt without weakening the plan-mode assertions. +### A test that only asserts its last dispatch cannot see a lost one + +Most state-operation tests dispatch two or three actions and then assert the +result of the **last** one. That shape is blind to an action that is echoed but +never applied, because the final read still shows the expected value. + +The first test written here that asserted a *cumulative* result across two +dispatches immediately exposed behavior nobody had written down: a message +queued onto an idle chat is not parked in `queuedMessages` at all, it is +promoted straight into a turn (`_tryConsumeNextQueuedMessage`), so the queue is +empty again by the time the next action is reduced. The envelope for the +dispatch looked completely normal — correct `serverSeq`, no `rejectionReason` — +so nothing short of asserting the accumulated state would have caught it. + +When adding state-operation tests, prefer at least one assertion over the state +that several actions built up together, not only over the last write. + ## Expected capability skips These pending tests do not currently indicate bugs. They are listed by capability rather than by test title: the titles change often, and the gate is what matters. diff --git a/src/vs/platform/agentHost/test/node/e2e/README.md b/src/vs/platform/agentHost/test/node/e2e/README.md index 7ffab58eceb..5886b0739ba 100644 --- a/src/vs/platform/agentHost/test/node/e2e/README.md +++ b/src/vs/platform/agentHost/test/node/e2e/README.md @@ -467,18 +467,22 @@ Existing tests there stay and keep running — they are cheap and they work. The A one-off union measurement (protocol + E2E vs. E2E alone) put the protocol suite's unique contribution at **1673 statements (+1.8pp)** across 30 files. Cross-referencing that with the protocol-surface `uncovered` list gives a concrete list of contracts that exist *only* in the frozen suite and should be re-expressed here as conformance tests, highest value first: -| Area | Only tested in | Uncovered protocol symbols | +The **Protocol symbols** column lists what each row is responsible for; check `coverage/protocol-surface.json` for the authoritative covered/uncovered split rather than reading it out of this table. + +| Area | Status | Protocol symbols | |---|---|---| -| Client-hosted filesystem (reverse requests) | *migrated* — `suites/clientFilesystemSuite.ts` | `resourceWatch/changed` | -| Turn history paging | `turnExecution` | `fetchTurns`, `chat/turnsLoaded` | -| Reconnect and multi-client fan-out | `multiClient` | `reconnect` | -| Changeset lifecycle | `sessionDiffs` | all 8 `changeset/*` actions | -| OTLP export | `otlpLogs` | `otlp/exportLogs`, `otlp/exportMetrics`, `otlp/exportTraces` | -| Liveness | `handshake`, several others | `ping` | +| Client-hosted filesystem (reverse requests) | migrated — `suites/clientFilesystemSuite.ts` | `resourceWatch/changed` still uncovered | +| Turn history paging | migrated — `suites/protocolContractsSuite.ts` | `fetchTurns`, `chat/turnsLoaded` — covered | +| Reconnect and multi-client fan-out | partly migrated — `suites/protocolContractsSuite.ts` covers `reconnect`; fan-out across several live clients is still only in `multiClient` | `reconnect` — covered | +| Changeset lifecycle | still only in `sessionDiffs` | all 8 `changeset/*` actions uncovered | +| OTLP export | still only in `otlpLogs` | `otlp/exportLogs`, `otlp/exportMetrics`, `otlp/exportTraces` uncovered | +| Liveness | migrated — `suites/protocolContractsSuite.ts` | `ping` — covered | The filesystem family was the largest of these and is now covered by `suites/clientFilesystemSuite.ts` in the conformance tier — both the `resource*` command surface the host executes against its own filesystem, and the reverse direction where the host asks the *client* for a file it cannot otherwise reach. See [The filesystem, in both directions](#the-filesystem-in-both-directions). -Some contracts are covered by **neither** suite and need new tests outright: the entire `annotations/*` channel (5 actions), `invokeChangesetOperation`, `auth/required`, `root/progress`, and `chat/toolCallAuthRequired` / `chat/toolCallAuthResolved`. +Some contracts are covered by **neither** suite and need new tests outright: `auth/required`, `root/progress`, and `chat/toolCallAuthRequired` / `chat/toolCallAuthResolved`. The `annotations/*` channel is now covered by `suites/annotationsSuite.ts`. + +`reconnect` is only answerable on a transport that has **not** completed the handshake — it is the alternative to `initialize`, not a command an established connection can issue. Testing it therefore needs a second connection that can be dropped and re-established, which is what `IAgentHostE2ETestContext.connectClient` exists for; the shared per-test client cannot express it. --- diff --git a/src/vs/platform/agentHost/test/node/e2e/coverage/protocol-surface.json b/src/vs/platform/agentHost/test/node/e2e/coverage/protocol-surface.json index 32a9c684d17..15d9a764aab 100644 --- a/src/vs/platform/agentHost/test/node/e2e/coverage/protocol-surface.json +++ b/src/vs/platform/agentHost/test/node/e2e/coverage/protocol-surface.json @@ -5,14 +5,11 @@ "note": "A symbol is \"covered\" when an E2E test sends or receives it; this does not measure how deeply its semantics are asserted." }, "commands": { - "covered": 25, + "covered": 28, "total": 29, - "percentage": 86.2, + "percentage": 96.55, "uncovered": [ - "fetchTurns", - "invokeChangesetOperation", - "ping", - "reconnect" + "invokeChangesetOperation" ] }, "notifications": { @@ -28,15 +25,10 @@ ] }, "actions": { - "covered": 44, + "covered": 59, "total": 85, - "percentage": 51.76, + "percentage": 69.41, "uncovered": [ - "annotations/entryRemoved", - "annotations/entrySet", - "annotations/removed", - "annotations/set", - "annotations/updated", "changeset/cleared", "changeset/contentChanged", "changeset/fileRemoved", @@ -45,21 +37,15 @@ "changeset/operationStatusChanged", "changeset/operationsChanged", "changeset/statusChanged", - "chat/activityChanged", "chat/error", "chat/inputAnswerChanged", - "chat/pendingMessageSet", "chat/reasoning", "chat/toolCallAuthRequired", "chat/toolCallAuthResolved", "chat/toolCallResultConfirmed", - "chat/turnsLoaded", - "chat/workingDirectoryRemoved", - "chat/workingDirectorySet", "resourceWatch/changed", "root/activeSessionsChanged", "root/agentsChanged", - "root/terminalsChanged", "session/activityChanged", "session/creationFailed", "session/customizationRemoved", @@ -68,11 +54,7 @@ "session/mcpServerStartRequested", "session/mcpServerStateChanged", "session/mcpServerStopRequested", - "session/workingDirectoryRemoved", - "session/workingDirectorySet", - "terminal/cleared", - "terminal/commandDetectionAvailable", - "terminal/exited" + "terminal/commandDetectionAvailable" ] } } diff --git a/src/vs/platform/agentHost/test/node/e2e/coverage/summary.json b/src/vs/platform/agentHost/test/node/e2e/coverage/summary.json index f88c7ef874f..31b498351a1 100644 --- a/src/vs/platform/agentHost/test/node/e2e/coverage/summary.json +++ b/src/vs/platform/agentHost/test/node/e2e/coverage/summary.json @@ -15,24 +15,24 @@ }, "total": { "statements": { - "covered": 64437, - "total": 91960, - "percentage": 70.07 + "covered": 65997, + "total": 93940, + "percentage": 70.25 }, "branches": { - "covered": 5476, - "total": 8687, + "covered": 5648, + "total": 8960, "percentage": 63.03 }, "functions": { - "covered": 2103, - "total": 3482, - "percentage": 60.39 + "covered": 2153, + "total": 3556, + "percentage": 60.54 }, "lines": { - "covered": 64437, - "total": 91960, - "percentage": 70.07 + "covered": 65997, + "total": 93940, + "percentage": 70.25 } }, "files": { @@ -258,14 +258,14 @@ }, "src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts": { "statements": { - "covered": 350, + "covered": 353, "total": 649, - "percentage": 53.92 + "percentage": 54.39 }, "branches": { - "covered": 16, - "total": 30, - "percentage": 53.33 + "covered": 19, + "total": 32, + "percentage": 59.37 }, "functions": { "covered": 10, @@ -273,16 +273,16 @@ "percentage": 41.66 }, "lines": { - "covered": 350, + "covered": 353, "total": 649, - "percentage": 53.92 + "percentage": 54.39 } }, "src/vs/platform/agentHost/common/agentHostFileSystemService.ts": { "statements": { - "covered": 57, - "total": 74, - "percentage": 77.02 + "covered": 58, + "total": 79, + "percentage": 73.41 }, "branches": { "covered": 0, @@ -295,16 +295,16 @@ "percentage": 0 }, "lines": { - "covered": 57, - "total": 74, - "percentage": 77.02 + "covered": 58, + "total": 79, + "percentage": 73.41 } }, "src/vs/platform/agentHost/common/agentHostGitService.ts": { "statements": { - "covered": 351, - "total": 361, - "percentage": 97.22 + "covered": 371, + "total": 381, + "percentage": 97.37 }, "branches": { "covered": 5, @@ -317,9 +317,9 @@ "percentage": 66.66 }, "lines": { - "covered": 351, - "total": 361, - "percentage": 97.22 + "covered": 371, + "total": 381, + "percentage": 97.37 } }, "src/vs/platform/agentHost/common/agentHostGitStateService.ts": { @@ -368,9 +368,9 @@ }, "src/vs/platform/agentHost/common/agentHostSchema.ts": { "statements": { - "covered": 659, - "total": 758, - "percentage": 86.93 + "covered": 673, + "total": 772, + "percentage": 87.17 }, "branches": { "covered": 46, @@ -383,9 +383,9 @@ "percentage": 68.18 }, "lines": { - "covered": 659, - "total": 758, - "percentage": 86.93 + "covered": 673, + "total": 772, + "percentage": 87.17 } }, "src/vs/platform/agentHost/common/agentHostSlashCommand.ts": { @@ -434,9 +434,9 @@ }, "src/vs/platform/agentHost/common/agentHostUri.ts": { "statements": { - "covered": 102, - "total": 163, - "percentage": 62.57 + "covered": 131, + "total": 206, + "percentage": 63.59 }, "branches": { "covered": 0, @@ -445,13 +445,13 @@ }, "functions": { "covered": 0, - "total": 4, + "total": 5, "percentage": 0 }, "lines": { - "covered": 102, - "total": 163, - "percentage": 62.57 + "covered": 131, + "total": 206, + "percentage": 63.59 } }, "src/vs/platform/agentHost/common/agentModelByokMeta.ts": { @@ -522,9 +522,9 @@ }, "src/vs/platform/agentHost/common/agentService.ts": { "statements": { - "covered": 2144, - "total": 2316, - "percentage": 92.57 + "covered": 2162, + "total": 2334, + "percentage": 92.63 }, "branches": { "covered": 24, @@ -537,9 +537,9 @@ "percentage": 42.85 }, "lines": { - "covered": 2144, - "total": 2316, - "percentage": 92.57 + "covered": 2162, + "total": 2334, + "percentage": 92.63 } }, "src/vs/platform/agentHost/common/ahpJsonlLogger.ts": { @@ -566,14 +566,14 @@ }, "src/vs/platform/agentHost/common/annotationsUri.ts": { "statements": { - "covered": 42, + "covered": 46, "total": 48, - "percentage": 87.5 + "percentage": 95.83 }, "branches": { - "covered": 3, - "total": 4, - "percentage": 75 + "covered": 7, + "total": 8, + "percentage": 87.5 }, "functions": { "covered": 3, @@ -581,9 +581,9 @@ "percentage": 100 }, "lines": { - "covered": 42, + "covered": 46, "total": 48, - "percentage": 87.5 + "percentage": 95.83 } }, "src/vs/platform/agentHost/common/changesetUri.ts": { @@ -764,8 +764,8 @@ }, "src/vs/platform/agentHost/common/diffComputeService.ts": { "statements": { - "covered": 40, - "total": 40, + "covered": 53, + "total": 53, "percentage": 100 }, "branches": { @@ -779,8 +779,8 @@ "percentage": 100 }, "lines": { - "covered": 40, - "total": 40, + "covered": 53, + "total": 53, "percentage": 100 } }, @@ -808,9 +808,9 @@ }, "src/vs/platform/agentHost/common/githubEndpoints.ts": { "statements": { - "covered": 95, - "total": 125, - "percentage": 76 + "covered": 100, + "total": 130, + "percentage": 76.92 }, "branches": { "covered": 3, @@ -823,9 +823,9 @@ "percentage": 75 }, "lines": { - "covered": 95, - "total": 125, - "percentage": 76 + "covered": 100, + "total": 130, + "percentage": 76.92 } }, "src/vs/platform/agentHost/common/meta/agentCompletionAttachmentMeta.ts": { @@ -923,9 +923,9 @@ "percentage": 84.71 }, "branches": { - "covered": 33, - "total": 44, - "percentage": 75 + "covered": 30, + "total": 41, + "percentage": 73.17 }, "functions": { "covered": 5, @@ -1011,9 +1011,9 @@ "percentage": 85.71 }, "branches": { - "covered": 16, - "total": 22, - "percentage": 72.72 + "covered": 17, + "total": 23, + "percentage": 73.91 }, "functions": { "covered": 11, @@ -1116,8 +1116,8 @@ }, "src/vs/platform/agentHost/common/sessionConfigKeys.ts": { "statements": { - "covered": 52, - "total": 52, + "covered": 54, + "total": 54, "percentage": 100 }, "branches": { @@ -1131,15 +1131,15 @@ "percentage": 100 }, "lines": { - "covered": 52, - "total": 52, + "covered": 54, + "total": 54, "percentage": 100 } }, "src/vs/platform/agentHost/common/sessionDataService.ts": { "statements": { - "covered": 421, - "total": 421, + "covered": 439, + "total": 439, "percentage": 100 }, "branches": { @@ -1153,11 +1153,33 @@ "percentage": 100 }, "lines": { - "covered": 421, - "total": 421, + "covered": 439, + "total": 439, "percentage": 100 } }, + "src/vs/platform/agentHost/common/sessionDbUri.ts": { + "statements": { + "covered": 74, + "total": 120, + "percentage": 61.66 + }, + "branches": { + "covered": 3, + "total": 6, + "percentage": 50 + }, + "functions": { + "covered": 3, + "total": 7, + "percentage": 42.85 + }, + "lines": { + "covered": 74, + "total": 120, + "percentage": 61.66 + } + }, "src/vs/platform/agentHost/common/state/agentSubscription.ts": { "statements": { "covered": 582, @@ -1248,24 +1270,24 @@ }, "src/vs/platform/agentHost/common/state/protocol/channels-annotations/reducer.ts": { "statements": { - "covered": 29, + "covered": 90, "total": 115, - "percentage": 25.21 + "percentage": 78.26 }, "branches": { - "covered": 0, - "total": 0, - "percentage": 100 + "covered": 12, + "total": 23, + "percentage": 52.17 }, "functions": { - "covered": 0, + "covered": 1, "total": 1, - "percentage": 0 + "percentage": 100 }, "lines": { - "covered": 29, + "covered": 90, "total": 115, - "percentage": 25.21 + "percentage": 78.26 } }, "src/vs/platform/agentHost/common/state/protocol/channels-changeset/reducer.ts": { @@ -1292,14 +1314,14 @@ }, "src/vs/platform/agentHost/common/state/protocol/channels-chat/reducer.ts": { "statements": { - "covered": 579, + "covered": 601, "total": 833, - "percentage": 69.5 + "percentage": 72.14 }, "branches": { - "covered": 108, - "total": 175, - "percentage": 61.71 + "covered": 114, + "total": 182, + "percentage": 62.63 }, "functions": { "covered": 14, @@ -1307,9 +1329,9 @@ "percentage": 100 }, "lines": { - "covered": 579, + "covered": 601, "total": 833, - "percentage": 69.5 + "percentage": 72.14 } }, "src/vs/platform/agentHost/common/state/protocol/channels-resource-watch/reducer.ts": { @@ -1776,24 +1798,24 @@ }, "src/vs/platform/agentHost/common/state/sessionState.ts": { "statements": { - "covered": 1076, - "total": 1396, - "percentage": 77.07 + "covered": 1129, + "total": 1458, + "percentage": 77.43 }, "branches": { - "covered": 90, - "total": 131, - "percentage": 68.7 + "covered": 98, + "total": 142, + "percentage": 69.01 }, "functions": { - "covered": 36, - "total": 57, - "percentage": 63.15 + "covered": 39, + "total": 62, + "percentage": 62.9 }, "lines": { - "covered": 1076, - "total": 1396, - "percentage": 77.07 + "covered": 1129, + "total": 1458, + "percentage": 77.43 } }, "src/vs/platform/agentHost/common/toolSearchConstants.ts": { @@ -1864,14 +1886,14 @@ }, "src/vs/platform/agentHost/node/agentHostAuthenticationService.ts": { "statements": { - "covered": 93, - "total": 121, - "percentage": 76.85 + "covered": 95, + "total": 127, + "percentage": 74.8 }, "branches": { "covered": 14, - "total": 22, - "percentage": 63.63 + "total": 24, + "percentage": 58.33 }, "functions": { "covered": 5, @@ -1879,9 +1901,9 @@ "percentage": 83.33 }, "lines": { - "covered": 93, - "total": 121, - "percentage": 76.85 + "covered": 95, + "total": 127, + "percentage": 74.8 } }, "src/vs/platform/agentHost/node/agentHostBangCommand.ts": { @@ -1979,9 +2001,9 @@ "percentage": 57.69 }, "branches": { - "covered": 25, - "total": 29, - "percentage": 86.2 + "covered": 23, + "total": 27, + "percentage": 85.18 }, "functions": { "covered": 7, @@ -2001,9 +2023,9 @@ "percentage": 62.38 }, "branches": { - "covered": 111, - "total": 144, - "percentage": 77.08 + "covered": 112, + "total": 145, + "percentage": 77.24 }, "functions": { "covered": 31, @@ -2023,9 +2045,9 @@ "percentage": 88.88 }, "branches": { - "covered": 16, - "total": 19, - "percentage": 84.21 + "covered": 15, + "total": 18, + "percentage": 83.33 }, "functions": { "covered": 10, @@ -2089,9 +2111,9 @@ "percentage": 79.77 }, "branches": { - "covered": 54, - "total": 69, - "percentage": 78.26 + "covered": 53, + "total": 68, + "percentage": 77.94 }, "functions": { "covered": 11, @@ -2304,24 +2326,24 @@ }, "src/vs/platform/agentHost/node/agentHostGitService.ts": { "statements": { - "covered": 755, - "total": 1257, - "percentage": 60.06 + "covered": 833, + "total": 1369, + "percentage": 60.84 }, "branches": { - "covered": 147, - "total": 217, - "percentage": 67.74 + "covered": 152, + "total": 226, + "percentage": 67.25 }, "functions": { - "covered": 37, - "total": 61, - "percentage": 60.65 + "covered": 40, + "total": 66, + "percentage": 60.6 }, "lines": { - "covered": 755, - "total": 1257, - "percentage": 60.06 + "covered": 833, + "total": 1369, + "percentage": 60.84 } }, "src/vs/platform/agentHost/node/agentHostGitStateService.ts": { @@ -2348,24 +2370,24 @@ }, "src/vs/platform/agentHost/node/agentHostHeadlessTerminal.ts": { "statements": { - "covered": 117, + "covered": 121, "total": 145, - "percentage": 80.68 + "percentage": 83.44 }, "branches": { - "covered": 11, - "total": 15, - "percentage": 73.33 - }, - "functions": { - "covered": 9, - "total": 12, + "covered": 12, + "total": 16, "percentage": 75 }, + "functions": { + "covered": 10, + "total": 12, + "percentage": 83.33 + }, "lines": { - "covered": 117, + "covered": 121, "total": 145, - "percentage": 80.68 + "percentage": 83.44 } }, "src/vs/platform/agentHost/node/agentHostLocalTurns.ts": { @@ -2502,9 +2524,9 @@ }, "src/vs/platform/agentHost/node/agentHostRepoInfoTelemetry.ts": { "statements": { - "covered": 88, - "total": 342, - "percentage": 25.73 + "covered": 89, + "total": 347, + "percentage": 25.64 }, "branches": { "covered": 2, @@ -2517,9 +2539,9 @@ "percentage": 15.38 }, "lines": { - "covered": 88, - "total": 342, - "percentage": 25.73 + "covered": 89, + "total": 347, + "percentage": 25.64 } }, "src/vs/platform/agentHost/node/agentHostRequestService.ts": { @@ -2590,9 +2612,9 @@ }, "src/vs/platform/agentHost/node/agentHostServerMain.ts": { "statements": { - "covered": 425, - "total": 474, - "percentage": 89.66 + "covered": 428, + "total": 477, + "percentage": 89.72 }, "branches": { "covered": 18, @@ -2605,31 +2627,31 @@ "percentage": 83.33 }, "lines": { - "covered": 425, - "total": 474, - "percentage": 89.66 + "covered": 428, + "total": 477, + "percentage": 89.72 } }, "src/vs/platform/agentHost/node/agentHostSessionTitleController.ts": { "statements": { - "covered": 437, - "total": 507, - "percentage": 86.19 + "covered": 446, + "total": 535, + "percentage": 83.36 }, "branches": { - "covered": 78, - "total": 99, - "percentage": 78.78 + "covered": 79, + "total": 100, + "percentage": 79 }, "functions": { "covered": 22, - "total": 28, - "percentage": 78.57 + "total": 29, + "percentage": 75.86 }, "lines": { - "covered": 437, - "total": 507, - "percentage": 86.19 + "covered": 446, + "total": 535, + "percentage": 83.36 } }, "src/vs/platform/agentHost/node/agentHostShellUtils.ts": { @@ -2700,24 +2722,24 @@ }, "src/vs/platform/agentHost/node/agentHostStateManager.ts": { "statements": { - "covered": 1325, + "covered": 1352, "total": 1495, - "percentage": 88.62 + "percentage": 90.43 }, "branches": { - "covered": 189, - "total": 232, - "percentage": 81.46 + "covered": 194, + "total": 235, + "percentage": 82.55 }, "functions": { - "covered": 50, + "covered": 51, "total": 62, - "percentage": 80.64 + "percentage": 82.25 }, "lines": { - "covered": 1325, + "covered": 1352, "total": 1495, - "percentage": 88.62 + "percentage": 90.43 } }, "src/vs/platform/agentHost/node/agentHostSyncOperationHandler.ts": { @@ -2766,24 +2788,24 @@ }, "src/vs/platform/agentHost/node/agentHostTelemetryReporter.ts": { "statements": { - "covered": 477, - "total": 608, - "percentage": 78.45 + "covered": 615, + "total": 752, + "percentage": 81.78 }, "branches": { - "covered": 29, - "total": 47, - "percentage": 61.7 + "covered": 31, + "total": 52, + "percentage": 59.61 }, "functions": { - "covered": 9, - "total": 14, - "percentage": 64.28 + "covered": 10, + "total": 15, + "percentage": 66.66 }, "lines": { - "covered": 477, - "total": 608, - "percentage": 78.45 + "covered": 615, + "total": 752, + "percentage": 81.78 } }, "src/vs/platform/agentHost/node/agentHostTelemetryService.ts": { @@ -2810,14 +2832,14 @@ }, "src/vs/platform/agentHost/node/agentHostTerminalManager.ts": { "statements": { - "covered": 875, + "covered": 887, "total": 971, - "percentage": 90.11 + "percentage": 91.34 }, "branches": { - "covered": 116, - "total": 146, - "percentage": 79.45 + "covered": 118, + "total": 147, + "percentage": 80.27 }, "functions": { "covered": 36, @@ -2825,9 +2847,9 @@ "percentage": 87.8 }, "lines": { - "covered": 875, + "covered": 887, "total": 971, - "percentage": 90.11 + "percentage": 91.34 } }, "src/vs/platform/agentHost/node/agentHostToolCallTracker.ts": { @@ -3008,46 +3030,46 @@ }, "src/vs/platform/agentHost/node/agentService.ts": { "statements": { - "covered": 2480, - "total": 4022, - "percentage": 61.66 + "covered": 2523, + "total": 4118, + "percentage": 61.26 }, "branches": { - "covered": 281, - "total": 476, - "percentage": 59.03 + "covered": 284, + "total": 482, + "percentage": 58.92 }, "functions": { - "covered": 91, - "total": 147, - "percentage": 61.9 + "covered": 93, + "total": 149, + "percentage": 62.41 }, "lines": { - "covered": 2480, - "total": 4022, - "percentage": 61.66 + "covered": 2523, + "total": 4118, + "percentage": 61.26 } }, "src/vs/platform/agentHost/node/agentSideEffects.ts": { "statements": { - "covered": 1455, - "total": 1811, - "percentage": 80.34 + "covered": 1535, + "total": 1885, + "percentage": 81.43 }, "branches": { - "covered": 237, - "total": 322, - "percentage": 73.6 + "covered": 261, + "total": 350, + "percentage": 74.57 }, "functions": { - "covered": 47, - "total": 53, - "percentage": 88.67 + "covered": 48, + "total": 54, + "percentage": 88.88 }, "lines": { - "covered": 1455, - "total": 1811, - "percentage": 80.34 + "covered": 1535, + "total": 1885, + "percentage": 81.43 } }, "src/vs/platform/agentHost/node/appNodeModules.ts": { @@ -3140,36 +3162,36 @@ }, "src/vs/platform/agentHost/node/claude/claudeAgent.ts": { "statements": { - "covered": 1616, - "total": 2325, - "percentage": 69.5 + "covered": 1644, + "total": 2400, + "percentage": 68.5 }, "branches": { - "covered": 135, - "total": 234, - "percentage": 57.69 + "covered": 136, + "total": 239, + "percentage": 56.9 }, "functions": { - "covered": 63, - "total": 93, - "percentage": 67.74 + "covered": 64, + "total": 95, + "percentage": 67.36 }, "lines": { - "covered": 1616, - "total": 2325, - "percentage": 69.5 + "covered": 1644, + "total": 2400, + "percentage": 68.5 } }, "src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts": { "statements": { - "covered": 266, + "covered": 264, "total": 312, - "percentage": 85.25 + "percentage": 84.61 }, "branches": { "covered": 12, - "total": 16, - "percentage": 75 + "total": 17, + "percentage": 70.58 }, "functions": { "covered": 10, @@ -3177,31 +3199,31 @@ "percentage": 66.66 }, "lines": { - "covered": 266, + "covered": 264, "total": 312, - "percentage": 85.25 + "percentage": 84.61 } }, "src/vs/platform/agentHost/node/claude/claudeAgentSession.ts": { "statements": { - "covered": 905, - "total": 1121, - "percentage": 80.73 + "covered": 952, + "total": 1169, + "percentage": 81.43 }, "branches": { - "covered": 47, - "total": 74, - "percentage": 63.51 + "covered": 52, + "total": 82, + "percentage": 63.41 }, "functions": { - "covered": 24, - "total": 51, - "percentage": 47.05 + "covered": 25, + "total": 52, + "percentage": 48.07 }, "lines": { - "covered": 905, - "total": 1121, - "percentage": 80.73 + "covered": 952, + "total": 1169, + "percentage": 81.43 } }, "src/vs/platform/agentHost/node/claude/claudeCanUseTool.ts": { @@ -3470,9 +3492,9 @@ }, "src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts": { "statements": { - "covered": 502, + "covered": 501, "total": 647, - "percentage": 77.58 + "percentage": 77.43 }, "branches": { "covered": 57, @@ -3485,9 +3507,9 @@ "percentage": 78.26 }, "lines": { - "covered": 502, + "covered": 501, "total": 647, - "percentage": 77.58 + "percentage": 77.43 } }, "src/vs/platform/agentHost/node/claude/claudeSdkMessageRouter.ts": { @@ -3514,14 +3536,14 @@ }, "src/vs/platform/agentHost/node/claude/claudeSdkOptions.ts": { "statements": { - "covered": 238, - "total": 255, - "percentage": 93.33 + "covered": 250, + "total": 268, + "percentage": 93.28 }, "branches": { "covered": 10, - "total": 19, - "percentage": 52.63 + "total": 20, + "percentage": 50 }, "functions": { "covered": 3, @@ -3529,9 +3551,9 @@ "percentage": 75 }, "lines": { - "covered": 238, - "total": 255, - "percentage": 93.33 + "covered": 250, + "total": 268, + "percentage": 93.28 } }, "src/vs/platform/agentHost/node/claude/claudeSdkPipeline.ts": { @@ -3580,24 +3602,24 @@ }, "src/vs/platform/agentHost/node/claude/claudeSessionMetadataStore.ts": { "statements": { - "covered": 162, - "total": 205, - "percentage": 79.02 + "covered": 182, + "total": 240, + "percentage": 75.83 }, "branches": { - "covered": 14, - "total": 25, - "percentage": 56 + "covered": 15, + "total": 29, + "percentage": 51.72 }, "functions": { - "covered": 5, - "total": 7, - "percentage": 71.42 + "covered": 6, + "total": 8, + "percentage": 75 }, "lines": { - "covered": 162, - "total": 205, - "percentage": 79.02 + "covered": 182, + "total": 240, + "percentage": 75.83 } }, "src/vs/platform/agentHost/node/claude/claudeSessionPermissionMode.ts": { @@ -4328,9 +4350,9 @@ }, "src/vs/platform/agentHost/node/codex/codexReplayMapper.ts": { "statements": { - "covered": 58, - "total": 222, - "percentage": 26.12 + "covered": 60, + "total": 240, + "percentage": 25 }, "branches": { "covered": 0, @@ -4339,13 +4361,13 @@ }, "functions": { "covered": 0, - "total": 6, + "total": 7, "percentage": 0 }, "lines": { - "covered": 58, - "total": 222, - "percentage": 26.12 + "covered": 60, + "total": 240, + "percentage": 25 } }, "src/vs/platform/agentHost/node/codex/codexSessionConfigKeys.ts": { @@ -4355,9 +4377,9 @@ "percentage": 87.81 }, "branches": { - "covered": 20, - "total": 41, - "percentage": 48.78 + "covered": 21, + "total": 42, + "percentage": 50 }, "functions": { "covered": 11, @@ -4394,14 +4416,14 @@ }, "src/vs/platform/agentHost/node/codex/codexShellCommand.ts": { "statements": { - "covered": 38, + "covered": 36, "total": 42, - "percentage": 90.47 + "percentage": 85.71 }, "branches": { - "covered": 5, - "total": 8, - "percentage": 62.5 + "covered": 2, + "total": 6, + "percentage": 33.33 }, "functions": { "covered": 2, @@ -4409,9 +4431,9 @@ "percentage": 100 }, "lines": { - "covered": 38, + "covered": 36, "total": 42, - "percentage": 90.47 + "percentage": 85.71 } }, "src/vs/platform/agentHost/node/codex/codexUserInputMapper.ts": { @@ -4438,31 +4460,31 @@ }, "src/vs/platform/agentHost/node/commandAutoApprover.ts": { "statements": { - "covered": 477, - "total": 595, - "percentage": 80.16 + "covered": 492, + "total": 606, + "percentage": 81.18 }, "branches": { - "covered": 33, - "total": 61, - "percentage": 54.09 + "covered": 36, + "total": 64, + "percentage": 56.25 }, "functions": { - "covered": 12, - "total": 14, - "percentage": 85.71 + "covered": 14, + "total": 16, + "percentage": 87.5 }, "lines": { - "covered": 477, - "total": 595, - "percentage": 80.16 + "covered": 492, + "total": 606, + "percentage": 81.18 } }, "src/vs/platform/agentHost/node/copilot/agentHostSandboxEngine.ts": { "statements": { - "covered": 99, - "total": 141, - "percentage": 70.21 + "covered": 97, + "total": 139, + "percentage": 69.78 }, "branches": { "covered": 4, @@ -4475,16 +4497,16 @@ "percentage": 30.76 }, "lines": { - "covered": 99, - "total": 141, - "percentage": 70.21 + "covered": 97, + "total": 139, + "percentage": 69.78 } }, "src/vs/platform/agentHost/node/copilot/buildSessionEvents.ts": { "statements": { - "covered": 90, - "total": 278, - "percentage": 32.37 + "covered": 91, + "total": 279, + "percentage": 32.61 }, "branches": { "covered": 0, @@ -4497,9 +4519,9 @@ "percentage": 0 }, "lines": { - "covered": 90, - "total": 278, - "percentage": 32.37 + "covered": 91, + "total": 279, + "percentage": 32.61 } }, "src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts": { @@ -4553,9 +4575,9 @@ "percentage": 69.75 }, "branches": { - "covered": 354, + "covered": 355, "total": 594, - "percentage": 59.59 + "percentage": 59.76 }, "functions": { "covered": 153, @@ -4570,24 +4592,24 @@ }, "src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts": { "statements": { - "covered": 3063, - "total": 4791, - "percentage": 63.93 + "covered": 3230, + "total": 5048, + "percentage": 63.98 }, "branches": { - "covered": 403, - "total": 640, - "percentage": 62.96 + "covered": 422, + "total": 696, + "percentage": 60.63 }, "functions": { - "covered": 118, - "total": 177, - "percentage": 66.66 + "covered": 125, + "total": 185, + "percentage": 67.56 }, "lines": { - "covered": 3063, - "total": 4791, - "percentage": 63.93 + "covered": 3230, + "total": 5048, + "percentage": 63.98 } }, "src/vs/platform/agentHost/node/copilot/copilotAttachmentUtils.ts": { @@ -4724,23 +4746,23 @@ }, "src/vs/platform/agentHost/node/copilot/copilotSessionWrapper.ts": { "statements": { - "covered": 285, - "total": 285, + "covered": 290, + "total": 290, "percentage": 100 }, "branches": { - "covered": 56, - "total": 56, + "covered": 57, + "total": 57, "percentage": 100 }, "functions": { - "covered": 52, - "total": 53, - "percentage": 98.11 + "covered": 53, + "total": 54, + "percentage": 98.14 }, "lines": { - "covered": 285, - "total": 285, + "covered": 290, + "total": 290, "percentage": 100 } }, @@ -4856,9 +4878,9 @@ }, "src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts": { "statements": { - "covered": 908, - "total": 1201, - "percentage": 75.6 + "covered": 910, + "total": 1203, + "percentage": 75.64 }, "branches": { "covered": 96, @@ -4871,31 +4893,31 @@ "percentage": 67.74 }, "lines": { - "covered": 908, - "total": 1201, - "percentage": 75.6 + "covered": 910, + "total": 1203, + "percentage": 75.64 } }, "src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts": { "statements": { - "covered": 539, - "total": 829, - "percentage": 65.01 + "covered": 572, + "total": 866, + "percentage": 66.05 }, "branches": { - "covered": 62, - "total": 130, - "percentage": 47.69 + "covered": 60, + "total": 134, + "percentage": 44.77 }, "functions": { - "covered": 17, - "total": 18, - "percentage": 94.44 + "covered": 19, + "total": 20, + "percentage": 95 }, "lines": { - "covered": 539, - "total": 829, - "percentage": 65.01 + "covered": 572, + "total": 866, + "percentage": 66.05 } }, "src/vs/platform/agentHost/node/copilot/pendingEditContentStore.ts": { @@ -5059,9 +5081,9 @@ "percentage": 40.01 }, "branches": { - "covered": 63, - "total": 80, - "percentage": 78.75 + "covered": 62, + "total": 79, + "percentage": 78.48 }, "functions": { "covered": 15, @@ -5098,31 +5120,31 @@ }, "src/vs/platform/agentHost/node/diffComputeService.ts": { "statements": { - "covered": 75, - "total": 94, - "percentage": 79.78 + "covered": 81, + "total": 102, + "percentage": 79.41 }, "branches": { - "covered": 9, - "total": 13, - "percentage": 69.23 + "covered": 11, + "total": 15, + "percentage": 73.33 }, "functions": { - "covered": 4, - "total": 4, - "percentage": 100 + "covered": 6, + "total": 7, + "percentage": 85.71 }, "lines": { - "covered": 75, - "total": 94, - "percentage": 79.78 + "covered": 81, + "total": 102, + "percentage": 79.41 } }, "src/vs/platform/agentHost/node/diffWorkerMain.ts": { "statements": { - "covered": 78, - "total": 92, - "percentage": 84.78 + "covered": 92, + "total": 174, + "percentage": 52.87 }, "branches": { "covered": 13, @@ -5131,13 +5153,13 @@ }, "functions": { "covered": 4, - "total": 4, - "percentage": 100 + "total": 9, + "percentage": 44.44 }, "lines": { - "covered": 78, - "total": 92, - "percentage": 84.78 + "covered": 92, + "total": 174, + "percentage": 52.87 } }, "src/vs/platform/agentHost/node/gitDiffContent.ts": { @@ -5318,24 +5340,24 @@ }, "src/vs/platform/agentHost/node/protocolServerHandler.ts": { "statements": { - "covered": 1161, - "total": 1646, - "percentage": 70.53 + "covered": 1283, + "total": 1638, + "percentage": 78.32 }, "branches": { - "covered": 154, - "total": 227, - "percentage": 67.84 + "covered": 187, + "total": 261, + "percentage": 71.64 }, "functions": { - "covered": 62, + "covered": 66, "total": 81, - "percentage": 76.54 + "percentage": 81.48 }, "lines": { - "covered": 1161, - "total": 1646, - "percentage": 70.53 + "covered": 1283, + "total": 1638, + "percentage": 78.32 } }, "src/vs/platform/agentHost/node/serverUrls.ts": { @@ -5367,9 +5389,9 @@ "percentage": 79.18 }, "branches": { - "covered": 23, - "total": 26, - "percentage": 88.46 + "covered": 22, + "total": 25, + "percentage": 88 }, "functions": { "covered": 12, @@ -5384,24 +5406,24 @@ }, "src/vs/platform/agentHost/node/sessionDatabase.ts": { "statements": { - "covered": 591, - "total": 781, - "percentage": 75.67 + "covered": 657, + "total": 869, + "percentage": 75.6 }, "branches": { - "covered": 98, - "total": 119, - "percentage": 82.35 + "covered": 102, + "total": 122, + "percentage": 83.6 }, "functions": { - "covered": 31, - "total": 51, - "percentage": 60.78 + "covered": 32, + "total": 53, + "percentage": 60.37 }, "lines": { - "covered": 591, - "total": 781, - "percentage": 75.67 + "covered": 657, + "total": 869, + "percentage": 75.6 } }, "src/vs/platform/agentHost/node/sessionDiffAggregator.ts": { @@ -5428,24 +5450,24 @@ }, "src/vs/platform/agentHost/node/sessionPermissions.ts": { "statements": { - "covered": 480, - "total": 635, - "percentage": 75.59 + "covered": 499, + "total": 658, + "percentage": 75.83 }, "branches": { - "covered": 60, - "total": 98, - "percentage": 61.22 + "covered": 68, + "total": 108, + "percentage": 62.96 }, "functions": { - "covered": 21, - "total": 25, - "percentage": 84 + "covered": 22, + "total": 27, + "percentage": 81.48 }, "lines": { - "covered": 480, - "total": 635, - "percentage": 75.59 + "covered": 499, + "total": 658, + "percentage": 75.83 } }, "src/vs/platform/agentHost/node/shared/agentBranchNameGenerator.ts": { @@ -5558,6 +5580,28 @@ "percentage": 94.44 } }, + "src/vs/platform/agentHost/node/shared/arcToolEdit.ts": { + "statements": { + "covered": 78, + "total": 97, + "percentage": 80.41 + }, + "branches": { + "covered": 14, + "total": 24, + "percentage": 58.33 + }, + "functions": { + "covered": 7, + "total": 7, + "percentage": 100 + }, + "lines": { + "covered": 78, + "total": 97, + "percentage": 80.41 + } + }, "src/vs/platform/agentHost/node/shared/copilotApiService.ts": { "statements": { "covered": 1029, @@ -5580,6 +5624,28 @@ "percentage": 80.26 } }, + "src/vs/platform/agentHost/node/shared/editArcReporter.ts": { + "statements": { + "covered": 142, + "total": 373, + "percentage": 38.06 + }, + "branches": { + "covered": 6, + "total": 11, + "percentage": 54.54 + }, + "functions": { + "covered": 4, + "total": 16, + "percentage": 25 + }, + "lines": { + "covered": 142, + "total": 373, + "percentage": 38.06 + } + }, "src/vs/platform/agentHost/node/shared/editChunkExtractor.ts": { "statements": { "covered": 123, @@ -5648,24 +5714,24 @@ }, "src/vs/platform/agentHost/node/shared/fileEditTracker.ts": { "statements": { - "covered": 254, - "total": 307, - "percentage": 82.73 + "covered": 229, + "total": 246, + "percentage": 93.08 }, "branches": { "covered": 27, - "total": 37, - "percentage": 72.97 + "total": 34, + "percentage": 79.41 }, "functions": { - "covered": 9, - "total": 12, - "percentage": 75 + "covered": 7, + "total": 7, + "percentage": 100 }, "lines": { - "covered": 254, - "total": 307, - "percentage": 82.73 + "covered": 229, + "total": 246, + "percentage": 93.08 } }, "src/vs/platform/agentHost/node/shared/forwardedChatError.ts": { @@ -5780,9 +5846,9 @@ }, "src/vs/platform/agentHost/node/shared/sessionServerTools.ts": { "statements": { - "covered": 503, - "total": 1097, - "percentage": 45.85 + "covered": 505, + "total": 1098, + "percentage": 45.99 }, "branches": { "covered": 3, @@ -5791,13 +5857,13 @@ }, "functions": { "covered": 3, - "total": 49, - "percentage": 6.12 + "total": 50, + "percentage": 6 }, "lines": { - "covered": 503, - "total": 1097, - "percentage": 45.85 + "covered": 505, + "total": 1098, + "percentage": 45.99 } }, "src/vs/platform/agentHost/node/shared/shellCommandExecution.ts": { @@ -5824,24 +5890,24 @@ }, "src/vs/platform/agentHost/node/shared/worktreeIsolation.ts": { "statements": { - "covered": 652, - "total": 837, - "percentage": 77.89 + "covered": 729, + "total": 926, + "percentage": 78.72 }, "branches": { - "covered": 67, - "total": 105, - "percentage": 63.8 + "covered": 73, + "total": 113, + "percentage": 64.6 }, "functions": { - "covered": 29, - "total": 40, - "percentage": 72.5 + "covered": 31, + "total": 42, + "percentage": 73.8 }, "lines": { - "covered": 652, - "total": 837, - "percentage": 77.89 + "covered": 729, + "total": 926, + "percentage": 78.72 } }, "src/vs/platform/agentHost/node/webSocketTransport.ts": { diff --git a/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts b/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts index 55824cfa1d0..5acbcc4a322 100644 --- a/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts +++ b/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts @@ -792,6 +792,23 @@ export class AgentHostE2EServerLease { return { server: this._server, client: this._client }; } + /** + * Open an additional connection to the current server. + * + * `reconnect` is only answerable on a transport that has not completed the + * handshake, so a test that exercises connection recovery needs a second + * socket it can close and re-establish without disturbing the shared + * client. The caller owns the returned client and must close it. + */ + async connectClient(): Promise { + if (!this._server) { + throw new Error('[agent-host-e2e] no server acquired yet'); + } + const client = new TestProtocolClient(this._server.port); + await client.connect(); + return client; + } + /** Stop the current shared server so the next {@link acquire} starts a fresh one. */ private async _recycleSharedServer(): Promise { try { diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/agentHostE2ESuites.ts b/src/vs/platform/agentHost/test/node/e2e/suites/agentHostE2ESuites.ts index ff85a0dc701..ef38c052392 100644 --- a/src/vs/platform/agentHost/test/node/e2e/suites/agentHostE2ESuites.ts +++ b/src/vs/platform/agentHost/test/node/e2e/suites/agentHostE2ESuites.ts @@ -7,7 +7,9 @@ import { AgentHostE2EServerLease, type IAgentHostE2EProviderConfig, removeTempDi import type { IAgentHostTarget } from '../harness/agentHostTarget.js'; import type { TestProtocolClient } from '../../serverIntegrationTestHelpers.js'; import { defineCoreTests } from './coreSuite.js'; +import { defineAnnotationsTests } from './annotationsSuite.js'; import { defineClientFilesystemTests } from './clientFilesystemSuite.js'; +import { defineProtocolContractTests } from './protocolContractsSuite.js'; import { defineFileOperationsTests } from './fileOperationsSuite.js'; import { defineHostFeaturesTests } from './hostFeaturesSuite.js'; import { defineMultiChatTests } from './multiChatSuite.js'; @@ -50,6 +52,12 @@ function defineSuite(config: IAgentHostE2EProviderConfig, options: IDefineOption runRecordOnlyTests: RUN_RECORD_ONLY_TESTS, registerNoModelTrafficTest: title => noModelTrafficTestTitles.add(title), get observedModelRequestBodies() { return lease?.observedModelRequestBodies ?? []; }, + connectClient: () => { + if (!lease) { + throw new Error('[agent-host-e2e] no server lease'); + } + return lease.connectClient(); + }, }; suiteSetup(async function () { @@ -101,6 +109,8 @@ function defineSuite(config: IAgentHostE2EProviderConfig, options: IDefineOption defineHostFeaturesTests(context); defineStateOperationsTests(context); defineClientFilesystemTests(context); + defineAnnotationsTests(context); + defineProtocolContractTests(context); } // Suites that contain only parity-tier scenarios. diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/annotationsSuite.ts b/src/vs/platform/agentHost/test/node/e2e/suites/annotationsSuite.ts new file mode 100644 index 00000000000..160709a9663 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/e2e/suites/annotationsSuite.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * The annotations channel: client-owned review comments anchored to a resource. + * + * Every annotations action is client-dispatchable (see `ClientAnnotationsAction` + * in `action-origin.generated.ts`), and the same reducer runs on both sides, so + * these scenarios exercise the synchronized-state contract end to end without + * any model traffic: dispatch, observe the server echo, then read the channel + * back to confirm the host applied the same reduction the client did. + * + * This channel was previously covered by neither the E2E suite nor the frozen + * `../protocol/` suite. + */ + +import assert from 'assert'; +import { mkdtempSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from '../../../../../../base/common/path.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../../base/common/uuid.js'; +import type { SubscribeResult } from '../../../../common/state/protocol/commands.js'; +import { ActionType } from '../../../../common/state/sessionActions.js'; +import { buildAnnotationsUri } from '../../../../common/annotationsUri.js'; +import { createRealSession } from '../harness/agentHostE2ETestHarness.js'; +import { getActionEnvelope, isActionNotification } from '../../serverIntegrationTestHelpers.js'; +import { conformanceTest, type IAgentHostE2ETestContext } from './e2eTestContext.js'; + +/** The subset of `Annotation` these tests assert on. */ +interface IObservedAnnotation { + readonly id: string; + readonly turnId: string; + readonly resolved: boolean; + readonly entries: readonly { readonly id: string; readonly text: string }[]; +} + +export function defineAnnotationsTests(context: IAgentHostE2ETestContext): void { + const { config, createdSessions, tempDirs } = context; + + /** + * Client sequence numbers must strictly increase for the lifetime of a + * client, and the suite shares one across tests, so they cannot be + * hard-coded per scenario. + */ + let clientSeq = 3000; + function nextClientSeq(): number { + return clientSeq++; + } + + async function createAnnotatedSession(prefix: string): Promise<{ sessionUri: string; annotationsUri: string; resource: string }> { + const workspace = mkdtempSync(join(tmpdir(), `ahp-${prefix}-`)); + tempDirs.push(workspace); + const sessionUri = await createRealSession(context.client, config, `${prefix}-${config.provider}`, createdSessions, URI.file(workspace)); + const annotationsUri = buildAnnotationsUri(sessionUri); + await context.client.call('subscribe', { channel: annotationsUri }); + context.client.clearReceived(); + return { sessionUri, annotationsUri, resource: URI.file(join(workspace, 'reviewed.ts')).toString() }; + } + + function dispatchAnnotationAction(channel: string, action: object): void { + context.client.dispatch({ channel, clientSeq: nextClientSeq(), action: action as Parameters[0]['action'] }); + } + + /** + * Waits for the server to echo `actionType` on the annotations channel, then + * reads the channel back. Reading without waiting would race the reduction + * and could observe the pre-dispatch state. + */ + async function annotationsAfter(channel: string, actionType: string): Promise { + await context.client.waitForNotification(n => + isActionNotification(n, actionType) && getActionEnvelope(n).channel === channel, + 30_000, + ); + const subscribed = await context.client.call('subscribe', { channel }); + return (subscribed.snapshot!.state as { annotations: IObservedAnnotation[] }).annotations; + } + + conformanceTest(context, 'an annotation dispatched by a client is applied to the channel', async function () { + const { annotationsUri, resource } = await createAnnotatedSession('annotations-set'); + const annotationId = generateUuid(); + + dispatchAnnotationAction(annotationsUri, { + type: ActionType.AnnotationsSet, + annotation: { + id: annotationId, + turnId: 'turn-annotate', + resource, + resolved: false, + entries: [{ id: `${annotationId}:0`, text: 'needs a second look' }], + }, + }); + + const annotations = await annotationsAfter(annotationsUri, 'annotations/set'); + + assert.deepStrictEqual(annotations.map(annotation => ({ + id: annotation.id, + turnId: annotation.turnId, + resolved: annotation.resolved, + entries: annotation.entries.map(entry => entry.text), + })), [{ + id: annotationId, + turnId: 'turn-annotate', + resolved: false, + entries: ['needs a second look'], + }]); + }); + + conformanceTest(context, 'an annotation can be resolved without resending its entries', async function () { + const { annotationsUri, resource } = await createAnnotatedSession('annotations-resolve'); + const annotationId = generateUuid(); + + dispatchAnnotationAction(annotationsUri, { + type: ActionType.AnnotationsSet, + annotation: { id: annotationId, turnId: 'turn-resolve', resource, resolved: false, entries: [{ id: `${annotationId}:0`, text: 'why this branch?' }] }, + }); + await annotationsAfter(annotationsUri, 'annotations/set'); + + // `annotations/updated` carries only the fields that change, so + // resolving must not disturb the entries already on the annotation. + context.client.clearReceived(); + dispatchAnnotationAction(annotationsUri, { type: ActionType.AnnotationsUpdated, annotationId, resolved: true }); + + const annotations = await annotationsAfter(annotationsUri, 'annotations/updated'); + + assert.deepStrictEqual(annotations.map(annotation => ({ + resolved: annotation.resolved, + entries: annotation.entries.map(entry => entry.text), + })), [{ + resolved: true, + entries: ['why this branch?'], + }]); + }); + + conformanceTest(context, 'entries can be added to and removed from an annotation', async function () { + const { annotationsUri, resource } = await createAnnotatedSession('annotations-entries'); + const annotationId = generateUuid(); + const replyId = `${annotationId}:1`; + + dispatchAnnotationAction(annotationsUri, { + type: ActionType.AnnotationsSet, + annotation: { id: annotationId, turnId: 'turn-entries', resource, resolved: false, entries: [{ id: `${annotationId}:0`, text: 'original' }] }, + }); + await annotationsAfter(annotationsUri, 'annotations/set'); + + context.client.clearReceived(); + dispatchAnnotationAction(annotationsUri, { type: ActionType.AnnotationsEntrySet, annotationId, entry: { id: replyId, text: 'reply' } }); + const withReply = await annotationsAfter(annotationsUri, 'annotations/entrySet'); + + context.client.clearReceived(); + dispatchAnnotationAction(annotationsUri, { type: ActionType.AnnotationsEntryRemoved, annotationId, entryId: replyId }); + const withoutReply = await annotationsAfter(annotationsUri, 'annotations/entryRemoved'); + + assert.deepStrictEqual({ + afterEntrySet: withReply[0]?.entries.map(entry => entry.text), + afterEntryRemoved: withoutReply[0]?.entries.map(entry => entry.text), + }, { + afterEntrySet: ['original', 'reply'], + afterEntryRemoved: ['original'], + }); + }); + + conformanceTest(context, 'removing an annotation clears it from the channel', async function () { + const { annotationsUri, resource } = await createAnnotatedSession('annotations-remove'); + const annotationId = generateUuid(); + + dispatchAnnotationAction(annotationsUri, { + type: ActionType.AnnotationsSet, + annotation: { id: annotationId, turnId: 'turn-remove', resource, resolved: false, entries: [{ id: `${annotationId}:0`, text: 'transient' }] }, + }); + await annotationsAfter(annotationsUri, 'annotations/set'); + + context.client.clearReceived(); + dispatchAnnotationAction(annotationsUri, { type: ActionType.AnnotationsRemoved, annotationId }); + + assert.deepStrictEqual(await annotationsAfter(annotationsUri, 'annotations/removed'), []); + }); +} diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/e2eTestContext.ts b/src/vs/platform/agentHost/test/node/e2e/suites/e2eTestContext.ts index d4a40b49d91..0744932eed5 100644 --- a/src/vs/platform/agentHost/test/node/e2e/suites/e2eTestContext.ts +++ b/src/vs/platform/agentHost/test/node/e2e/suites/e2eTestContext.ts @@ -44,6 +44,12 @@ export interface IAgentHostE2ETestContext { readonly runRecordOnlyTests: boolean; readonly registerNoModelTrafficTest: (title: string) => void; readonly observedModelRequestBodies: readonly string[]; + /** + * Open an extra connection to the same server. Needed only by tests that + * exercise connection lifecycle, which cannot be expressed on the single + * shared connection. The caller must close what it opens. + */ + readonly connectClient: () => Promise; } function registerHostOnlyTest(context: IAgentHostE2ETestContext, title: string, run: Mocha.AsyncFunc, enabled: boolean): void { diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/protocolContractsSuite.ts b/src/vs/platform/agentHost/test/node/e2e/suites/protocolContractsSuite.ts new file mode 100644 index 00000000000..28b95460cd4 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/e2e/suites/protocolContractsSuite.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. + *--------------------------------------------------------------------------------------------*/ + +/** + * Protocol-level contracts that are not tied to any one channel: liveness, + * turn-history paging, and how the host answers a client action it declares + * but does not yet implement. + * + * All of these are host-owned and cross no model boundary. + */ + +import assert from 'assert'; +import { mkdtempSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from '../../../../../../base/common/path.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ReconnectResultType, type ReconnectResult, type SubscribeResult } from '../../../../common/state/protocol/commands.js'; +import { ActionType, type StateAction } from '../../../../common/state/sessionActions.js'; +import { buildDefaultChatUri, MessageKind, ROOT_STATE_URI } from '../../../../common/state/sessionState.js'; +import { createRealSession, dispatchTurn } from '../harness/agentHostE2ETestHarness.js'; +import { PROTOCOL_VERSION } from '../../../../common/state/protocol/version/registry.js'; +import { getActionEnvelope, isActionNotification, type TestProtocolClient } from '../../serverIntegrationTestHelpers.js'; +import { conformanceTest, type IAgentHostE2ETestContext } from './e2eTestContext.js'; + +export function defineProtocolContractTests(context: IAgentHostE2ETestContext): void { + const { config, createdSessions, tempDirs } = context; + + /** + * Client sequence numbers must strictly increase for the lifetime of a + * client, and the suite shares one across tests, so they cannot be + * hard-coded per scenario. + */ + let clientSeq = 4000; + function nextClientSeq(): number { + return clientSeq++; + } + + /** Dispatch on the shared client and wait for the server to echo it back. */ + async function dispatchAndWaitOnShared(channel: string, action: StateAction): Promise { + const seq = nextClientSeq(); + context.client.dispatch({ channel, clientSeq: seq, action }); + await context.client.waitForNotification(n => + isActionNotification(n, action.type) + && getActionEnvelope(n).channel === channel + && getActionEnvelope(n).origin?.clientSeq === seq, + 30_000, + ); + } + + async function createSession(prefix: string): Promise<{ sessionUri: string; workspace: string }> { + const workspace = mkdtempSync(join(tmpdir(), `ahp-${prefix}-`)); + tempDirs.push(workspace); + const sessionUri = await createRealSession(context.client, config, `${prefix}-${config.provider}`, createdSessions, URI.file(workspace)); + return { sessionUri, workspace }; + } + + conformanceTest(context, 'ping answers while the connection is live', async function () { + // Liveness has no payload — the response itself is the signal, so the + // contract is that the call resolves rather than what it returns. + await context.client.call('ping', { channel: ROOT_STATE_URI }); + }); + + conformanceTest(context, 'fetchTurns reports the turns a chat already has', async function () { + const { sessionUri } = await createSession('fetch-turns'); + const chatUri = buildDefaultChatUri(sessionUri); + await context.client.call('subscribe', { channel: chatUri }); + + // Give the chat a turn to page over. `/rename` is handled entirely by the + // host's local-command dispatcher, so the turn is real without crossing + // the model boundary and without depending on a shell. + dispatchTurn(context.client, sessionUri, 'turn-fetch', '/rename Fetch Turns', 1); + await context.client.waitForNotification(n => + isActionNotification(n, 'chat/turnComplete') + && getActionEnvelope(n).channel === chatUri + && (getActionEnvelope(n).action as { turnId: string }).turnId === 'turn-fetch', + 60_000, + ); + + context.client.clearReceived(); + await context.client.call('fetchTurns', { channel: chatUri }); + + // `fetchTurns` answers with an empty result and delivers the page as a + // `chat/turnsLoaded` action, so the action is the contract. + const loaded = await context.client.waitForNotification(n => + isActionNotification(n, 'chat/turnsLoaded') && getActionEnvelope(n).channel === chatUri, + 30_000, + ); + + assert.strictEqual((getActionEnvelope(loaded).action as { type: string }).type, ActionType.ChatTurnsLoaded); + }); + + /** + * Runs `body` against a second connection that has completed the handshake + * under its own clientId, then drops that connection and hands back a fresh + * un-handshaked one. `reconnect` is only answerable pre-handshake, so + * recovery cannot be exercised on the shared client. + */ + async function afterConnectionDrop( + clientId: string, + body: (client: TestProtocolClient) => Promise, + ): Promise<{ carried: T; revived: TestProtocolClient }> { + const first = await context.connectClient(); + let carried: T; + try { + await first.call('initialize', { channel: ROOT_STATE_URI, protocolVersions: [PROTOCOL_VERSION], clientId }); + carried = await body(first); + } finally { + first.close(); + } + return { carried, revived: await context.connectClient() }; + } + + conformanceTest(context, 'reconnect replays only the actions a dropped client missed', async function () { + const { sessionUri } = await createSession('reconnect'); + const chatUri = buildDefaultChatUri(sessionUri); + + const { carried: seenThrough, revived } = await afterConnectionDrop(`reconnect-${config.provider}`, async first => { + await first.call('subscribe', { channel: chatUri }); + const seq = nextClientSeq(); + first.dispatch({ + channel: chatUri, + clientSeq: seq, + action: { type: ActionType.ChatDraftChanged, draft: { text: 'seen before the drop', origin: { kind: MessageKind.User } } }, + }); + const echoed = await first.waitForNotification(n => + isActionNotification(n, 'chat/draftChanged') + && getActionEnvelope(n).channel === chatUri + && getActionEnvelope(n).origin?.clientSeq === seq, + 30_000, + ); + return getActionEnvelope(echoed).serverSeq; + }); + + try { + // Produced while nobody was listening on that clientId, so it can only + // reach the client through replay. + await dispatchAndWaitOnShared(chatUri, { type: ActionType.ChatDraftChanged, draft: { text: 'missed while disconnected', origin: { kind: MessageKind.User } } }); + + const result = await revived.call('reconnect', { + channel: ROOT_STATE_URI, + clientId: `reconnect-${config.provider}`, + lastSeenServerSeq: seenThrough, + subscriptions: [chatUri], + }); + + // A client that reconnects inside the replay window must be able to + // catch up by applying actions rather than discarding local state for + // a fresh snapshot, so the cutoff has to be exclusive and exact. + assert.deepStrictEqual({ + type: result.type, + replayedAlreadySeen: result.type === ReconnectResultType.Replay + && result.actions.some(envelope => envelope.serverSeq <= seenThrough), + replayedTheGap: result.type === ReconnectResultType.Replay + && result.actions.some(envelope => envelope.serverSeq > seenThrough), + }, { + type: ReconnectResultType.Replay, + replayedAlreadySeen: false, + replayedTheGap: true, + }); + } finally { + revived.close(); + } + }); + + conformanceTest(context, 'reconnect reports a subscription it cannot resume as missing', async function () { + const { sessionUri } = await createSession('reconnect-missing'); + const chatUri = buildDefaultChatUri(sessionUri); + // A channel that never existed stands in for one disposed while the client + // was away: either way the server cannot resume it, and the client has to + // be told rather than left waiting on a dead channel. + const goneUri = URI.from({ scheme: 'agenthost-terminal', authority: 'e2e', path: '/never-existed' }).toString(); + + const { carried: seenThrough, revived } = await afterConnectionDrop(`reconnect-missing-${config.provider}`, async first => { + const subscribed = await first.call('subscribe', { channel: chatUri }); + return subscribed.snapshot!.fromSeq; + }); + + try { + const result = await revived.call('reconnect', { + channel: ROOT_STATE_URI, + clientId: `reconnect-missing-${config.provider}`, + lastSeenServerSeq: seenThrough, + subscriptions: [chatUri, goneUri], + }); + + assert.deepStrictEqual({ + type: result.type, + missing: result.type === ReconnectResultType.Replay ? result.missing : undefined, + }, { + type: ReconnectResultType.Replay, + missing: [goneUri], + }); + } finally { + revived.close(); + } + }); + + // The protocol declares working-directory mutation on both the session and + // chat channels, but the host rejects all four: applying one would change + // the synchronized directory set without reconfiguring the agent's actual + // access. Each is answered through the normal reconciliation path so the + // client can roll back its optimistic write-ahead action instead of leaving + // it pending until reconnect. + const unsupportedWorkingDirectoryActions = [ + { notification: 'session/workingDirectorySet', channel: 'session', build: (directory: string): StateAction => ({ type: ActionType.SessionWorkingDirectorySet, directory }) }, + { notification: 'session/workingDirectoryRemoved', channel: 'session', build: (directory: string): StateAction => ({ type: ActionType.SessionWorkingDirectoryRemoved, directory }) }, + { notification: 'chat/workingDirectorySet', channel: 'chat', build: (directory: string): StateAction => ({ type: ActionType.ChatWorkingDirectorySet, directory }) }, + { notification: 'chat/workingDirectoryRemoved', channel: 'chat', build: (directory: string): StateAction => ({ type: ActionType.ChatWorkingDirectoryRemoved, directory }) }, + ] as const; + + for (const unsupported of unsupportedWorkingDirectoryActions) { + conformanceTest(context, `${unsupported.notification} is rejected rather than silently dropped`, async function () { + const { sessionUri, workspace } = await createSession('unsupported-action'); + const channel = unsupported.channel === 'session' ? sessionUri : buildDefaultChatUri(sessionUri); + await context.client.call('subscribe', { channel }); + context.client.clearReceived(); + + const seq = nextClientSeq(); + const directory = URI.file(join(workspace, 'second-root')).toString(); + context.client.dispatch({ channel, clientSeq: seq, action: unsupported.build(directory) }); + + const rejected = await context.client.waitForNotification(n => + isActionNotification(n, unsupported.notification) && getActionEnvelope(n).channel === channel, + 30_000, + ); + const envelope = getActionEnvelope(rejected) as { rejectionReason?: string; origin?: { clientSeq?: number } }; + const state = (await context.client.call('subscribe', { channel })).snapshot!.state as { workingDirectories?: readonly string[] }; + + assert.deepStrictEqual({ + hasRejectionReason: typeof envelope.rejectionReason === 'string' && envelope.rejectionReason.length > 0, + echoedClientSeq: envelope.origin?.clientSeq, + // The reducer is deliberately not run, so state never moves. + directoryApplied: (state.workingDirectories ?? []).includes(directory), + }, { + hasRejectionReason: true, + echoedClientSeq: seq, + directoryApplied: false, + }); + }); + } +} diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/stateOperationsSuite.ts b/src/vs/platform/agentHost/test/node/e2e/suites/stateOperationsSuite.ts index eb674db23ef..120ab30469f 100644 --- a/src/vs/platform/agentHost/test/node/e2e/suites/stateOperationsSuite.ts +++ b/src/vs/platform/agentHost/test/node/e2e/suites/stateOperationsSuite.ts @@ -27,6 +27,7 @@ import { } from '../../../../common/state/sessionState.js'; import { createRealSession } from '../harness/agentHostE2ETestHarness.js'; import { getActionEnvelope, isActionNotification } from '../../serverIntegrationTestHelpers.js'; +import type { AhpNotification } from '../../../../common/state/sessionProtocol.js'; import { conformanceTest, type IAgentHostE2ETestContext } from './e2eTestContext.js'; export function defineStateOperationsTests(context: IAgentHostE2ETestContext): void { @@ -239,6 +240,41 @@ export function defineStateOperationsTests(context: IAgentHostE2ETestContext): v assert.strictEqual((await chatState(chatUri)).draft, undefined); }); + conformanceTest(context, 'a message queued on an idle chat is promoted straight into a turn', async function () { + const { chatUri } = await createSession('queue-promote'); + context.client.clearReceived(); + + // Queueing exists to hold work while a turn is running. With nothing + // running there is nothing to wait for, so the host must start the + // message rather than park it — otherwise a queued message on an idle + // chat would never run at all. `/rename` keeps the promoted turn inside + // the host's local-command dispatcher, with no shell and no model. + await dispatchAndWait(chatUri, 1, { + type: ActionType.ChatPendingMessageSet, + kind: PendingMessageKind.Queued, + id: 'queued-1', + message: userMessage('/rename Queue Promoted'), + }); + + const started = await context.client.waitForNotification(n => + isActionNotification(n, 'chat/turnStarted') + && getActionEnvelope(n).channel === chatUri + && (getActionEnvelope(n).action as { queuedMessageId?: string }).queuedMessageId === 'queued-1', + 30_000, + ); + const turnId = (getActionEnvelope(started).action as { turnId: string }).turnId; + await context.client.waitForNotification(n => + isActionNotification(n, 'chat/turnComplete') + && getActionEnvelope(n).channel === chatUri + && (getActionEnvelope(n).action as { turnId: string }).turnId === turnId, + 60_000, + ); + + // Promotion has to be atomic with removal: a message left in the queue + // after being started would run a second time on the next idle event. + assert.deepStrictEqual((await chatState(chatUri)).queuedMessages ?? [], []); + }); + conformanceTest(context, 'removing a missing queued message leaves chat state unchanged', async function () { const { chatUri } = await createSession('queue-remove-missing'); @@ -364,6 +400,92 @@ export function defineStateOperationsTests(context: IAgentHostE2ETestContext): v }); }); + conformanceTest(context, 'clearing a terminal drops the scrollback the client already saw', async function () { + await withTerminal('terminal-clear', async ({ terminalUri }) => { + context.client.dispatch({ + channel: terminalUri, + clientSeq: 1, + action: { type: ActionType.TerminalInput, data: 'node -p "\'CLEAR_MARKER\'"\r' }, + }); + await context.client.waitForNotification(n => + isActionNotification(n, 'terminal/data') + && getActionEnvelope(n).channel === terminalUri + && (getActionEnvelope(n).action as { data: string }).data.includes('CLEAR_MARKER'), + 30_000, + ); + + await dispatchAndWait(terminalUri, 2, { type: ActionType.TerminalCleared }); + + // The scrollback lives in host state, not just in the client's view, + // so clearing must empty it for every subscriber including one that + // subscribes later. + assert.deepStrictEqual((await terminalState(terminalUri)).content, []); + }); + }); + + conformanceTest(context, 'a terminal whose shell exits reports its exit code', async function () { + await withTerminal('terminal-exit', async ({ terminalUri }) => { + context.client.clearReceived(); + context.client.dispatch({ + channel: terminalUri, + clientSeq: 1, + action: { type: ActionType.TerminalInput, data: 'exit\r' }, + }); + + const exited = await context.client.waitForNotification(n => + isActionNotification(n, 'terminal/exited') && getActionEnvelope(n).channel === terminalUri, + 30_000, + ); + + // The exit code itself is the shell's, not the host's, so only its + // presence and its arrival in state are contractual. + const action = getActionEnvelope(exited).action as { exitCode?: number }; + assert.deepStrictEqual({ + reportedExitCode: typeof action.exitCode, + stateMatchesNotification: (await terminalState(terminalUri)).exitCode === action.exitCode, + }, { + reportedExitCode: 'number', + stateMatchesNotification: true, + }); + }); + }); + + conformanceTest(context, 'root state tracks terminals as they appear and disappear', async function () { + // The first terminal also establishes the connection; root can only be + // subscribed once the client has handshaked. + const { clientId, workspace } = await createTerminal('terminal-root'); + await context.client.call('subscribe', { channel: ROOT_STATE_URI }); + context.client.clearReceived(); + + function terminalsIn(n: AhpNotification): readonly { resource: string }[] { + return (getActionEnvelope(n).action as { terminals?: readonly { resource: string }[] }).terminals ?? []; + } + + // Root is how a client discovers terminals it did not create itself, so + // it has to be told on both edges, not only on creation. + const observedUri = URI.from({ scheme: 'agenthost-terminal', authority: 'e2e', path: `/${generateUuid()}` }).toString(); + await context.client.call('createTerminal', { + channel: observedUri, + claim: { kind: TerminalClaimKind.Client, clientId }, + name: 'E2E terminal-root-observed', + cwd: URI.file(workspace).toString(), + cols: 90, + rows: 30, + }); + await context.client.waitForNotification(n => + isActionNotification(n, 'root/terminalsChanged') + && terminalsIn(n).some(terminal => terminal.resource === observedUri), + 30_000, + ); + + await disposeTerminal(observedUri); + await context.client.waitForNotification(n => + isActionNotification(n, 'root/terminalsChanged') + && !terminalsIn(n).some(terminal => terminal.resource === observedUri), + 30_000, + ); + }); + conformanceTest(context, 'disposeTerminal removes the terminal from root state', async function () { const { terminalUri } = await createTerminal('terminal-dispose'); From 705ede9033af896f23282a557f4cc3306a704f53 Mon Sep 17 00:00:00 2001 From: Aaron Munger <2019016+amunger@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:28:25 -0700 Subject: [PATCH 16/86] selectable text ask user widget (#328069) * make text selectable in the ask user widget * only change the question area * chat: remove obsolete selection-aware clicks Question selection is scoped to the non-interactive title, so answer rows no longer need drag-aware click handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../widget/chatContentParts/media/chatQuestionCarousel.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index a02a87f4460..6c49afefb5a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -95,6 +95,8 @@ font-weight: var(--vscode-agents-fontWeight-semiBold); font-size: var(--vscode-agents-fontSize-heading3); margin: 0; + user-select: text; + -webkit-user-select: text; .rendered-markdown { a { From 5add818e927ad95a68013534894ebecd5801307d Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 30 Jul 2026 19:32:31 +0200 Subject: [PATCH 17/86] address PR review feedback for the custom view grid - suspend the single-pane editor resize sync during the visibility pass so it cannot write back the desired part visibility - disable Open in Editor Area while a custom view replaces the sessions grid - scope the sessions chat accessibility help so it does not claim a shown custom view - move phone behaviour into MobileCustomViewGridPart, selected by CustomViewGridParts - drop the unread title from ICustomViewDescriptor --- src/vs/sessions/LAYOUT.md | 2 +- .../browser/parts/customViewGridPart.ts | 23 +++------ .../browser/parts/customViewGridParts.ts | 13 +++-- .../parts/mobile/mobileCustomViewGridPart.ts | 51 +++++++++++++++++++ src/vs/sessions/browser/workbench.ts | 20 +++++--- .../browser/sessionsChatAccessibilityHelp.ts | 5 +- .../browser/customViewTest.contribution.ts | 1 - .../editor/browser/editor.contribution.ts | 4 +- .../services/customView/browser/customView.ts | 3 -- .../test/browser/customViewService.test.ts | 2 +- .../sessions/customViewNode.fixture.ts | 1 - 11 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 src/vs/sessions/browser/parts/mobile/mobileCustomViewGridPart.ts diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index 437ada9e98a..ee5f780f304 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -96,7 +96,7 @@ Which view is shown is owned by `ICustomViewService` ([services/customView/brows **Dismissal.** Opening a session (`SessionsService._startOpenSession`, which every explicit open gesture funnels through) hides the custom view. On phone layouts showing one pushes a `MobileNavigationStack` layer, so the Android back button dismisses it. Actions that operate on the hidden parts — Toggle Side Panel, Open Terminal, and the secondary side bar toggle — are disabled while it is shown (`CustomViewVisibleContext`). -**Chrome.** Each grid leaf is a `CustomViewNode` ([browser/parts/customViewNode.ts](src/vs/sessions/browser/parts/customViewNode.ts)) that owns the shared header — title, optional description and the contributed actions rendered either as an icon toolbar or a button bar — above a scroll container that grows a bottom border on the header as soon as the content is scrolled. The header band and the content are centred and capped to `AGENTS_CENTERED_CONTENT_MAX_WIDTH` (the same measure the session views use); a view may override it with `AbstractCustomView.maxWidth`. Views only fill the content container and are disposed when hidden. +**Chrome.** Each grid leaf is a `CustomViewNode` ([browser/parts/customViewNode.ts](src/vs/sessions/browser/parts/customViewNode.ts)) that owns the shared header — title, optional description and the contributed actions rendered either as an icon toolbar or a button bar — above a scroll container that grows a bottom border on the header as soon as the content is scrolled. The header band and the content are centred and capped to `AGENTS_CENTERED_CONTENT_MAX_WIDTH` (the same measure the session views use); a view may override it with `AbstractCustomView.maxWidth`. Views only fill the content container and are disposed when hidden. On phone-class viewports `CustomViewGridParts` selects `MobileCustomViewGridPart` instead, mirroring `SessionsParts`/`MobileSessionsPart`. **Card chrome is shared.** The Sessions Part and the Custom View Grid both carry the `agents-part-card` class (`AGENTS_PART_CARD_CLASS`) and use `agentsPartCard.ts` for their metrics, themed colors and content-box math, so their padding, margins, background, border and corner radius are defined once and are identical. diff --git a/src/vs/sessions/browser/parts/customViewGridPart.ts b/src/vs/sessions/browser/parts/customViewGridPart.ts index a3c7b8df692..ae1df26b6c1 100644 --- a/src/vs/sessions/browser/parts/customViewGridPart.ts +++ b/src/vs/sessions/browser/parts/customViewGridPart.ts @@ -14,9 +14,8 @@ import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { Part } from '../../../workbench/browser/part.js'; import { Parts } from '../../../workbench/services/layout/browser/layoutService.js'; import { ICustomViewDescriptor } from '../../services/customView/browser/customView.js'; -import { applyAgentsPartCardStyles, clearAgentsPartCardStyles, getAgentsPartCardContentSize } from './agentsPartCard.js'; +import { applyAgentsPartCardStyles, getAgentsPartCardContentSize } from './agentsPartCard.js'; import { CustomViewNode } from './customViewNode.js'; -import { isPhoneLayout } from './mobile/mobileLayout.js'; import { IAgentWorkbenchLayoutService } from '../workbench.js'; /** @@ -40,12 +39,12 @@ export class CustomViewGridPart extends Part { private _contentArea: HTMLElement | undefined; private readonly _node = this._register(new MutableDisposable()); private _descriptor: ICustomViewDescriptor | undefined; - private _lastContentSize: { readonly width: number; readonly height: number } | undefined; + protected _lastContentSize: { readonly width: number; readonly height: number } | undefined; constructor( @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, - @IAgentWorkbenchLayoutService private readonly agentWorkbenchLayoutService: IAgentWorkbenchLayoutService, + @IAgentWorkbenchLayoutService protected readonly agentWorkbenchLayoutService: IAgentWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super( @@ -110,13 +109,7 @@ export class CustomViewGridPart extends Part { override updateStyles(): void { super.updateStyles(); - const container = assertReturnsDefined(this.getContainer()); - if (isPhoneLayout(this.layoutService)) { - clearAgentsPartCardStyles(container); - return; - } - - applyAgentsPartCardStyles(container, this.theme); + applyAgentsPartCardStyles(assertReturnsDefined(this.getContainer()), this.theme); } override layout(width: number, height: number, top: number, left: number): void { @@ -124,18 +117,14 @@ export class CustomViewGridPart extends Part { return; } - // On phone the part fills the grid cell without the card margins/border. - const cardSize = isPhoneLayout(this.layoutService) - ? { width, height } - : getAgentsPartCardContentSize(width, height, this.agentWorkbenchLayoutService.isEditorPaneVisible()); - + const cardSize = getAgentsPartCardContentSize(width, height, this.agentWorkbenchLayoutService.isEditorPaneVisible()); const { contentSize } = this.layoutContents(cardSize.width, cardSize.height); this._layoutNode(contentSize.width, contentSize.height); super.layout(width, height, top, left); } - private _layoutNode(width: number, height: number): void { + protected _layoutNode(width: number, height: number): void { this._lastContentSize = { width, height }; const node = this._node.value; diff --git a/src/vs/sessions/browser/parts/customViewGridParts.ts b/src/vs/sessions/browser/parts/customViewGridParts.ts index 34561933633..54db89cad79 100644 --- a/src/vs/sessions/browser/parts/customViewGridParts.ts +++ b/src/vs/sessions/browser/parts/customViewGridParts.ts @@ -4,15 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../base/common/lifecycle.js'; +import { getClientArea } from '../../../base/browser/dom.js'; +import { mainWindow } from '../../../base/browser/window.js'; import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { ICustomViewDescriptor } from '../../services/customView/browser/customView.js'; import { ICustomViewGridPartService } from '../../services/customView/browser/customViewGridPartService.js'; import { CustomViewGridPart } from './customViewGridPart.js'; +import { MobileCustomViewGridPart } from './mobile/mobileCustomViewGridPart.js'; /** - * Owns the lifecycle of the {@link CustomViewGridPart}. Registered as an eager - * singleton so the part registers itself with the workbench layout service + * Owns the lifecycle of the {@link CustomViewGridPart}. Selects the mobile vs. + * desktop variant based on viewport width at construction time. Registered as an + * eager singleton so the part registers itself with the workbench layout service * before the workbench starts laying out parts. */ export class CustomViewGridParts extends Disposable implements ICustomViewGridPartService { @@ -26,7 +30,10 @@ export class CustomViewGridParts extends Disposable implements ICustomViewGridPa ) { super(); - this._mainPart = this._register(instantiationService.createInstance(CustomViewGridPart)); + const { width } = getClientArea(mainWindow.document.body); + const isPhoneLayout = width < 640; + + this._mainPart = this._register(instantiationService.createInstance(isPhoneLayout ? MobileCustomViewGridPart : CustomViewGridPart)); } setView(descriptor: ICustomViewDescriptor | undefined): void { diff --git a/src/vs/sessions/browser/parts/mobile/mobileCustomViewGridPart.ts b/src/vs/sessions/browser/parts/mobile/mobileCustomViewGridPart.ts new file mode 100644 index 00000000000..08d61c706cc --- /dev/null +++ b/src/vs/sessions/browser/parts/mobile/mobileCustomViewGridPart.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; +import { Part } from '../../../../workbench/browser/part.js'; +import { clearAgentsPartCardStyles } from '../agentsPartCard.js'; +import { CustomViewGridPart } from '../customViewGridPart.js'; +import { isPhoneLayout } from './mobileLayout.js'; + +/** + * Mobile variant of {@link CustomViewGridPart}. + * + * On phone-sized viewports the part fills the full grid cell without card + * margins or border insets. When the viewport transitions to tablet/desktop + * (e.g. device rotation crossing the phone breakpoint) this delegates to the + * desktop implementation so layout math stays correct. + */ +export class MobileCustomViewGridPart extends CustomViewGridPart { + + override updateStyles(): void { + // Always run the desktop implementation first so inline styles are set on + // tablet/desktop transitions; clear them again in phone mode so CSS takes over. + super.updateStyles(); + + if (!isPhoneLayout(this.layoutService)) { + return; + } + + const container = this.getContainer(); + if (container) { + clearAgentsPartCardStyles(container); + } + } + + override layout(width: number, height: number, top: number, left: number): void { + if (!isPhoneLayout(this.layoutService)) { + super.layout(width, height, top, left); + return; + } + + if (!this.layoutService.isVisible(Parts.CUSTOM_VIEW_GRID_PART)) { + return; + } + + const { contentSize } = this.layoutContents(width, height); + this._layoutNode(contentSize.width, contentSize.height); + Part.prototype.layout.call(this, width, height, top, left); + } +} diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 8688386d257..1ee6deedd6a 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -2366,14 +2366,18 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic this._applyingCustomViewGridVisibility = true; try { - // One pass, revealing before hiding so the row never goes empty in between. - if (visible) { - this.workbenchGrid.setViewVisible(this.customViewGridPartView, true); - this._applyExclusivePartVisibility(); - } else { - this._applyExclusivePartVisibility(); - this.workbenchGrid.setViewVisible(this.customViewGridPartView, false); - } + // Suspended so the single-pane width sync cannot read the transient node + // widths as a sash drag and write back the desired visibility. + this._runWithEditorResizeSyncSuspended(() => { + // One pass, revealing before hiding so the row never goes empty in between. + if (visible) { + this.workbenchGrid.setViewVisible(this.customViewGridPartView, true); + this._applyExclusivePartVisibility(); + } else { + this._applyExclusivePartVisibility(); + this.workbenchGrid.setViewVisible(this.customViewGridPartView, false); + } + }); } finally { this._applyingCustomViewGridVisibility = false; } diff --git a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts index 1b791f6aea9..1db90b0fe6c 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts @@ -9,6 +9,8 @@ import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { AccessibilityVerbositySettingId } from '../../../../workbench/contrib/accessibility/browser/accessibilityConfiguration.js'; import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { CustomViewVisibleContext } from '../../../common/contextkeys.js'; import { localize } from '../../../../nls.js'; import { FOCUS_AI_CUSTOMIZATION_VIEW_ID } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeView.js'; import { ISessionsPartService } from '../../../services/sessions/browser/sessionsPartService.js'; @@ -17,7 +19,8 @@ export class SessionsChatAccessibilityHelp implements IAccessibleViewImplementat readonly priority = 120; readonly name = 'sessionsChat'; readonly type = AccessibleViewType.Help; - readonly when = IsSessionsWindowContext; + // A custom view replaces the chat surface this help describes, so it does not apply then. + readonly when = ContextKeyExpr.and(IsSessionsWindowContext, CustomViewVisibleContext.negate()); getProvider(accessor: ServicesAccessor) { const sessionsPartService = accessor.get(ISessionsPartService); diff --git a/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts b/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts index 529a8d801bf..aac67a78445 100644 --- a/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts +++ b/src/vs/sessions/contrib/customViewTest/browser/customViewTest.contribution.ts @@ -51,7 +51,6 @@ class TestCustomViewContribution extends Disposable { this._register(customViewService.registerCustomView({ id: TEST_CUSTOM_VIEW_ID, - title: localize('testCustomView.title', "Test Custom View"), ctor: new SyncDescriptor(TestCustomView), actions: { style: 'toolbar', menuId: Menus.CustomViewTest }, })); diff --git a/src/vs/sessions/contrib/editor/browser/editor.contribution.ts b/src/vs/sessions/contrib/editor/browser/editor.contribution.ts index 61f71e68a62..ac2b453ebd8 100644 --- a/src/vs/sessions/contrib/editor/browser/editor.contribution.ts +++ b/src/vs/sessions/contrib/editor/browser/editor.contribution.ts @@ -21,7 +21,7 @@ import { ActiveEditorContext, AuxiliaryBarVisibleContext, EditorPartModalContext import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { Menus } from '../../../browser/menus.js'; import { IAgentWorkbenchLayoutService } from '../../../browser/workbench.js'; -import { EditorMaximizedContext, HasDockedDetailsContext, SinglePaneLayoutEnabledContext } from '../../../common/contextkeys.js'; +import { CustomViewVisibleContext, EditorMaximizedContext, HasDockedDetailsContext, SinglePaneLayoutEnabledContext } from '../../../common/contextkeys.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; @@ -337,6 +337,8 @@ class OpenModalEditorInEditorAction extends Action2 { title: localize2('openModalEditorInEditor', "Open in Editor Area"), icon: Codicon.openInWindow, f1: false, + // The editor area is not rendered while a custom view replaces the sessions grid. + precondition: CustomViewVisibleContext.negate(), menu: { id: MenuId.ModalEditorTitle, group: 'navigation', diff --git a/src/vs/sessions/services/customView/browser/customView.ts b/src/vs/sessions/services/customView/browser/customView.ts index 89cfecaf0e5..69cabf4c492 100644 --- a/src/vs/sessions/services/customView/browser/customView.ts +++ b/src/vs/sessions/services/customView/browser/customView.ts @@ -25,9 +25,6 @@ export interface ICustomViewDescriptor { /** Stable id, used by `ICustomViewService.showCustomView`. */ readonly id: string; - /** Title used before the view instance exists (aria, command labels). */ - readonly title: string; - readonly ctor: SyncDescriptor; readonly actions?: ICustomViewActions; diff --git a/src/vs/sessions/services/customView/test/browser/customViewService.test.ts b/src/vs/sessions/services/customView/test/browser/customViewService.test.ts index 09f589ebacf..0425c176524 100644 --- a/src/vs/sessions/services/customView/test/browser/customViewService.test.ts +++ b/src/vs/sessions/services/customView/test/browser/customViewService.test.ts @@ -25,7 +25,7 @@ suite('Sessions - CustomViewService', () => { } function descriptor(id: string): ICustomViewDescriptor { - return { id, title: id, ctor: new SyncDescriptor(TestCustomView) }; + return { id, ctor: new SyncDescriptor(TestCustomView) }; } test('shows, replaces and hides registered views', () => { diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/customViewNode.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/customViewNode.fixture.ts index adf23ebe8dd..058dfd9f925 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/customViewNode.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/customViewNode.fixture.ts @@ -82,7 +82,6 @@ function renderNode(ctx: ComponentFixtureContext, options: IFixtureOptions): voi const descriptor: ICustomViewDescriptor = { id: 'fixture.customView', - title: options.title, ctor: new SyncDescriptor(FixtureCustomView, [options.title, options.description, options.itemCount, options.maxWidth]), }; From cb1185742d0ad02fcc812ae5aeb90c5cc3c6fa81 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 30 Jul 2026 14:24:58 -0400 Subject: [PATCH 18/86] Drive session cost from SDK's API (#328232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Drive session cost from SDK's API * Address PR feedback on SDK-driven session cost - Serialize `usage.getMetrics` reads behind a `Throttler`. Several handlers refresh the session total, so their RPCs could overlap and an older one resolving last would publish a session cost that visibly regresses. A high-water guard can't reject the stale value because the total is legitimately non-monotonic — `history.truncate` makes the SDK re-fold usage from the surviving events. Keeping one read in flight removes the interleaving and coalesces the redundant reads a burst of events would issue. - Emit the compaction's per-turn cost synchronously, before awaiting the metrics read, then re-emit to enrich it with the session total. The terminal `session.idle` can close the turn mid-read, after which the reducer drops its usage — so a compaction whose turn ends immediately was never persisted. - Teach `hasReportedUsage` about `sessionTotalNanoAiu`. A compaction billed while no turn was active advances only the session total, and such a report was being treated as empty and dropped by `usageInfoToChatUsage`. - Correct two comments that described the wrong billing source: per-turn cost accumulates synchronously from each event's `copilotUsage`; the SDK's usage metrics supply only the session-wide total. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c33a0289-7f6d-40de-9f11-4b2e474a5e9c --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c33a0289-7f6d-40de-9f11-4b2e474a5e9c --- .../agentHost/common/state/sessionState.ts | 13 + .../node/copilot/copilotAgentSession.ts | 387 +++++++++++------- .../test/node/copilotAgentSession.test.ts | 326 ++++++++++++--- .../agentHost/agentHostSessionHandler.ts | 4 + .../agentHost/stateToProgressAdapter.ts | 8 + .../chat/common/chatService/chatService.ts | 14 + .../contrib/chat/common/model/chatModel.ts | 50 ++- .../common/model/chatSessionOperationLog.ts | 1 + .../ChatService_can_deserialize.0.snap | 1 + ...rvice_can_deserialize_with_response.0.snap | 1 + .../ChatService_can_serialize.1.snap | 2 + .../ChatService_sendRequest_fails.0.snap | 1 + .../chat/test/common/model/chatModel.test.ts | 47 +++ 13 files changed, 639 insertions(+), 216 deletions(-) diff --git a/src/vs/platform/agentHost/common/state/sessionState.ts b/src/vs/platform/agentHost/common/state/sessionState.ts index 97ca3739874..d0be6ee48bb 100644 --- a/src/vs/platform/agentHost/common/state/sessionState.ts +++ b/src/vs/platform/agentHost/common/state/sessionState.ts @@ -111,7 +111,15 @@ export interface UsageInfoMeta { autoModeResolved?: IAutoModeResolvedInfo; /** Copilot-specific usage breakdown, including nano-AIU totals. */ copilotUsage?: { + /** This turn's nano-AIU cost. */ totalNanoAiu?: number; + /** + * The whole session's accumulated nano-AIU cost, as reported by the + * backend rather than summed from the turns. Clients SHOULD prefer this + * over adding up per-turn totals: it is authoritative, and it also + * covers work billed outside any turn (e.g. an out-of-turn compaction). + */ + sessionTotalNanoAiu?: number; [key: string]: unknown; }; /** @@ -210,6 +218,7 @@ export function readUsageInfoMeta(usage: UsageInfo | undefined): UsageInfoMeta { const rawUsage = copilotUsage as Record; const usage: Mutable> = {}; if (typeof rawUsage['totalNanoAiu'] === 'number') { usage.totalNanoAiu = rawUsage['totalNanoAiu']; } + if (typeof rawUsage['sessionTotalNanoAiu'] === 'number') { usage.sessionTotalNanoAiu = rawUsage['sessionTotalNanoAiu']; } result.copilotUsage = usage; } const quotaSnapshots = meta['quotaSnapshots']; @@ -248,6 +257,10 @@ export function hasReportedUsage(usage: UsageInfo | undefined): boolean { const meta = readUsageInfoMeta(usage); // Negative totals are treated as absent, matching how credits are read for display. return (typeof meta.copilotUsage?.totalNanoAiu === 'number' && meta.copilotUsage.totalNanoAiu >= 0) + // A report can carry only the session total — a compaction billed while no turn + // was active advances it without any per-event billing payload — and that is + // still consumption worth showing. + || (typeof meta.copilotUsage?.sessionTotalNanoAiu === 'number' && meta.copilotUsage.sessionTotalNanoAiu >= 0) || (typeof meta.cost === 'number' && meta.cost >= 0); } diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index f66c7a32ac1..42de10a84c5 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { CopilotSession, CurrentToolMetadata, ExitPlanModeRequest, McpServersLoadedServer, MessageOptions, PermissionAllowAllMode, PermissionAutoApproval, PermissionRequestResult, PermissionResult, SessionConfig, Tool, ToolResultObject, McpServerStatus as SdkMcpServerStatus } from '@github/copilot-sdk'; -import { raceCancellation, Sequencer } from '../../../../base/common/async.js'; +import { raceCancellation, Sequencer, Throttler } from '../../../../base/common/async.js'; import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter } from '../../../../base/common/event.js'; @@ -43,7 +43,7 @@ import { isAgentFeedbackAnnotationsAttachment, renderAgentFeedbackAnnotationsAtt import { ISessionDatabase, ISessionDataService, SESSION_ATTACHMENTS_DIRNAME } from '../../common/sessionDataService.js'; import { MessageAttachmentKind, ToolCallContributorKind, type FileEdit, type MessageAttachment } from '../../common/state/protocol/state.js'; import { ActionType, isChatAction, type ChatAction, type SessionAction } from '../../common/state/sessionActions.js'; -import { MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallStatus, ToolResultContentType, buildSubagentSessionUri, getToolSubagentContent, isDefaultChatUri, isSubagentSession, withSessionPromptCacheState, type Message, type PendingMessage, type ChatInputAnswer, type ChatInputOption, type ChatInputQuestion, type ChatInputRequest, type ToolCallResult, type ToolResultContent, type ToolResultTerminalContent, type Turn, type UsageInfo, type UsageInfoMeta } from '../../common/state/sessionState.js'; +import { MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallStatus, ToolResultContentType, buildSubagentSessionUri, getToolSubagentContent, isDefaultChatUri, isSubagentSession, withSessionPromptCacheState, type Message, type PendingMessage, type ChatInputAnswer, type ChatInputOption, type ChatInputQuestion, type ChatInputRequest, type ToolCallResult, type ToolResultContent, type ToolResultTerminalContent, type Turn, type UsageInfo, type UsageInfoMeta, type IContextAttributionData } from '../../common/state/sessionState.js'; import { IAgentConfigurationService } from '../agentConfigurationService.js'; import type { IExitPlanModeResponse } from './copilotAgent.js'; import { CopilotSessionWrapper } from './copilotSessionWrapper.js'; @@ -488,21 +488,33 @@ class CopilotTurn { private readonly _stopWatch = StopWatch.create(false); /** - * Accumulated Copilot usage for this turn, in nano-AIU, keyed by scope. - * Scope `''` is the parent turn aggregate (parent agent calls plus every - * subagent call), so the parent turn's reported cost is the full turn - * total. Each subagent additionally accumulates under its `parentToolCallId` - * so its own component cost can be reported on the subagent's child session. + * This turn's own Copilot cost in nano-AIU, summed from the `copilotUsage` + * carried by the model calls the turn caused — its own, every subagent's, + * and any compaction that ran mid-turn. + * + * Accumulated synchronously as each event arrives rather than derived from + * the SDK's session-wide total: that total is read asynchronously, and the + * terminal `session.idle` can close the turn while a read is in flight, + * which would drop the turn's last model call from its reported cost. */ - readonly copilotUsageTotalNanoAiuByScope = new Map(); + copilotNanoAiu = 0; + + /** + * Per-subagent component cost, in nano-AIU, keyed by `parentToolCallId`. + * The SDK's session metrics are session-wide and carry no per-agent + * breakdown, so a subagent's own running total is still accumulated from + * its usage events in order to report it on the subagent's child session. + */ + readonly subagentNanoAiuByToolCallId = new Map(); /** * The parent (main-agent) turn's own last context usage — model plus token - * counts and per-event cost. Subagent usage events are folded into the - * parent aggregate for credit purposes only, so they must not overwrite the - * parent turn's model/context-token usage. Retaining the parent's own last - * values lets each subagent usage event refresh the parent aggregate's - * credit total while preserving the model that produced the parent response. + * counts and per-event cost. A subagent's model call contributes to the + * turn's credits (the SDK's session metrics already include it) but must not + * overwrite the parent turn's model/context-token usage. Retaining the + * parent's own last values lets each subagent usage event refresh the parent + * aggregate's credit total while preserving the model that produced the + * parent response. */ parentContextUsage: UsageContext | undefined; @@ -679,15 +691,28 @@ export class CopilotAgentSession extends Disposable { */ private _lastSeenModelId: string | undefined; /** - * Compaction credits (nano-AIU) billed while no turn was active, carried - * forward onto the next turn. Automatic compaction can run outside a turn - * (e.g. after an abort, or between turns); the `chat/usage` reducer only - * applies usage to the *active* turn, so without this the cost — which is - * at its highest exactly here, since an out-of-turn compaction usually - * finds a cold prompt cache and pays the ~12x cache-write rate — would be - * dropped entirely. + * Latest session-wide nano-AIU total reported by the SDK's usage metrics + * (`rpc.usage.getMetrics`), which is authoritative for what the session as a + * whole has been billed: it folds in every model call plus compaction, + * covers work billed while no turn was active, and survives resume. + * + * Deliberately *not* used to derive per-turn cost. It is session-scoped and + * read asynchronously, so differencing it against a previous reading races + * turn boundaries — the SDK's terminal `session.idle` can close a turn while + * a read is still in flight. Per-turn cost comes from the synchronous + * per-event `copilotUsage` instead (see {@link CopilotTurn.copilotNanoAiu}). */ - private _carriedCompactionNanoAiu = 0; + private _sessionTotalNanoAiu = 0; + /** + * Serializes the metrics reads behind {@link _refreshSessionTotalNanoAiu}. Several + * handlers refresh the total, so without this their RPCs overlap and an older + * one resolving last would publish a session cost that visibly regresses. A + * high-water mark cannot be used to reject stale reads instead, because the + * total is legitimately non-monotonic (see the truncation note below). Keeping + * one read in flight makes out-of-order resolution impossible, and coalesces + * the redundant reads that a burst of usage events would otherwise issue. + */ + private readonly _sessionTotalRefreshThrottler = this._register(new Throttler()); /** SDK session wrapper, set by {@link initializeSession}. */ private _wrapper!: CopilotSessionWrapper; private readonly _slashCommandProvider: CopilotSlashCommandProvider; @@ -1044,24 +1069,68 @@ export class CopilotAgentSession extends Disposable { */ resetTurnState(turnId: string, senderClientId?: string, clientType = AgentHostClientType.Unknown): void { this._currentTurn = new CopilotTurn(turnId, this._nextTurnOrdinal++, senderClientId, clientType); - // Seed the parent scope with any compaction billed while no turn was active so the cost - // surfaces on this turn rather than being lost. The bank is deliberately NOT cleared here: - // a turn can end without ever reporting usage (it fails before the first SDK usage event, - // runs as a purely local slash command, or is replaced by another reset), and clearing on - // seed would drop the credits on the floor. It is cleared only once a report actually - // carries it — see {@link _onCarriedCompactionReported}. - if (this._carriedCompactionNanoAiu > 0) { - this._currentTurn.copilotUsageTotalNanoAiuByScope.set('', this._carriedCompactionNanoAiu); + } + + /** + * Re-reads the SDK's session-wide nano-AIU total. Returns `true` when the + * value changed, i.e. when a usage report is worth re-emitting. + * + * Reads are serialized by {@link _sessionTotalRefreshThrottler}, so the value + * always reflects the most recent one. The total is not monotonic — + * `history.truncate` (checkpoint restore, editing an earlier message) makes + * the SDK re-fold usage from the surviving events, so it legitimately drops — + * which is why a decrease is adopted rather than rejected as stale. + */ + private async _refreshSessionTotalNanoAiu(): Promise { + try { + return await this._sessionTotalRefreshThrottler.queue(async () => { + const total = (await this._wrapper.session.rpc.usage.getMetrics()).totalNanoAiu; + if (typeof total !== 'number' || !Number.isFinite(total) || total < 0 || total === this._sessionTotalNanoAiu) { + return false; + } + this._sessionTotalNanoAiu = total; + return true; + }); + } catch (err) { + // Also covers the rejection from a throttler disposed mid-read. + this._logService.trace(`[Copilot:${this.sessionId}] usage.getMetrics RPC failed: ${getErrorMessage(err)}`); + return false; } } /** - * Clears the out-of-turn compaction bank once a parent-scope usage report has carried it, so - * the credits are billed to exactly one turn. Called from every site that emits a parent-scope - * running total, since any of them can be the one that first reports the carry. + * The parent-scope Copilot billing metadata for the active turn: the turn's + * own accumulated cost plus the SDK's session-wide total. Absent until + * something has actually been billed. */ - private _onCarriedCompactionReported(): void { - this._carriedCompactionNanoAiu = 0; + private _parentCopilotUsageMeta(): UsageInfoMeta['copilotUsage'] | undefined { + const turnNanoAiu = this._currentTurn?.copilotNanoAiu ?? 0; + if (!turnNanoAiu && !this._sessionTotalNanoAiu) { + return undefined; + } + return { + ...(turnNanoAiu ? { totalNanoAiu: turnNanoAiu } : {}), + ...(this._sessionTotalNanoAiu ? { sessionTotalNanoAiu: this._sessionTotalNanoAiu } : {}), + }; + } + + /** Reads the SDK's per-source context-window attribution, or `undefined` when unavailable. */ + private async _readContextAttribution(): Promise { + let attribution: IContextAttributionData | undefined; + try { + attribution = (await this._wrapper.session.rpc.metadata.getContextAttribution())?.contextAttribution ?? undefined; + } catch (err) { + this._logService.trace(`[Copilot:${this.sessionId}] contextAttribution RPC failed: ${getErrorMessage(err)}`); + return undefined; + } + if (!attribution) { + this._logService.trace(`[Copilot:${this.sessionId}] contextAttribution: null/empty`); + return undefined; + } + if (this._logService.getLevel() <= LogLevel.Trace) { + this._logService.trace(`[Copilot:${this.sessionId}] contextAttribution: totalTokens=${attribution.totalTokens}, entries=${JSON.stringify(attribution.entries.map(e => ({ kind: e.kind, id: e.id, label: e.label, tokens: e.tokens, parentId: e.parentId })))}`); + } + return attribution; } private _completeActiveTurn(): void { @@ -1756,13 +1825,11 @@ export class CopilotAgentSession extends Disposable { // `_completeActiveTurn` since the reducer drops usage for a non-active turn. const usedTokens = result.contextWindow?.currentTokens; if (typeof usedTokens === 'number') { - // `session.compaction_complete` accumulates the summarization call's credits onto the - // turn before this RPC resolves; carry that running total through so the response - // footer reports the compaction's cost instead of dropping it. - const totalNanoAiu = this._currentTurn?.copilotUsageTotalNanoAiuByScope.get(''); - if (typeof totalNanoAiu === 'number') { - this._onCarriedCompactionReported(); - } + // `session.compaction_complete` has already folded the summarization call's + // cost into the turn by the time this RPC resolves; refresh the session total + // so the report carries both. + await this._refreshSessionTotalNanoAiu(); + const copilotUsage = this._parentCopilotUsageMeta(); this._emitAction({ type: ActionType.ChatUsage, turnId: this._turnId, @@ -1770,7 +1837,7 @@ export class CopilotAgentSession extends Disposable { inputTokens: usedTokens, outputTokens: 0, model: this._lastSeenModelId, - ...(typeof totalNanoAiu === 'number' ? { _meta: { copilotUsage: { totalNanoAiu } } } : {}), + ...(copilotUsage ? { _meta: { copilotUsage } } : {}), }, }); } @@ -3994,15 +4061,11 @@ export class CopilotAgentSession extends Disposable { this._register(wrapper.onUsage(e => { this._resumeSubagentForEvent(e); // Usage events for a subagent's model calls carry the subagent's - // `agentId`. Such an event is reported twice: - // 1. Folded into the parent turn (scope `''`) so the parent turn's - // reported cost stays the full turn aggregate (parent + every - // subagent), and - // 2. Emitted to the subagent's own child session (via - // `parentToolCallId`) carrying just that subagent's running - // component total, so the subagent tool can show its own cost. - // Main-agent (or unmapped subagent) events only contribute to the - // parent aggregate. + // `agentId`. Every model call — the parent's own and every subagent's — + // is folded into the turn's cost below, so such an event additionally + // needs only the subagent's own running component total emitted to its + // child session (via `parentToolCallId`) for the subagent tool to show + // its own cost. const parentToolCallId = this._parentToolCallIdForSubagentEvent(e); if (!parentToolCallId && !e.agentId && !e.data.parentToolCallId && e.data.model) { this._setPromptCacheState(e.data.cacheExpiresAt ? { modelId: e.data.model, cacheExpiresAt: e.data.cacheExpiresAt } : undefined); @@ -4035,27 +4098,19 @@ export class CopilotAgentSession extends Disposable { turn.parentContextUsage = eventContext; } - // Builds a usage object carrying the given context's tokens/model - // and the running credit total for the given scope. - const buildUsage = (scope: string, context: UsageContext): UsageInfo => { + // Builds a usage object carrying the given context's tokens/model plus + // the credit total for the given scope. `copilotUsage` is the scope's + // Copilot billing metadata, or `undefined` when nothing is billed yet. + const buildUsage = (context: UsageContext, scopedCopilotUsage: UsageInfoMeta['copilotUsage'], isParentScope: boolean): UsageInfo => { const metadata: UsageInfoMeta = {}; if (typeof context.cost === 'number') { metadata.cost = context.cost; } - if (scope === '' && autoModeResolved?.turnId === this._turnId) { + if (isParentScope && autoModeResolved?.turnId === this._turnId) { metadata.autoModeResolved = autoModeResolved.data; } - if (turn && typeof copilotUsage?.totalNanoAiu === 'number') { - const scopedTotal = (turn.copilotUsageTotalNanoAiuByScope.get(scope) ?? 0) + copilotUsage.totalNanoAiu; - turn.copilotUsageTotalNanoAiuByScope.set(scope, scopedTotal); - if (scope === '') { - // The parent-scope total includes any seeded out-of-turn compaction credits. - this._onCarriedCompactionReported(); - } - metadata.copilotUsage = { - ...copilotUsage, - totalNanoAiu: scopedTotal, - }; + if (scopedCopilotUsage) { + metadata.copilotUsage = scopedCopilotUsage; } if (quotaSnapshots) { metadata.quotaSnapshots = quotaSnapshots; @@ -4069,12 +4124,23 @@ export class CopilotAgentSession extends Disposable { }; }; - // Parent turn aggregate (scope `''`): every model call contributes - // its credits, but a subagent event must not replace the parent - // turn's own model/context-token usage. Fold subagent credits into - // the parent aggregate while preserving the parent's context. + // Fold this call's cost into the turn before building any report, so the + // emission below already carries it. Every model call the turn caused + // counts toward it, subagents included. Done synchronously here rather + // than from the SDK's session total, which is read across an await that + // the terminal `session.idle` can beat. + if (turn && copilotUsage) { + turn.copilotNanoAiu += copilotUsage.totalNanoAiu; + if (parentToolCallId) { + const scopedTotal = (turn.subagentNanoAiuByToolCallId.get(parentToolCallId) ?? 0) + copilotUsage.totalNanoAiu; + turn.subagentNanoAiuByToolCallId.set(parentToolCallId, scopedTotal); + } + } + + // Parent turn aggregate: a subagent event must not replace the parent + // turn's own model/context-token usage, so preserve the parent's context. const parentContext = parentToolCallId ? (turn?.parentContextUsage ?? {}) : eventContext; - const parentUsage = buildUsage('', parentContext); + const parentUsage = buildUsage(parentContext, this._parentCopilotUsageMeta(), true); lastParentUsage = parentUsage; lastParentUsageTurnId = this._turnId; this._emitAction({ @@ -4084,25 +4150,33 @@ export class CopilotAgentSession extends Disposable { }); // Subagent component: additionally report the subagent's own running - // total to its child session. + // total to its child session. The SDK's session metrics carry no + // per-agent breakdown, so this is the only source for it. if (parentToolCallId) { + const scopedTotal = turn?.subagentNanoAiuByToolCallId.get(parentToolCallId); + const subagentCopilotUsage = copilotUsage && scopedTotal !== undefined + ? { ...copilotUsage, totalNanoAiu: scopedTotal } + : undefined; this._emitAction({ type: ActionType.ChatUsage, turnId: this._turnId, - usage: buildUsage(parentToolCallId, eventContext), + usage: buildUsage(eventContext, subagentCopilotUsage, false), }, parentToolCallId); } })); - // After each usage event, asynchronously fetch the per-source context- - // window attribution from the SDK and re-emit the usage action enriched - // with the attribution data. The reducer replaces `activeTurn.usage` so - // the widget picks up the detailed breakdown on the next render cycle. + // After each usage event, asynchronously refresh the SDK's session-wide total + // (authoritative for the session, and the only source that sees work billed + // outside a turn) and re-emit the parent aggregate with it. For main-agent + // calls the per-source context-window attribution is fetched and merged in + // too — a subagent runs against its own context, so its events must not + // rewrite the parent's attribution. The reducer replaces `activeTurn.usage`, + // so the widget picks up the update on the next render cycle. + // + // Losing this re-emit to a turn that ended mid-flight costs only the session + // total's freshness; the turn's own cost was already reported synchronously. this._register(wrapper.onUsage(async e => { - // Only enrich the parent-turn aggregate (not subagent scopes). - if (this._parentToolCallIdForSubagentEvent(e)) { - return; - } + const isSubagentEvent = !!this._parentToolCallIdForSubagentEvent(e); const turnId = this._turnId; // Capture the base usage before the await boundary so concurrent // usage events don't overwrite what we merge into. @@ -4113,91 +4187,95 @@ export class CopilotAgentSession extends Disposable { model: e.data.model, cacheReadTokens: e.data.cacheReadTokens, }; - try { - const result = await this._wrapper.session.rpc.metadata.getContextAttribution(); - const attribution = result?.contextAttribution; - if (!attribution || !turnId) { - this._logService.trace(`[Copilot:${sessionId}] contextAttribution: null/empty (turnId=${turnId})`); - return; - } - // If the turn changed while we were awaiting, don't pollute the - // new turn's state with stale attribution data. - if (turnId !== this._turnId) { - return; - } - // Guard against a newer usage event having arrived while we - // were awaiting — only enrich if baseUsage is still current. - if (usage !== lastParentUsage || lastParentUsageTurnId !== turnId) { - return; - } - if (this._logService.getLevel() <= LogLevel.Trace) { - this._logService.trace(`[Copilot:${sessionId}] contextAttribution: totalTokens=${attribution.totalTokens}, entries=${JSON.stringify(attribution.entries.map(e => ({ kind: e.kind, id: e.id, label: e.label, tokens: e.tokens, parentId: e.parentId })))}`); - } - // Re-emit the usage action preserving the captured parent-scope - // usage (with accumulated credits) but adding the attribution. - const enriched: UsageInfo = { - ...usage, - _meta: { - ...(usage._meta ?? {}), - contextAttribution: attribution, - }, - }; - lastParentUsage = enriched; - lastParentUsageTurnId = turnId; - this._emitAction({ - type: ActionType.ChatUsage, - turnId, - usage: enriched, - }); - } catch (err) { - this._logService.trace(`[Copilot:${sessionId}] contextAttribution RPC failed: ${(err as Error)?.message ?? err}`); - } - })); - - // Compaction (manual `/compact` or automatic mid-turn) runs its own summarization model call. - // The SDK bills it separately and reports it on `session.compaction_complete` rather than as an - // `assistant.usage` event, so fold those credits into the turn's parent-scope running total the - // same way `buildUsage` does. This makes the turn's response footer include the compaction cost. - this._register(wrapper.onSessionCompactionComplete(e => { - if (e.agentId || e.data.success === false) { + await this._refreshSessionTotalNanoAiu(); + const attribution = isSubagentEvent ? undefined : await this._readContextAttribution(); + if (!turnId) { return; } - const turn = this._currentTurn; - const turnId = this._turnId; - const copilotUsage = readCopilotUsage(e.data.compactionTokensUsed); - if (!copilotUsage) { + // If the turn changed while we were awaiting, don't pollute the + // new turn's state with stale data. Likewise, guard against a newer + // usage event having arrived — only enrich if baseUsage is current. + if (turnId !== this._turnId || usage !== lastParentUsage || lastParentUsageTurnId !== turnId) { return; } - if (!turn || !turnId) { - // Compaction outside a turn: the reducer would discard usage for a non-active - // turn, so bank the credits for the next one instead of losing them. - this._carriedCompactionNanoAiu += copilotUsage.totalNanoAiu; + const copilotUsage = this._parentCopilotUsageMeta(); + if (!attribution && !copilotUsage) { return; } - const scopedTotal = (turn.copilotUsageTotalNanoAiuByScope.get('') ?? 0) + copilotUsage.totalNanoAiu; - turn.copilotUsageTotalNanoAiuByScope.set('', scopedTotal); - // This report carries any credits banked from an earlier out-of-turn compaction. - this._onCarriedCompactionReported(); - // Preserve the parent turn's own model/context tokens: the compaction call's tokens describe - // the summarization request, not the conversation, so they must not replace what is shown. - const base = lastParentUsageTurnId === turnId ? lastParentUsage : undefined; - const usage: UsageInfo = { - ...base, - model: base?.model ?? this._lastSeenModelId, + const enriched: UsageInfo = { + ...usage, _meta: { - ...(base?._meta ?? {}), - copilotUsage: { ...copilotUsage, totalNanoAiu: scopedTotal }, + ...(usage._meta ?? {}), + ...(copilotUsage ? { copilotUsage } : {}), + ...(attribution ? { contextAttribution: attribution } : {}), }, }; - lastParentUsage = usage; + lastParentUsage = enriched; lastParentUsageTurnId = turnId; this._emitAction({ type: ActionType.ChatUsage, turnId, - usage, + usage: enriched, }); })); + // Compaction (manual `/compact` or automatic) runs its own summarization model call, which the + // SDK bills on `session.compaction_complete` rather than as an `assistant.usage` event. + // + // A compaction that runs *during* a turn is that turn's cost, so fold it in like any other + // call. One that runs between turns belongs to no turn: it is reflected in the session total + // only, rather than being carried onto whatever runs next and inflating an unrelated + // response footer by what is often the session's single most expensive call. + this._register(wrapper.onSessionCompactionComplete(async e => { + if (e.agentId || e.data.success === false) { + return; + } + const copilotUsage = readCopilotUsage(e.data.compactionTokensUsed); + // Report the turn's cost before awaiting anything. The terminal `session.idle` + // can arrive while the metrics read is in flight and close the turn, after + // which the reducer drops usage for it — so a compaction whose turn ends + // immediately (e.g. one followed by a failing model call) would never be + // persisted if this waited. + const emitParentUsage = (): string | undefined => { + const turnId = this._turnId; + const parentCopilotUsage = this._parentCopilotUsageMeta(); + if (!turnId || !parentCopilotUsage) { + return undefined; + } + // Preserve the parent turn's own model/context tokens: the compaction call's tokens describe + // the summarization request, not the conversation, so they must not replace what is shown. + const base = lastParentUsageTurnId === turnId ? lastParentUsage : undefined; + const usage: UsageInfo = { + ...base, + model: base?.model ?? this._lastSeenModelId, + _meta: { + ...(base?._meta ?? {}), + copilotUsage: parentCopilotUsage, + }, + }; + lastParentUsage = usage; + lastParentUsageTurnId = turnId; + this._emitAction({ + type: ActionType.ChatUsage, + turnId, + usage, + }); + return turnId; + }; + + const turn = this._currentTurn; + if (turn && copilotUsage) { + turn.copilotNanoAiu += copilotUsage.totalNanoAiu; + emitParentUsage(); + } + // Then pick up the session-wide total, which also covers a compaction billed + // while no turn was active, and re-emit so the widget reflects it. + const turnIdBeforeRefresh = this._turnId; + if (await this._refreshSessionTotalNanoAiu() && turnIdBeforeRefresh === this._turnId) { + emitParentUsage(); + } + })); + this._register(wrapper.onReasoningDelta(e => { this._logService.trace(`[Copilot:${sessionId}] Reasoning delta: ${e.data.deltaContent.length} chars`); this._resumeSubagentForEvent(e); @@ -5029,10 +5107,13 @@ function countUnifiedDiffLines(diff: string): { added: number; removed: number } } /** - * Reads the SDK's internal `copilotUsage` billing payload. It is marked `asInternal` in the SDK - * schema, so it is absent from the generated event types (`AssistantUsageData`, + * Reads the SDK's internal `copilotUsage` billing payload, carried on both the `assistant.usage` + * event and `session.compaction_complete`'s `compactionTokensUsed`. It is marked `asInternal` in + * the SDK schema, so it is absent from the generated types (`AssistantUsageData`, * `CompactionCompleteCompactionTokensUsed`) even though it is present at runtime — hence the - * dynamic read. Returns `undefined` when the payload carries no usable nano-AIU total. + * dynamic read. This is the source for per-turn and per-subagent cost, accumulated synchronously + * as each event arrives; only the session-wide total comes from the SDK's usage metrics. + * Returns `undefined` when the payload carries no usable nano-AIU total. */ function readCopilotUsage(raw: unknown): { totalNanoAiu: number } & Record | undefined { if (!raw || typeof raw !== 'object') { diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index 944735eed85..a7212ba4e20 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -100,6 +100,7 @@ class MockCopilotSession { totalPremiumRequestCost: 0, totalUserRequests: 0, totalApiDurationMs: 0, + totalNanoAiu: 0, sessionStartTime: new Date().toISOString(), codeChanges: { linesAdded: 0, linesRemoved: 0, filesModifiedCount: 0, filesModified: [] }, modelMetrics: {}, @@ -107,6 +108,16 @@ class MockCopilotSession { lastCallInputTokens: 0, lastCallOutputTokens: 0, }; + /** Rejects the next `usage.getMetrics` call, then clears itself. */ + usageMetricsError: unknown = undefined; + usageMetricsCalls = 0; + /** Awaited inside `usage.getMetrics` so tests can hold a refresh in flight. */ + usageMetricsGate: Promise | undefined; + /** + * Per-call gates, consumed in call order, for holding individual reads in flight. + * Lets a test make an earlier-issued read resolve after a later one. + */ + readonly usageMetricsGates: Array> = []; private readonly _handlers = new Map void>>(); private readonly _allHandlers = new Set(); @@ -139,6 +150,7 @@ class MockCopilotSession { /** Push an event through to all registered handlers of the given type. */ fire(type: K, data: SessionEventPayload['data'], overrides?: Partial, 'type' | 'data'>>): void { const event = { type, data, id: 'evt-1', timestamp: new Date().toISOString(), parentId: null, ...overrides } as SessionEventPayload; + this._accumulateUsageMetrics(type, data); const set = this._handlers.get(type); if (set) { for (const handler of set) { @@ -150,6 +162,23 @@ class MockCopilotSession { } } + /** + * Mirrors the SDK's own usage tracker, which folds the `copilotUsage` billed on + * `assistant.usage` (including sub-agent calls) and `session.compaction_complete` + * into the session-wide total that `usage.getMetrics` reports. + */ + private _accumulateUsageMetrics(type: SessionEventType, data: unknown): void { + const billed = type === 'assistant.usage' + ? data + : type === 'session.compaction_complete' + ? (data as { compactionTokensUsed?: unknown } | undefined)?.compactionTokensUsed + : undefined; + const totalNanoAiu = (billed as { copilotUsage?: { totalNanoAiu?: number } } | undefined)?.copilotUsage?.totalNanoAiu; + if (typeof totalNanoAiu === 'number') { + this.usageMetricsResult.totalNanoAiu += totalNanoAiu; + } + } + // Stubs for methods the wrapper / session class calls async send(request: unknown) { this.sendRequests.push(request); @@ -252,7 +281,19 @@ class MockCopilotSession { }, }, usage: { - getMetrics: async () => this.usageMetricsResult, + getMetrics: async () => { + this.usageMetricsCalls++; + if (this.usageMetricsError !== undefined) { + const err = this.usageMetricsError; + this.usageMetricsError = undefined; + throw err; + } + // Snapshot at call time, like a real RPC whose result reflects the state + // when the request was served rather than when the caller observes it. + const snapshot = { ...this.usageMetricsResult }; + await (this.usageMetricsGates.shift() ?? this.usageMetricsGate); + return snapshot; + }, }, }; @@ -1163,6 +1204,175 @@ suite('CopilotAgentSession', () => { assert.deepStrictEqual(usage?.usage, { inputTokens: 4500, outputTokens: 0, model: 'claude-sonnet-4.6' }); }); + test('a resumed session does not bill its restored history to the first new turn', async () => { + // The SDK re-folds usage from its durable event log on resume, so `getMetrics` + // opens at the accumulated total of everything already billed. + const { session, mockSession, signals } = await createAgentSession(disposables, { + configureMockSession: mock => { mock.usageMetricsResult.totalNanoAiu = 40_000_000_000; }, + }); + + session.resetTurnState('turn-after-resume'); + mockSession.fire('assistant.usage', { + model: 'claude-opus-4.6', + inputTokens: 10, + outputTokens: 20, + copilotUsage: { totalNanoAiu: 500_000_000 }, + } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); + + const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; + // The new turn bills only its own call, while the session total carries the history. + assert.deepStrictEqual(usageActions.at(-1)?.usage._meta?.copilotUsage, { + totalNanoAiu: 500_000_000, + sessionTotalNanoAiu: 40_500_000_000, + }); + }); + + test('a failed usage read leaves the turn cost intact', async () => { + // The turn's own cost comes from the events, so a metrics outage costs only + // the session total's freshness rather than the turn's reported cost. + const { session, mockSession, signals } = await createAgentSession(disposables, { + configureMockSession: mock => { + mock.usageMetricsResult.totalNanoAiu = 40_000_000_000; + mock.usageMetricsError = new Error('rpc unavailable'); + }, + }); + + session.resetTurnState('turn-with-failed-read'); + mockSession.fire('assistant.usage', { + model: 'claude-opus-4.6', + inputTokens: 10, + outputTokens: 20, + copilotUsage: { totalNanoAiu: 500_000_000 }, + } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); + mockSession.fire('assistant.usage', { + model: 'claude-opus-4.6', + inputTokens: 10, + outputTokens: 20, + copilotUsage: { totalNanoAiu: 250_000_000 }, + } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); + + const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; + // Both calls counted toward the turn even though the first metrics read failed. + assert.deepStrictEqual(usageActions.at(-1)?.usage._meta?.copilotUsage, { + totalNanoAiu: 750_000_000, + sessionTotalNanoAiu: 40_750_000_000, + }); + }); + + test('a session total that drops after truncation is adopted rather than treated as stale', async () => { + // `history.truncate` (checkpoint restore, editing an earlier message) makes the + // SDK re-fold usage from the surviving events, so its authoritative total + // legitimately decreases. Treating that as a stale read would freeze the + // reported cost until billing climbed back past the pre-truncation figure. + const { session, mockSession, signals } = await createAgentSession(disposables); + + session.resetTurnState('turn-before-truncate'); + mockSession.fire('assistant.usage', { + model: 'claude-opus-4.6', + inputTokens: 10, + outputTokens: 20, + copilotUsage: { totalNanoAiu: 10_000_000_000 }, + } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); + + // Truncation rewinds the SDK's total from 10 down to 3; the next call brings it + // to 4. A high-water guard would reject everything below 10 and freeze the + // reported cost, so the drop must be adopted. + mockSession.usageMetricsResult.totalNanoAiu = 3_000_000_000; + session.resetTurnState('turn-after-truncate'); + mockSession.fire('assistant.usage', { + model: 'claude-opus-4.6', + inputTokens: 10, + outputTokens: 20, + copilotUsage: { totalNanoAiu: 1_000_000_000 }, + } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); + + const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; + assert.deepStrictEqual(usageActions.at(-1)?.usage._meta?.copilotUsage, { + totalNanoAiu: 1_000_000_000, + sessionTotalNanoAiu: 4_000_000_000, + }); + }); + + test('overlapping usage events issue one metrics read at a time and converge on the newest', async () => { + // `getMetrics` is a real RPC round trip. Letting several overlap means an older + // one can resolve last and publish a stale session cost, and a high-water guard + // can't reject it because the total legitimately drops after a truncation. + // Serializing the reads removes the interleaving entirely and coalesces the + // redundant reads a burst of usage events would otherwise issue. + const { session, mockSession, signals } = await createAgentSession(disposables); + + session.resetTurnState('turn-overlapping'); + const slowFirstRead = new DeferredPromise(); + mockSession.usageMetricsGates.push(slowFirstRead.p); + + const fireUsage = (totalNanoAiu: number) => mockSession.fire('assistant.usage', { + model: 'claude-opus-4.6', + inputTokens: 10, + outputTokens: 20, + copilotUsage: { totalNanoAiu }, + } as unknown as SessionEventPayload<'assistant.usage'>['data']); + + fireUsage(500_000_000); + await timeout(0); + fireUsage(1_000_000_000); + await timeout(0); + fireUsage(500_000_000); + await timeout(0); + // The first read is still in flight, so the two later events have not started + // their own — they are waiting behind it and collapse into a single follow-up. + assert.strictEqual(mockSession.usageMetricsCalls, 1); + + slowFirstRead.complete(); + for (let i = 0; i < 5; i++) { + await timeout(0); + } + + // One follow-up read for the three events that queued behind the first, and it + // observed the newest total rather than any earlier snapshot. + assert.strictEqual(mockSession.usageMetricsCalls, 2); + session.resetTurnState('turn-after-overlap'); + fireUsage(250_000_000); + for (let i = 0; i < 5; i++) { + await timeout(0); + } + const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; + assert.strictEqual( + (usageActions.at(-1)?.usage._meta as UsageInfoMeta | undefined)?.copilotUsage?.sessionTotalNanoAiu, + 2_250_000_000, + ); + }); + + test('a turn ending while its usage refresh is in flight still bills that turn', async () => { + const { session, mockSession, signals } = await createAgentSession(disposables); + + session.resetTurnState('turn-racing-idle'); + const gate = new DeferredPromise(); + mockSession.usageMetricsGate = gate.p; + mockSession.fire('assistant.usage', { + model: 'claude-opus-4.6', + inputTokens: 10, + outputTokens: 20, + copilotUsage: { totalNanoAiu: 500_000_000 }, + } as unknown as SessionEventPayload<'assistant.usage'>['data']); + // The SDK's terminal `session.idle` lands before the metrics RPC resolves — the + // common case, since idle follows a turn's last usage event almost immediately. + mockSession.fire('session.idle', { aborted: false } as SessionEventPayload<'session.idle'>['data']); + gate.complete(); + await timeout(0); + + const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; + // The cost belongs to the turn that incurred it, so it must survive the turn + // ending mid-refresh. The session total may lag here — the re-emit carrying it + // is dropped once the turn is no longer active — which `ChatModel.sessionCost` + // absorbs by taking the larger of the reported total and the summed turns. + assert.strictEqual((usageActions.at(-1)?.usage._meta as UsageInfoMeta | undefined)?.copilotUsage?.totalNanoAiu, 500_000_000); + }); + test('`/compact` reports the compaction call credits on the post-compaction usage', async () => { const { session, mockSession, signals } = await createAgentSession(disposables); mockSession.compactResult = { success: true, tokensRemoved: 1200, messagesRemoved: 3, contextWindow: { currentTokens: 4500, tokenLimit: 128000, messagesLength: 7 } }; @@ -1176,18 +1386,19 @@ suite('CopilotAgentSession', () => { } as unknown as SessionEventPayload<'session.compaction_complete'>['data']); await session.send('/compact', undefined, 'turn-compact'); + await timeout(0); const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; - assert.deepStrictEqual(usageActions.map(a => ({ turnId: a.turnId, usage: a.usage })), [ - { - turnId: 'turn-compact', - usage: { model: undefined, _meta: { copilotUsage: { totalNanoAiu: 250_000_000 } } }, + assert.deepStrictEqual(usageActions.at(-1), { + type: ActionType.ChatUsage, + turnId: 'turn-compact', + usage: { + inputTokens: 4500, + outputTokens: 0, + model: undefined, + _meta: { copilotUsage: { totalNanoAiu: 250_000_000, sessionTotalNanoAiu: 250_000_000 } }, }, - { - turnId: 'turn-compact', - usage: { inputTokens: 4500, outputTokens: 0, model: undefined, _meta: { copilotUsage: { totalNanoAiu: 250_000_000 } } }, - }, - ]); + }); }); test('automatic compaction folds its credits into the turn running total', async () => { @@ -1205,6 +1416,7 @@ suite('CopilotAgentSession', () => { tokensRemoved: 1200, compactionTokensUsed: { model: 'claude-sonnet-4.6', copilotUsage: { totalNanoAiu: 250_000_000 } }, } as unknown as SessionEventPayload<'session.compaction_complete'>['data']); + await timeout(0); const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; // The compaction credits add to the turn total while the parent turn's own model and @@ -1214,7 +1426,7 @@ suite('CopilotAgentSession', () => { outputTokens: 20, model: 'claude-sonnet-4.6', cacheReadTokens: undefined, - _meta: { copilotUsage: { totalNanoAiu: 750_000_000 } }, + _meta: { copilotUsage: { totalNanoAiu: 750_000_000, sessionTotalNanoAiu: 750_000_000 } }, }); }); @@ -1227,19 +1439,23 @@ suite('CopilotAgentSession', () => { error: 'boom', compactionTokensUsed: { copilotUsage: { totalNanoAiu: 250_000_000 } }, } as unknown as SessionEventPayload<'session.compaction_complete'>['data']); + await timeout(0); assert.deepStrictEqual(getActions(signals).filter(a => a.type === ActionType.ChatUsage), []); }); - test('compaction billed outside a turn is carried onto the next turn', async () => { + test('compaction billed outside a turn shows in the session total, not on the next turn', async () => { const { session, mockSession, signals } = await createAgentSession(disposables); - // Automatic compaction can run with no turn active (e.g. after an abort). The reducer only - // applies usage to the active turn, so the credits must be banked rather than dropped. + // Automatic compaction can run with no turn active (e.g. after an abort). It is + // nobody's turn cost — and an out-of-turn compaction usually finds a cold prompt + // cache and pays the ~12x cache-write rate, so billing it to an unrelated next + // turn would dominate that turn's footer. mockSession.fire('session.compaction_complete', { success: true, compactionTokensUsed: { model: 'claude-opus-4.6', copilotUsage: { totalNanoAiu: 133_468_375_000 } }, } as unknown as SessionEventPayload<'session.compaction_complete'>['data']); + await timeout(0); assert.deepStrictEqual(getActions(signals).filter(a => a.type === ActionType.ChatUsage), []); session.resetTurnState('turn-after-compact'); @@ -1249,6 +1465,7 @@ suite('CopilotAgentSession', () => { outputTokens: 20, copilotUsage: { totalNanoAiu: 500_000_000 }, } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; assert.deepStrictEqual(usageActions.at(-1)?.usage, { @@ -1256,21 +1473,20 @@ suite('CopilotAgentSession', () => { outputTokens: 20, model: 'claude-opus-4.6', cacheReadTokens: undefined, - _meta: { copilotUsage: { totalNanoAiu: 133_968_375_000 } }, + // The turn bills only its own call; the compaction is visible in the session total. + _meta: { copilotUsage: { totalNanoAiu: 500_000_000, sessionTotalNanoAiu: 133_968_375_000 } }, }); }); - test('carried compaction credits survive a turn that never reports usage', async () => { + test('a turn that never reports usage does not inherit out-of-turn compaction cost', async () => { const { session, mockSession, signals } = await createAgentSession(disposables); mockSession.fire('session.compaction_complete', { success: true, compactionTokensUsed: { copilotUsage: { totalNanoAiu: 2_000_000_000 } }, } as unknown as SessionEventPayload<'session.compaction_complete'>['data']); + await timeout(0); - // `turn-1` is started and then replaced without ever reporting usage — the same shape as a - // turn that fails before its first SDK usage event or runs as a purely local slash command. - // The credits must roll forward rather than dying with it. session.resetTurnState('turn-1'); session.resetTurnState('turn-2'); mockSession.fire('assistant.usage', { @@ -1279,18 +1495,22 @@ suite('CopilotAgentSession', () => { outputTokens: 1, copilotUsage: { totalNanoAiu: 1_000_000_000 }, } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; - assert.deepStrictEqual(usageActions.at(-1)?.usage._meta, { copilotUsage: { totalNanoAiu: 3_000_000_000 } }); + assert.deepStrictEqual(usageActions.at(-1)?.usage._meta, { + copilotUsage: { totalNanoAiu: 1_000_000_000, sessionTotalNanoAiu: 3_000_000_000 }, + }); }); - test('carried compaction credits are billed to only one turn once reported', async () => { + test('each turn bills only its own calls while the session total accumulates', async () => { const { session, mockSession, signals } = await createAgentSession(disposables); mockSession.fire('session.compaction_complete', { success: true, compactionTokensUsed: { copilotUsage: { totalNanoAiu: 2_000_000_000 } }, } as unknown as SessionEventPayload<'session.compaction_complete'>['data']); + await timeout(0); session.resetTurnState('turn-1'); mockSession.fire('assistant.usage', { @@ -1299,6 +1519,7 @@ suite('CopilotAgentSession', () => { outputTokens: 1, copilotUsage: { totalNanoAiu: 1_000_000_000 }, } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); session.resetTurnState('turn-2'); mockSession.fire('assistant.usage', { model: 'claude-opus-4.6', @@ -1306,10 +1527,17 @@ suite('CopilotAgentSession', () => { outputTokens: 1, copilotUsage: { totalNanoAiu: 1_000_000_000 }, } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); const usageActions = getActions(signals).filter(a => a.type === ActionType.ChatUsage) as ChatUsageAction[]; - // `turn-1` reported the carry, so `turn-2` bills only its own model call. - assert.deepStrictEqual(usageActions.map(a => a.usage._meta?.copilotUsage), [{ totalNanoAiu: 3_000_000_000 }, { totalNanoAiu: 1_000_000_000 }]); + // Each turn reports its own single call; the session total carries the + // out-of-turn compaction plus both turns. + assert.deepStrictEqual(usageActions.map(a => ({ turnId: a.turnId, copilotUsage: a.usage._meta?.copilotUsage })), [ + { turnId: 'turn-1', copilotUsage: { totalNanoAiu: 1_000_000_000, sessionTotalNanoAiu: 2_000_000_000 } }, + { turnId: 'turn-1', copilotUsage: { totalNanoAiu: 1_000_000_000, sessionTotalNanoAiu: 3_000_000_000 } }, + { turnId: 'turn-2', copilotUsage: { totalNanoAiu: 1_000_000_000, sessionTotalNanoAiu: 3_000_000_000 } }, + { turnId: 'turn-2', copilotUsage: { totalNanoAiu: 1_000_000_000, sessionTotalNanoAiu: 4_000_000_000 } }, + ]); }); test('`/compact` completes the turn even when compaction reports failure', async () => { @@ -1632,6 +1860,7 @@ suite('CopilotAgentSession', () => { // `copilotUsage` is marked `asInternal` in the SDK schema so it is not on the public type, but is present at runtime. copilotUsage: { totalNanoAiu: 500_000_000, tokenDetails: [] }, } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); mockSession.fire('assistant.usage', { model: 'claude-sonnet-4.6', inputTokens: 30, @@ -1639,34 +1868,25 @@ suite('CopilotAgentSession', () => { cost: 2, copilotUsage: { totalNanoAiu: 750_000_000, tokenDetails: [] }, } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); const usageActions = signals .filter((s): s is IAgentActionSignal => s.kind === 'action') .map(s => s.action) .filter(a => a.type === ActionType.ChatUsage); - assert.deepStrictEqual(usageActions.map(a => a.usage), [ - { - inputTokens: 10, - outputTokens: 20, - model: 'claude-sonnet-4.6', - cacheReadTokens: 5, - _meta: { - cost: 2, - copilotUsage: { totalNanoAiu: 500_000_000, tokenDetails: [] }, - }, + // The turn's running total and the session total both come from the SDK's usage + // metrics, so they are reported on the enrichment re-emit that follows each event. + assert.deepStrictEqual(usageActions.at(-1)?.usage, { + inputTokens: 30, + outputTokens: 40, + model: 'claude-sonnet-4.6', + cacheReadTokens: undefined, + _meta: { + cost: 2, + copilotUsage: { totalNanoAiu: 1_250_000_000, sessionTotalNanoAiu: 1_250_000_000 }, }, - { - inputTokens: 30, - outputTokens: 40, - model: 'claude-sonnet-4.6', - cacheReadTokens: undefined, - _meta: { - cost: 2, - copilotUsage: { totalNanoAiu: 1_250_000_000, tokenDetails: [] }, - }, - }, - ]); + }); }); test('updates prompt cache expiration from main-agent usage only', async () => { @@ -1791,15 +2011,17 @@ suite('CopilotAgentSession', () => { outputTokens: 20, copilotUsage: { totalNanoAiu: 500_000_000, tokenDetails: [] }, } as unknown as SessionEventPayload<'assistant.usage'>['data']); + await timeout(0); - // Subagent usage (its agentId) is reported twice: folded into the parent - // aggregate AND emitted to the subagent's child session as its component. + // Subagent usage (its agentId) is emitted to the subagent's child session as + // its own component; the parent aggregate grows via the SDK's session metrics. mockSession.fire('assistant.usage', { model: 'gpt-5.5', inputTokens: 5, outputTokens: 7, copilotUsage: { totalNanoAiu: 200_000_000, tokenDetails: [] }, } as unknown as SessionEventPayload<'assistant.usage'>['data'], { agentId: 'agent-1' }); + await timeout(0); mockSession.fire('assistant.usage', { model: 'gpt-5.5', @@ -1807,6 +2029,7 @@ suite('CopilotAgentSession', () => { outputTokens: 8, copilotUsage: { totalNanoAiu: 300_000_000, tokenDetails: [] }, } as unknown as SessionEventPayload<'assistant.usage'>['data'], { agentId: 'agent-1' }); + await timeout(0); const usageSignals = signals.flatMap(signal => { if (signal.kind !== 'action' || signal.action.type !== ActionType.ChatUsage) { @@ -1821,17 +2044,18 @@ suite('CopilotAgentSession', () => { }]; }); + // The parent aggregate always keeps the parent's own model/context tokens, and + // its credits cover every call the turn caused (its own plus every subagent's). + // They land on the synchronous emit, so a turn ending mid-refresh cannot lose them. assert.deepStrictEqual(usageSignals, [ - // Parent-only call → parent aggregate. { parentToolCallId: undefined, model: 'claude-opus-4.8', inputTokens: 10, outputTokens: 20, totalNanoAiu: 500_000_000 }, - // First subagent call → parent aggregate grows but keeps the parent - // model/context, plus the subagent component carries the child model. + { parentToolCallId: undefined, model: 'claude-opus-4.8', inputTokens: 10, outputTokens: 20, totalNanoAiu: 500_000_000 }, { parentToolCallId: undefined, model: 'claude-opus-4.8', inputTokens: 10, outputTokens: 20, totalNanoAiu: 700_000_000 }, { parentToolCallId: 'tc-subagent', model: 'gpt-5.5', inputTokens: 5, outputTokens: 7, totalNanoAiu: 200_000_000 }, - // Second subagent call → parent aggregate grows but keeps the parent - // model/context, plus the subagent component. + { parentToolCallId: undefined, model: 'claude-opus-4.8', inputTokens: 10, outputTokens: 20, totalNanoAiu: 700_000_000 }, { parentToolCallId: undefined, model: 'claude-opus-4.8', inputTokens: 10, outputTokens: 20, totalNanoAiu: 1_000_000_000 }, { parentToolCallId: 'tc-subagent', model: 'gpt-5.5', inputTokens: 6, outputTokens: 8, totalNanoAiu: 500_000_000 }, + { parentToolCallId: undefined, model: 'claude-opus-4.8', inputTokens: 10, outputTokens: 20, totalNanoAiu: 1_000_000_000 }, ]); }); 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 d37bb3ae266..23c4ed76b87 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts @@ -2367,6 +2367,10 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC && lastUsage.completionTokens === usage.completionTokens && lastUsage.outputBuffer === usage.outputBuffer && lastUsage.copilotCredits === usage.copilotCredits + // The session total moves independently of this turn's own cost — + // it also covers work billed while no turn was active — so it has + // to be compared, or a session-cost update would be dropped here. + && lastUsage.sessionCopilotCredits === usage.sessionCopilotCredits && equals(lastUsage.promptTokenDetails, usage.promptTokenDetails)) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts index 523dea21220..7835cf65852 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts @@ -551,10 +551,18 @@ export function usageInfoToChatUsage(usage: UsageInfo | undefined): IChatUsage | promptTokens: usage?.inputTokens ?? 0, completionTokens: usage?.outputTokens ?? 0, copilotCredits: getCopilotCredits(usage), + sessionCopilotCredits: getSessionCopilotCredits(usage), promptTokenDetails: contextAttributionToPromptTokenDetails(usage), }; } +function getSessionCopilotCredits(usage: UsageInfo | undefined): number | undefined { + const sessionTotalNanoAiu = readUsageInfoMeta(usage).copilotUsage?.sessionTotalNanoAiu; + return typeof sessionTotalNanoAiu === 'number' && sessionTotalNanoAiu >= 0 + ? sessionTotalNanoAiu / 1_000_000_000 + : undefined; +} + function getCopilotCredits(usage: UsageInfo | undefined): number | undefined { const meta = readUsageInfoMeta(usage); const totalNanoAiu = meta?.copilotUsage?.totalNanoAiu; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 8915272323f..f80feea2b3a 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -165,7 +165,21 @@ export interface IChatUsage { completionTokens: number; outputBuffer?: number; promptTokenDetails?: readonly IChatUsagePromptTokenDetail[]; + /** + * The Copilot credit cost of whatever this usage describes — a turn's own + * cost on a response's usage, a sub-agent's component cost on a sub-agent's. + * Scoped to its container, so summing it across responses is only ever a + * lower bound on the session; prefer {@link sessionCopilotCredits} for that. + */ copilotCredits?: number; + /** + * The whole session's Copilot credit cost as reported by the backend, rather + * than summed from the individual turns. Unlike {@link copilotCredits} this + * deliberately describes more than its container: it is authoritative when + * present, and covers work billed outside any turn (e.g. a compaction that + * ran between turns). Not every backend reports it. + */ + sessionCopilotCredits?: number; /** * The language-model ID that actually served the request. Set when a * meta-model (e.g. "auto") routes to a concrete model so consumers diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index d14742f9488..5ea9db6d3a8 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -1539,12 +1539,27 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } private _setUsage(usage: IChatUsage, countCompletionTokens: boolean): void { - if (this.isSameUsage(usage)) { + const currentUsage = this._usageObs.get(); + if (currentUsage && this.isSameUsage(currentUsage, usage)) { return; } + // Only a report describing a *different* model call adds to the running + // completion-token total. A backend can re-report one call several times as + // slower-arriving detail resolves — the agent host re-emits with the context + // attribution and the session cost once its RPCs return — and those + // refinements must update the stored usage without being counted again. + // + // Two consecutive calls reporting identical tokens are indistinguishable here + // and the second is treated as a refinement. That is pre-existing: the + // `isSameUsage` guard already discarded such a report wholesale. + const isNewCall = !currentUsage + || currentUsage.promptTokens !== usage.promptTokens + || currentUsage.completionTokens !== usage.completionTokens + || currentUsage.outputBuffer !== usage.outputBuffer; + this._usageObs.set(usage, undefined); - if (countCompletionTokens) { + if (countCompletionTokens && isNewCall) { const previousCompletionTokens = this._completionTokenCountObs.get() ?? 0; this._completionTokenCountObs.set(previousCompletionTokens + usage.completionTokens, undefined); } @@ -1555,13 +1570,12 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._elapsedMs = Math.max(0, elapsedMs); } - private isSameUsage(usage: IChatUsage): boolean { - const currentUsage = this._usageObs.get(); - return !!currentUsage - && currentUsage.promptTokens === usage.promptTokens + private isSameUsage(currentUsage: IChatUsage, usage: IChatUsage): boolean { + return currentUsage.promptTokens === usage.promptTokens && currentUsage.completionTokens === usage.completionTokens && currentUsage.outputBuffer === usage.outputBuffer && currentUsage.copilotCredits === usage.copilotCredits + && currentUsage.sessionCopilotCredits === usage.sessionCopilotCredits && equals(currentUsage.promptTokenDetails, usage.promptTokenDetails); } @@ -1680,6 +1694,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel outputBuffer: this.usage?.outputBuffer, promptTokenDetails: this.usage?.promptTokenDetails, copilotCredits: this.usage?.copilotCredits, + sessionCopilotCredits: this.usage?.sessionCopilotCredits, elapsedMs: this.elapsedMs ?? (this.completedAt ? Math.max(0, this.completedAt - this.confirmationAdjustedTimestamp.get()) : undefined), } satisfies WithDefinedProps>; } @@ -1783,6 +1798,7 @@ interface ISerializableChatResponseData { outputBuffer?: number; promptTokenDetails?: readonly IChatUsagePromptTokenDetail[]; copilotCredits?: number; + sessionCopilotCredits?: number; elapsedMs?: number; } @@ -2488,14 +2504,23 @@ export class ChatModel extends Disposable implements IChatModel { } get sessionCost(): number { - let totalCredits = 0; + let summedCredits = 0; + let reportedSessionCredits = 0; for (const request of this._requests) { - const credits = request.response?.usage?.copilotCredits; - if (typeof credits === 'number') { - totalCredits += credits; + const usage = request.response?.usage; + if (typeof usage?.copilotCredits === 'number') { + summedCredits += usage.copilotCredits; + } + if (typeof usage?.sessionCopilotCredits === 'number') { + reportedSessionCredits = Math.max(reportedSessionCredits, usage.sessionCopilotCredits); } } - return totalCredits; + // A backend that reports the session total covers work billed outside any + // turn, which summing the turns would miss. Summing covers turns whose + // backend reports no session total, and any billed after the most recent + // reported total. Neither is a superset, so take whichever is larger — + // which is also independent of the order the two kinds are interleaved in. + return Math.max(summedCredits, reportedSessionCredits); } private _timestamp: number; @@ -2821,7 +2846,7 @@ export class ChatModel extends Disposable implements IChatModel { codeBlockInfos: raw.responseMarkdownInfo?.map(info => ({ suggestionId: info.suggestionId })), }); request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; - if (typeof raw.completionTokens === 'number' || typeof raw.promptTokens === 'number' || typeof raw.copilotCredits === 'number') { + if (typeof raw.completionTokens === 'number' || typeof raw.promptTokens === 'number' || typeof raw.copilotCredits === 'number' || typeof raw.sessionCopilotCredits === 'number') { request.response.setUsage({ kind: 'usage', promptTokens: raw.promptTokens ?? 0, @@ -2829,6 +2854,7 @@ export class ChatModel extends Disposable implements IChatModel { outputBuffer: raw.outputBuffer, promptTokenDetails: raw.promptTokenDetails, copilotCredits: raw.copilotCredits, + sessionCopilotCredits: raw.sessionCopilotCredits, }); } if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts index f9d24949f4f..dd91d5d428d 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts @@ -160,6 +160,7 @@ const requestSchema = Adapt.object m.response?.usage?.outputBuffer), promptTokenDetails: Adapt.v(m => m.response?.usage?.promptTokenDetails, objectsEqual), copilotCredits: Adapt.v(m => m.response?.usage?.copilotCredits), + sessionCopilotCredits: Adapt.v(m => m.response?.usage?.sessionCopilotCredits), elapsedMs: Adapt.v(m => m.response?.elapsedMs ?? (m.response?.completedAt ? Math.max(0, m.response.completedAt - m.response.confirmationAdjustedTimestamp.get()) : undefined)), modeInfo: Adapt.v(m => m.modeInfo, objectsEqual), isSystemInitiated: Adapt.v(m => m.isSystemInitiated), diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap index c3be6013e60..f4afd2f8f25 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap @@ -86,6 +86,7 @@ completedAt: undefined }, vote: undefined, + sessionCopilotCredits: undefined, voteDownReason: undefined, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap index efc87326930..9907cb36091 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -86,6 +86,7 @@ completedAt: undefined }, vote: undefined, + sessionCopilotCredits: undefined, voteDownReason: undefined, slashCommand: undefined, usedContext: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap index c7e7f976174..17129075b1c 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap @@ -95,6 +95,7 @@ completedAt: undefined }, vote: undefined, + sessionCopilotCredits: undefined, voteDownReason: undefined, slashCommand: undefined, usedContext: { @@ -175,6 +176,7 @@ completedAt: undefined }, vote: undefined, + sessionCopilotCredits: undefined, voteDownReason: undefined, slashCommand: undefined, usedContext: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap index f1d5d9a85c5..bb56aa4596d 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -88,6 +88,7 @@ completedAt: undefined }, vote: undefined, + sessionCopilotCredits: undefined, voteDownReason: undefined, slashCommand: undefined, usedContext: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts index d5b246a404e..07f01b361cb 100644 --- a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts @@ -211,6 +211,26 @@ suite('ChatModel', () => { }); }); + test('a refinement of the same model call updates usage without recounting its tokens', () => { + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); + const text = 'hello'; + const request = model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); + + // The agent host reports one model call several times as its context attribution + // and session cost resolve asynchronously. Those refinements must update the + // stored usage without adding the call's completion tokens again. + model.acceptResponseProgress(request, { kind: 'usage', promptTokens: 10, completionTokens: 2, copilotCredits: 1, sessionCopilotCredits: 1 }); + model.acceptResponseProgress(request, { kind: 'usage', promptTokens: 10, completionTokens: 2, copilotCredits: 1, sessionCopilotCredits: 5 }); + + assert.deepStrictEqual({ + sessionCopilotCredits: request.response?.usage?.sessionCopilotCredits, + completionTokenCount: request.response?.completionTokenCount, + }, { + sessionCopilotCredits: 5, + completionTokenCount: 2, + }); + }); + test('subagent credits are folded into parent response usage', () => { const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); const text = 'hello'; @@ -237,6 +257,33 @@ suite('ChatModel', () => { assert.strictEqual(restoredSeparateCosts.sessionCost, 11); }); + test('the session total and the summed turns each provide a floor for session cost', () => { + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); + const addRequest = (text: string) => model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); + + // A turn from a backend that reports no session total (e.g. Claude) still counts. + const first = addRequest('one'); + model.acceptResponseProgress(first, { kind: 'usage', promptTokens: 10, completionTokens: 2, copilotCredits: 2 }); + // The reported session total exceeds the summed turns because it also covers work + // billed outside any turn, such as a compaction that ran between them. + const second = addRequest('two'); + model.acceptResponseProgress(second, { kind: 'usage', promptTokens: 10, completionTokens: 2, copilotCredits: 3, sessionCopilotCredits: 9 }); + + assert.strictEqual(model.sessionCost, 9); + const restored = testDisposables.add(instantiationService.createInstance( + ChatModel, + { value: JSON.parse(JSON.stringify(model.toJSON())) as ISerializableChatData3, serializer: undefined! }, + { initialLocation: ChatAgentLocation.Chat, canUseTools: true } + )); + assert.strictEqual(restored.sessionCost, 9); + + // A later turn whose cost has not yet reached the reported total must not shrink + // the session cost, and the summed turns take over once they exceed it. + const third = addRequest('three'); + model.acceptResponseProgress(third, { kind: 'usage', promptTokens: 10, completionTokens: 2, copilotCredits: 6 }); + assert.strictEqual(model.sessionCost, 11); + }); + test('response details, elapsed time, and tokens roundtrip through serialization', () => { const completedAt = 1_752_012_405_000; const serializableData: ISerializableChatData3 = { From 609a4f1fb224cd2845be90a8b0a4feb603ac0e17 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 30 Jul 2026 14:34:48 -0400 Subject: [PATCH 19/86] Support offline dictation model install for registry-blocked networks (#328220) * Support offline dictation model install for registry-blocked networks Adds a 'Chat: Install Dictation Model from Local Package...' command that imports the official Foundry Local expansion pack (ZIP or extracted OCI layout) or a prepared model directory into the dictation model cache, so dictation works in environments where the Azure ML model registry is unreachable. When a model download fails with a network/registry error, the failure notification now offers an 'Install from Local Package...' action so the recovery is discoverable exactly when the user is blocked. Reuses the existing utility-process transcription service and base zip helper; no new dependencies. Fixes #328154 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate model identity/integrity and harden install swap Address review feedback: - Reject OCI packages whose embedded inference_model.json names a different model, so a wrong CPU expansion pack is not stamped as nemotron (with test). - Verify each OCI blob against its content digest (streaming, Node built-in crypto only) so a corrupt package fails at install rather than cryptically in the native loader (with test). - Never force-remove the backup during install cleanup; a failed swap rollback must not delete the only surviving copy of a working model. - Name the specific model in the install dialog and the recovery notification so users know which package to obtain offline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/base/node/zip.ts | 5 +- .../common/localTranscription.ts | 14 + .../node/foundryLocalModelImport.ts | 358 ++++++++++++++++++ .../node/localTranscriptionService.ts | 11 +- .../test/node/foundryLocalModelImport.test.ts | 204 ++++++++++ .../speechToText/chatSpeechToTextService.ts | 51 ++- .../actions/installDictationModelAction.ts | 90 +++++ .../electron-browser/chat.contribution.ts | 2 + .../browser/localTranscriptionService.ts | 4 + .../localTranscriptionService.ts | 1 + 10 files changed, 730 insertions(+), 10 deletions(-) create mode 100644 src/vs/platform/localTranscription/node/foundryLocalModelImport.ts create mode 100644 src/vs/platform/localTranscription/test/node/foundryLocalModelImport.test.ts create mode 100644 src/vs/workbench/contrib/chat/electron-browser/actions/installDictationModelAction.ts diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 8beee3266ca..7be49cbb75d 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -82,12 +82,13 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa let istream: WriteStream; - token.onCancellationRequested(() => { + const listener = token.onCancellationRequested(() => { istream?.destroy(); }); return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise((c, e) => { if (token.isCancellationRequested) { + c(); return; } @@ -100,7 +101,7 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa } catch (error) { e(error); } - })); + })).finally(() => listener.dispose()); } function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, token: CancellationToken): Promise { diff --git a/src/vs/platform/localTranscription/common/localTranscription.ts b/src/vs/platform/localTranscription/common/localTranscription.ts index 2b76ac0457c..d0a2423e3a0 100644 --- a/src/vs/platform/localTranscription/common/localTranscription.ts +++ b/src/vs/platform/localTranscription/common/localTranscription.ts @@ -12,6 +12,14 @@ export const ILocalTranscriptionService = createDecorator; + /** + * Imports the default on-device model from an official Foundry Local expansion + * pack or a prepared model directory into `cacheDir`. + */ + importModel(options: { readonly sourcePath: string; readonly cacheDir: string }): Promise; + /** * Ensure the model is downloaded/loaded (idempotent) and begin a new * transcription session. `cacheDir` is where model files are stored. `model` diff --git a/src/vs/platform/localTranscription/node/foundryLocalModelImport.ts b/src/vs/platform/localTranscription/node/foundryLocalModelImport.ts new file mode 100644 index 00000000000..b2fcdc9882b --- /dev/null +++ b/src/vs/platform/localTranscription/node/foundryLocalModelImport.ts @@ -0,0 +1,358 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createHash, randomUUID } from 'crypto'; +import { createReadStream, promises as fs } from 'fs'; +import { basename, dirname, join } from '../../../base/common/path.js'; +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { extract } from '../../../base/node/zip.js'; +import { DEFAULT_LOCAL_TRANSCRIPTION_MODEL, ILocalTranscriptionModelImportResult } from '../common/localTranscription.js'; + +const MODEL_PUBLISHER = 'Microsoft'; +const MODEL_VARIANT = `${DEFAULT_LOCAL_TRANSCRIPTION_MODEL}-generic-cpu`; +const INFERENCE_MODEL_FILE = 'inference_model.json'; +const GENAI_CONFIG_FILE = 'genai_config.json'; +const OCI_TITLE_ANNOTATION = 'org.opencontainers.image.title'; + +interface IOciDescriptor { + readonly digest: string; + readonly annotations?: Record; +} + +interface IOciIndex { + readonly manifests?: readonly IOciDescriptor[]; +} + +interface IOciManifest { + readonly layers?: readonly IOciDescriptor[]; +} + +interface IPreparedModel { + readonly version: number; + readonly versionDirectory: string; + readonly canMove: boolean; +} + +/** + * Imports the official Foundry Local expansion pack (ZIP or extracted OCI + * layout), or an already prepared model directory, into the model cache. + */ +export async function importFoundryLocalModel(sourcePath: string, cacheDir: string): Promise { + const sourceStat = await fs.stat(sourcePath); + await fs.mkdir(cacheDir, { recursive: true }); + const workDirectory = await fs.mkdtemp(join(cacheDir, '.dictation-model-import-')); + + try { + const prepared = sourceStat.isDirectory() + ? await prepareModelSource(sourcePath, workDirectory, false) + : await prepareModelArchive(sourcePath, workDirectory); + await verifyPreparedModel(prepared.versionDirectory); + await installPreparedModel(prepared, cacheDir); + return { model: DEFAULT_LOCAL_TRANSCRIPTION_MODEL, version: prepared.version }; + } finally { + await fs.rm(workDirectory, { recursive: true, force: true }); + } +} + +async function prepareModelArchive(sourcePath: string, workDirectory: string): Promise { + if (!sourcePath.toLowerCase().endsWith('.zip')) { + throw new Error('The selected dictation model package must be a ZIP archive or a folder.'); + } + + const extracted = join(workDirectory, 'archive'); + await extract(sourcePath, extracted, {}, CancellationToken.None); + return prepareModelSource(extracted, workDirectory, true); +} + +async function prepareModelSource(sourcePath: string, workDirectory: string, canMove: boolean): Promise { + const nestedPackage = (await findNamedFiles(sourcePath, 'Package.zip'))[0]; + if (nestedPackage) { + const extractedPackage = join(workDirectory, 'package'); + await extract(nestedPackage, extractedPackage, {}, CancellationToken.None); + if (canMove) { + await fs.rm(sourcePath, { recursive: true, force: true }); + } + sourcePath = extractedPackage; + canMove = true; + } + + const ociIndex = await findOciIndex(sourcePath); + if (ociIndex) { + return materializeOciModel(ociIndex, workDirectory, canMove); + } + + return findPreparedModel(sourcePath, canMove); +} + +async function findOciIndex(root: string): Promise { + for (const candidate of await findNamedFiles(root, 'index.json')) { + try { + const layout = JSON.parse(await fs.readFile(join(dirname(candidate), 'oci-layout'), 'utf8')) as { imageLayoutVersion?: string }; + if (layout.imageLayoutVersion) { + return candidate; + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } + } + return undefined; +} + +async function materializeOciModel(indexPath: string, workDirectory: string, canMove: boolean): Promise { + const ociRoot = dirname(indexPath); + const index = JSON.parse(await fs.readFile(indexPath, 'utf8')) as IOciIndex; + if (!index.manifests?.length) { + throw new Error('The selected dictation model package has no OCI manifest.'); + } + for (const descriptor of index.manifests) { + const manifest = JSON.parse((await readVerifiedOciBlob(ociRoot, descriptor.digest)).toString('utf8')) as IOciManifest; + const titledLayers = manifest.layers?.filter(layer => layer.annotations?.[OCI_TITLE_ANNOTATION]); + if (!titledLayers?.length) { + continue; + } + + let version: number | undefined; + const materializedRoot = join(workDirectory, 'materialized'); + for (const layer of titledLayers) { + const title = layer.annotations?.[OCI_TITLE_ANNOTATION]; + if (!title) { + continue; + } + const match = /^v(?[1-9]\d*)\/(?.+)$/.exec(title); + if (!match?.groups?.version || !match.groups.path) { + throw new Error(`Invalid model file path in the OCI package: ${title}`); + } + const layerVersion = Number(match.groups.version); + if (version !== undefined && version !== layerVersion) { + throw new Error('The selected dictation model package contains multiple model versions.'); + } + version = layerVersion; + + const pathSegments = match.groups.path.split('/'); + if (pathSegments.some(segment => !segment || segment === '.' || segment === '..' || segment.includes('\\'))) { + throw new Error(`Invalid model file path in the OCI package: ${title}`); + } + + const target = join(materializedRoot, `v${version}`, ...pathSegments); + await fs.mkdir(dirname(target), { recursive: true }); + const blob = resolveOciBlob(ociRoot, layer.digest); + await verifyOciBlob(blob); + if (canMove) { + await fs.rename(blob.path, target); + } else { + await fs.copyFile(blob.path, target); + } + } + + if (version !== undefined) { + const versionDirectory = join(materializedRoot, `v${version}`); + await assertMaterializedIdentity(versionDirectory, version); + await writeInferenceModel(versionDirectory, version); + return { version, versionDirectory, canMove: true }; + } + } + + throw new Error('The selected package does not contain a dictation model OCI payload.'); +} + +function resolveOciBlob(ociRoot: string, digest: string): { readonly path: string; readonly hash: string } { + const match = /^sha256:(?[a-fA-F0-9]{64})$/.exec(digest); + if (!match?.groups?.hash) { + throw new Error(`Unsupported OCI digest: ${digest}`); + } + const hash = match.groups.hash.toLowerCase(); + return { path: join(ociRoot, 'blobs', 'sha256', hash), hash }; +} + +/** Read a (small) OCI blob into memory and verify it against its content digest. */ +async function readVerifiedOciBlob(ociRoot: string, digest: string): Promise { + const blob = resolveOciBlob(ociRoot, digest); + const contents = await fs.readFile(blob.path); + if (createHash('sha256').update(contents).digest('hex') !== blob.hash) { + throw new Error('The selected model package is corrupt (checksum mismatch).'); + } + return contents; +} + +/** Verify a blob against its content digest by streaming it (no full buffering). */ +async function verifyOciBlob(blob: { readonly path: string; readonly hash: string }): Promise { + const hash = createHash('sha256'); + for await (const chunk of createReadStream(blob.path)) { + hash.update(chunk as Buffer); + } + if (hash.digest('hex') !== blob.hash) { + throw new Error('The selected model package is corrupt (checksum mismatch).'); + } +} + +/** + * Interpret a Foundry Local `inference_model.json` `Name`. Returns the model + * version when the name identifies the supported nemotron CPU model, throws when + * it names a *different* model, and returns `undefined` when there is no name to + * check (a truly unlabeled package we accept on trust). + */ +function modelVersionFromName(name: string | undefined): number | undefined { + if (!name) { + return undefined; + } + const match = new RegExp(`^${MODEL_VARIANT.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:(?[1-9]\\d*)$`).exec(name); + if (!match?.groups?.version) { + throw new Error(`The selected package is not the ${DEFAULT_LOCAL_TRANSCRIPTION_MODEL} CPU model.`); + } + return Number(match.groups.version); +} + +/** + * Guard the OCI path against mislabeling: if the materialized payload already + * carries an `inference_model.json`, it must identify the supported model at + * this version before we (re)write our own scanner metadata over it. A payload + * with no embedded identity is accepted on trust. + */ +async function assertMaterializedIdentity(versionDirectory: string, version: number): Promise { + let metadata: { Name?: string }; + try { + metadata = JSON.parse(await fs.readFile(join(versionDirectory, INFERENCE_MODEL_FILE), 'utf8')); + } catch { + return; + } + const named = modelVersionFromName(metadata.Name); + if (named !== undefined && named !== version) { + throw new Error('The selected package identifies a different model version than its files.'); + } +} + +async function findPreparedModel(root: string, canMove: boolean): Promise { + const inferenceModel = (await findNamedFiles(root, INFERENCE_MODEL_FILE))[0]; + if (inferenceModel) { + const metadata = JSON.parse(await fs.readFile(inferenceModel, 'utf8')) as { Name?: string }; + const version = modelVersionFromName(metadata.Name); + if (version === undefined) { + throw new Error(`The selected folder is not the ${DEFAULT_LOCAL_TRANSCRIPTION_MODEL} CPU model.`); + } + const versionDirectory = dirname(inferenceModel); + if (basename(versionDirectory) !== `v${version}`) { + throw new Error(`The model files must be stored in a v${version} folder.`); + } + return { version, versionDirectory, canMove }; + } + + const genaiConfig = (await findNamedFiles(root, GENAI_CONFIG_FILE))[0]; + if (genaiConfig) { + const versionDirectory = dirname(genaiConfig); + const match = /^v(?[1-9]\d*)$/.exec(basename(versionDirectory)); + if (!match?.groups?.version) { + throw new Error('The model files must be stored in a version folder such as v3.'); + } + return { version: Number(match.groups.version), versionDirectory, canMove }; + } + + throw new Error('The selected package does not contain a supported dictation model.'); +} + +async function verifyPreparedModel(versionDirectory: string): Promise { + const entries = await fs.readdir(versionDirectory, { withFileTypes: true }); + const hasOnnxModel = entries.some(entry => entry.isFile() && entry.name.toLowerCase().endsWith('.onnx')); + if (!hasOnnxModel) { + throw new Error('The selected dictation model is missing its ONNX model files.'); + } + try { + JSON.parse(await fs.readFile(join(versionDirectory, GENAI_CONFIG_FILE), 'utf8')); + } catch { + throw new Error(`The selected dictation model has a missing or invalid ${GENAI_CONFIG_FILE}.`); + } +} + +async function installPreparedModel(model: IPreparedModel, cacheDir: string): Promise { + const publisherDirectory = join(cacheDir, MODEL_PUBLISHER); + await fs.mkdir(publisherDirectory, { recursive: true }); + const modelDirectoryName = `${MODEL_VARIANT}-${model.version}`; + const target = join(publisherDirectory, modelDirectoryName); + const staged = join(publisherDirectory, `.${modelDirectoryName}.staged-${randomUUID()}`); + const backup = join(publisherDirectory, `.${modelDirectoryName}.backup-${randomUUID()}`); + const stagedVersion = join(staged, `v${model.version}`); + + try { + if (model.canMove) { + await fs.mkdir(staged, { recursive: true }); + await fs.rename(model.versionDirectory, stagedVersion); + } else { + await copyDirectory(model.versionDirectory, stagedVersion); + } + await writeInferenceModel(stagedVersion, model.version); + await replaceDirectory(staged, target, backup); + } finally { + // Only the staging area is always safe to remove. The backup is owned by + // `replaceDirectory`: it is deleted there once the swap succeeds and is + // otherwise the sole surviving copy of a previously working model, so it + // must never be force-removed here (a failed rollback would lose it). + await fs.rm(staged, { recursive: true, force: true }); + } +} + +async function replaceDirectory(staged: string, target: string, backup: string): Promise { + let movedExisting = false; + try { + await fs.rename(target, backup); + movedExisting = true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } + + try { + await fs.rename(staged, target); + } catch (error) { + if (movedExisting) { + await fs.rename(backup, target); + } + throw error; + } + + if (movedExisting) { + await fs.rm(backup, { recursive: true, force: true }); + } +} + +async function copyDirectory(source: string, target: string): Promise { + await fs.mkdir(target, { recursive: true }); + for (const entry of await fs.readdir(source, { withFileTypes: true })) { + const sourceEntry = join(source, entry.name); + const targetEntry = join(target, entry.name); + if (entry.isDirectory()) { + await copyDirectory(sourceEntry, targetEntry); + } else if (entry.isFile()) { + await fs.copyFile(sourceEntry, targetEntry); + } else { + throw new Error(`Unsupported model package entry: ${entry.name}`); + } + } +} + +async function writeInferenceModel(versionDirectory: string, version: number): Promise { + await fs.writeFile(join(versionDirectory, INFERENCE_MODEL_FILE), JSON.stringify({ + Name: `${MODEL_VARIANT}:${version}`, + PromptTemplate: null, + }, undefined, 2)); +} + +async function findNamedFiles(root: string, name: string): Promise { + const matches: string[] = []; + const directories = [root]; + while (directories.length) { + const directory = directories.pop()!; + for (const entry of await fs.readdir(directory, { withFileTypes: true })) { + const entryPath = join(directory, entry.name); + if (entry.isDirectory()) { + directories.push(entryPath); + } else if (entry.isFile() && entry.name === name) { + matches.push(entryPath); + } + } + } + return matches.sort(); +} diff --git a/src/vs/platform/localTranscription/node/localTranscriptionService.ts b/src/vs/platform/localTranscription/node/localTranscriptionService.ts index 02303b89139..a036f2a3a6e 100644 --- a/src/vs/platform/localTranscription/node/localTranscriptionService.ts +++ b/src/vs/platform/localTranscription/node/localTranscriptionService.ts @@ -13,8 +13,11 @@ import { ILocalTranscriptionModelStatus, ILocalTranscriptionResult, ILocalTranscriptionService, + DEFAULT_LOCAL_TRANSCRIPTION_MODEL, + ILocalTranscriptionModelImportResult, LocalTranscriptionModelState, } from '../common/localTranscription.js'; +import { importFoundryLocalModel } from './foundryLocalModelImport.js'; /** PCM audio format the renderer captures and streams: mono 16 kHz signed 16-bit. */ const SAMPLE_RATE = 16000; @@ -26,8 +29,6 @@ const BITS_PER_SAMPLE = 16; * Nemotron streaming RNN-T model the GitHub Copilot app ships for dictation; it * runs through Foundry Local's native streaming ASR engine (ORT + ORT-GenAI). */ -const DEFAULT_MODEL = 'nemotron-speech-streaming-en-0.6b'; - /** Application name reported to Foundry Local for logs/telemetry and its data dir. */ const FOUNDRY_APP_NAME = 'vscode-dictation'; @@ -270,6 +271,10 @@ export class LocalTranscriptionService extends Disposable implements ILocalTrans return this._status; } + importModel(options: { sourcePath: string; cacheDir: string }): Promise { + return importFoundryLocalModel(options.sourcePath, options.cacheDir); + } + private _setStatus(status: ILocalTranscriptionModelStatus): void { this._status = status; this._onDidChangeModelStatus.fire(status); @@ -298,7 +303,7 @@ export class LocalTranscriptionService extends Disposable implements ILocalTrans this._pendingChunks = []; this._runtimeError = undefined; - const model = options.model ?? DEFAULT_MODEL; + const model = options.model ?? DEFAULT_LOCAL_TRANSCRIPTION_MODEL; const language = options.language; // Do not block capture on the (possibly first-use) model download/load and // session open; buffer audio until the session is ready, then flush it. diff --git a/src/vs/platform/localTranscription/test/node/foundryLocalModelImport.test.ts b/src/vs/platform/localTranscription/test/node/foundryLocalModelImport.test.ts new file mode 100644 index 00000000000..caf55d1bd9d --- /dev/null +++ b/src/vs/platform/localTranscription/test/node/foundryLocalModelImport.test.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as fs from 'fs'; +import { createHash } from 'crypto'; +import { tmpdir } from 'os'; +import { join } from '../../../../base/common/path.js'; +import { zip } from '../../../../base/node/zip.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { getRandomTestPath } from '../../../../base/test/node/testUtils.js'; +import { DEFAULT_LOCAL_TRANSCRIPTION_MODEL } from '../../common/localTranscription.js'; +import { importFoundryLocalModel } from '../../node/foundryLocalModelImport.js'; + +suite('FoundryLocalModelImport', () => { + let testDirectory: string; + let cacheDirectory: string; + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(async () => { + testDirectory = getRandomTestPath(tmpdir(), 'vsctests', 'foundry-model-import'); + cacheDirectory = join(testDirectory, 'cache'); + await fs.promises.mkdir(testDirectory, { recursive: true }); + }); + + teardown(() => fs.promises.rm(testDirectory, { recursive: true, force: true })); + + test('imports a prepared model directory and creates scanner metadata', async () => { + const source = join(testDirectory, 'prepared', 'v4'); + await fs.promises.mkdir(source, { recursive: true }); + await fs.promises.writeFile(join(source, 'genai_config.json'), '{}'); + await fs.promises.writeFile(join(source, 'encoder.onnx'), 'model'); + + const staleTarget = modelVersionDirectory(cacheDirectory, 4); + await fs.promises.mkdir(staleTarget, { recursive: true }); + await fs.promises.writeFile(join(staleTarget, 'stale'), 'old'); + + const result = await importFoundryLocalModel(source, cacheDirectory); + const target = modelVersionDirectory(cacheDirectory, 4); + const metadata = JSON.parse(await fs.promises.readFile(join(target, 'inference_model.json'), 'utf8')); + + assert.deepStrictEqual({ + result, + files: (await fs.promises.readdir(target)).sort(), + metadata, + }, { + result: { model: DEFAULT_LOCAL_TRANSCRIPTION_MODEL, version: 4 }, + files: ['encoder.onnx', 'genai_config.json', 'inference_model.json'], + metadata: { + Name: `${DEFAULT_LOCAL_TRANSCRIPTION_MODEL}-generic-cpu:4`, + PromptTemplate: null, + }, + }); + }); + + test('imports the official nested expansion-pack OCI layout', async () => { + const packageZip = join(testDirectory, 'Package.zip'); + const configContents = '{}'; + const modelContents = 'model'; + const configDigest = digest(configContents); + const modelDigest = digest(modelContents); + const ociPrefix = 'payload/oci/models/foundry-local/nemotron/cpu-onnx'; + const manifest = { + layers: [ + { digest: configDigest, annotations: { 'org.opencontainers.image.title': 'v3/genai_config.json' } }, + { digest: modelDigest, annotations: { 'org.opencontainers.image.title': 'v3/encoder.onnx' } }, + ], + }; + const manifestContents = JSON.stringify(manifest); + const manifestDigest = digest(manifestContents); + await zip(packageZip, [ + { path: `${ociPrefix}/oci-layout`, contents: JSON.stringify({ imageLayoutVersion: '1.0.0' }) }, + { path: `${ociPrefix}/index.json`, contents: JSON.stringify({ manifests: [{ digest: manifestDigest }] }) }, + { path: `${ociPrefix}/blobs/sha256/${manifestDigest.slice('sha256:'.length)}`, contents: manifestContents }, + { path: `${ociPrefix}/blobs/sha256/${configDigest.slice('sha256:'.length)}`, contents: configContents }, + { path: `${ociPrefix}/blobs/sha256/${modelDigest.slice('sha256:'.length)}`, contents: modelContents }, + ]); + + const expansionPack = join(testDirectory, 'model.zip'); + await zip(expansionPack, [ + { path: 'Manifest.xml', contents: '' }, + { path: 'Package.zip', localPath: packageZip }, + ]); + + const result = await importFoundryLocalModel(expansionPack, cacheDirectory); + const target = modelVersionDirectory(cacheDirectory, 3); + + assert.deepStrictEqual({ + result, + files: (await fs.promises.readdir(target)).sort(), + }, { + result: { model: DEFAULT_LOCAL_TRANSCRIPTION_MODEL, version: 3 }, + files: ['encoder.onnx', 'genai_config.json', 'inference_model.json'], + }); + }); + + test('rejects OCI layer paths that escape the model version directory', async () => { + const source = join(testDirectory, 'oci'); + const modelContents = 'model'; + const modelDigest = digest(modelContents); + const manifestContents = JSON.stringify({ + layers: [{ + digest: modelDigest, + annotations: { 'org.opencontainers.image.title': 'v3/../outside.onnx' }, + }], + }); + const manifestDigest = digest(manifestContents); + await fs.promises.mkdir(join(source, 'blobs', 'sha256'), { recursive: true }); + await fs.promises.writeFile(join(source, 'oci-layout'), JSON.stringify({ imageLayoutVersion: '1.0.0' })); + await fs.promises.writeFile(join(source, 'index.json'), JSON.stringify({ manifests: [{ digest: manifestDigest }] })); + await fs.promises.writeFile(join(source, 'blobs', 'sha256', manifestDigest.slice('sha256:'.length)), manifestContents); + await fs.promises.writeFile(join(source, 'blobs', 'sha256', modelDigest.slice('sha256:'.length)), modelContents); + + await assert.rejects( + importFoundryLocalModel(source, cacheDirectory), + /Invalid model file path/, + ); + assert.strictEqual(fs.existsSync(join(cacheDirectory, 'Microsoft')), false); + }); + + test('rejects an OCI package whose embedded metadata names a different model', async () => { + const source = join(testDirectory, 'oci-wrong-model'); + const configContents = '{}'; + const inferenceContents = JSON.stringify({ Name: 'some-other-model-generic-cpu:3' }); + const configDigest = digest(configContents); + const inferenceDigest = digest(inferenceContents); + const manifestContents = JSON.stringify({ + layers: [ + { digest: configDigest, annotations: { 'org.opencontainers.image.title': 'v3/genai_config.json' } }, + { digest: inferenceDigest, annotations: { 'org.opencontainers.image.title': 'v3/inference_model.json' } }, + ], + }); + const manifestDigest = digest(manifestContents); + await fs.promises.mkdir(join(source, 'blobs', 'sha256'), { recursive: true }); + await fs.promises.writeFile(join(source, 'oci-layout'), JSON.stringify({ imageLayoutVersion: '1.0.0' })); + await fs.promises.writeFile(join(source, 'index.json'), JSON.stringify({ manifests: [{ digest: manifestDigest }] })); + await fs.promises.writeFile(join(source, 'blobs', 'sha256', manifestDigest.slice('sha256:'.length)), manifestContents); + await fs.promises.writeFile(join(source, 'blobs', 'sha256', configDigest.slice('sha256:'.length)), configContents); + await fs.promises.writeFile(join(source, 'blobs', 'sha256', inferenceDigest.slice('sha256:'.length)), inferenceContents); + + await assert.rejects( + importFoundryLocalModel(source, cacheDirectory), + new RegExp(`not the ${DEFAULT_LOCAL_TRANSCRIPTION_MODEL}`), + ); + assert.strictEqual(fs.existsSync(join(cacheDirectory, 'Microsoft')), false); + }); + + test('rejects an OCI package with a blob that fails its checksum', async () => { + const source = join(testDirectory, 'oci-corrupt'); + const configContents = '{}'; + const modelContents = 'model'; + const configDigest = digest(configContents); + const modelDigest = digest(modelContents); + const manifestContents = JSON.stringify({ + layers: [ + { digest: configDigest, annotations: { 'org.opencontainers.image.title': 'v3/genai_config.json' } }, + { digest: modelDigest, annotations: { 'org.opencontainers.image.title': 'v3/encoder.onnx' } }, + ], + }); + const manifestDigest = digest(manifestContents); + await fs.promises.mkdir(join(source, 'blobs', 'sha256'), { recursive: true }); + await fs.promises.writeFile(join(source, 'oci-layout'), JSON.stringify({ imageLayoutVersion: '1.0.0' })); + await fs.promises.writeFile(join(source, 'index.json'), JSON.stringify({ manifests: [{ digest: manifestDigest }] })); + await fs.promises.writeFile(join(source, 'blobs', 'sha256', manifestDigest.slice('sha256:'.length)), manifestContents); + await fs.promises.writeFile(join(source, 'blobs', 'sha256', configDigest.slice('sha256:'.length)), configContents); + // Content that does not hash to the digest used as its blob filename. + await fs.promises.writeFile(join(source, 'blobs', 'sha256', modelDigest.slice('sha256:'.length)), 'tampered'); + + await assert.rejects( + importFoundryLocalModel(source, cacheDirectory), + /corrupt/, + ); + assert.strictEqual(fs.existsSync(join(cacheDirectory, 'Microsoft')), false); + }); + + test('rejects prepared directories for other models', async () => { + const source = join(testDirectory, 'prepared', 'v3'); + await fs.promises.mkdir(source, { recursive: true }); + await fs.promises.writeFile(join(source, 'genai_config.json'), '{}'); + await fs.promises.writeFile(join(source, 'encoder.onnx'), 'model'); + await fs.promises.writeFile(join(source, 'inference_model.json'), JSON.stringify({ Name: 'other-model:3' })); + + await assert.rejects( + importFoundryLocalModel(source, cacheDirectory), + new RegExp(`not the ${DEFAULT_LOCAL_TRANSCRIPTION_MODEL}`), + ); + }); +}); + +function modelVersionDirectory(cacheDirectory: string, version: number): string { + return join( + cacheDirectory, + 'Microsoft', + `${DEFAULT_LOCAL_TRANSCRIPTION_MODEL}-generic-cpu-${version}`, + `v${version}`, + ); +} + +function digest(contents: string): string { + return `sha256:${createHash('sha256').update(contents).digest('hex')}`; +} diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts b/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts index c1f037db0b8..37366b0cbc5 100644 --- a/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts +++ b/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts @@ -10,6 +10,8 @@ import { generateUuid } from '../../../../../base/common/uuid.js'; import { computeLevenshteinDistance } from '../../../../../base/common/diff/diff.js'; import { joinPath } from '../../../../../base/common/resources.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IAction, toAction } from '../../../../../base/common/actions.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; @@ -21,7 +23,7 @@ import { localize } from '../../../../../nls.js'; import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; -import { ILocalTranscriptionModelStatus, ILocalTranscriptionService, LocalTranscriptionModelState } from '../../../../../platform/localTranscription/common/localTranscription.js'; +import { DEFAULT_LOCAL_TRANSCRIPTION_MODEL, ILocalTranscriptionModelStatus, ILocalTranscriptionService, LocalTranscriptionModelState } from '../../../../../platform/localTranscription/common/localTranscription.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IAuthenticationService } from '../../../../services/authentication/common/authentication.js'; import { IVoiceClientService, IVoiceSessionContext, IVoiceTranscription, IVoiceTurnConfig } from '../../common/voiceClient/voiceClientService.js'; @@ -35,6 +37,14 @@ import { createPcmCaptureNode } from '../pcmCaptureWorklet.js'; export const IChatSpeechToTextService = createDecorator('chatSpeechToTextService'); +/** + * Command that imports a locally supplied Foundry Local dictation model package + * into the model cache. Registered in the desktop layer + * (`installDictationModelAction.ts`); referenced here so a failed download in a + * registry-blocked environment can offer the offline install as a next step. + */ +export const INSTALL_DICTATION_MODEL_COMMAND_ID = 'workbench.action.chat.installDictationModel'; + function joinIncrementalDictationText(prefix: string, suffix: string): string { if (!prefix || !suffix) { return `${prefix}${suffix}`; @@ -567,6 +577,7 @@ export class ChatSpeechToTextService extends Disposable implements IChatSpeechTo @INotificationService private readonly _notificationService: INotificationService, @IProgressService private readonly _progressService: IProgressService, @ILogService private readonly _logService: ILogService, + @ICommandService private readonly _commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService, @IStorageService private readonly _storageService: IStorageService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @@ -1211,7 +1222,7 @@ export class ChatSpeechToTextService extends Disposable implements IChatSpeechTo } else if (status.state === LocalTranscriptionModelState.Error) { this._logModelPrepareTelemetry(status); this._setPreparingModel(false); - this._failSession('model', localize('chatStt.modelError', "On-device speech-to-text model failed to load: {0}", status.error ?? '')); + this._failModelSession(status); } } @@ -1286,12 +1297,38 @@ export class ChatSpeechToTextService extends Disposable implements IChatSpeechTo this._downloadNotification = undefined; } + /** + * Handle a terminal model-preparation error. A download failure caused by a + * blocked/unreachable model registry (common on locked-down corporate + * networks) is recoverable by importing the model from a locally supplied + * package, so in that case the error surfaces an action that launches the + * offline install flow. Other failures show a plain error. + */ + private _failModelSession(status: ILocalTranscriptionModelStatus): void { + const canImport = this._localTranscription.isSupported + && (status.errorCode === 'network' || status.errorCode === 'notFound'); + if (!canImport) { + this._failSession('model', localize('chatStt.modelError', "On-device speech-to-text model failed to load: {0}", status.error ?? '')); + return; + } + // Name the specific model so users know exactly which package to obtain + // on a machine that can reach the download, then sideload via the command. + const message = localize('chatStt.modelErrorOffline', "Could not download the {0} speech-to-text model, which can happen on networks that block the model registry. You can install it from a downloaded package instead.", DEFAULT_LOCAL_TRANSCRIPTION_MODEL); + const importAction = toAction({ + id: INSTALL_DICTATION_MODEL_COMMAND_ID, + label: localize('chatStt.installFromPackage', "Install from Local Package..."), + run: () => this._commandService.executeCommand(INSTALL_DICTATION_MODEL_COMMAND_ID), + }); + this._failSession('model', message, importAction); + } + /** * Abort the active recording because of an unrecoverable error (e.g. the * model failed to download/load), surfacing a notification instead of - * silently returning an empty transcript. + * silently returning an empty transcript. An optional recovery action is + * attached to the notification when the failure is actionable. */ - private _failSession(errorCode: string, message: string): void { + private _failSession(errorCode: string, message: string, action?: IAction): void { if (this._state === ChatSpeechToTextState.Idle) { return; } @@ -1300,7 +1337,11 @@ export class ChatSpeechToTextService extends Disposable implements IChatSpeechTo this._cancelBackend(); this._teardown(); this._setState(ChatSpeechToTextState.Idle); - this._notificationService.error(message); + if (action) { + this._notificationService.notify({ severity: Severity.Error, message, actions: { primary: [action] } }); + } else { + this._notificationService.error(message); + } } /** diff --git a/src/vs/workbench/contrib/chat/electron-browser/actions/installDictationModelAction.ts b/src/vs/workbench/contrib/chat/electron-browser/actions/installDictationModelAction.ts new file mode 100644 index 00000000000..c8548e5a067 --- /dev/null +++ b/src/vs/workbench/contrib/chat/electron-browser/actions/installDictationModelAction.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { joinPath } from '../../../../../base/common/resources.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; +import { DEFAULT_LOCAL_TRANSCRIPTION_MODEL, ILocalTranscriptionService } from '../../../../../platform/localTranscription/common/localTranscription.js'; +import { INotificationService } from '../../../../../platform/notification/common/notification.js'; +import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; +import { CHAT_CATEGORY } from '../../browser/actions/chatActions.js'; +import { INSTALL_DICTATION_MODEL_COMMAND_ID } from '../../browser/speechToText/chatSpeechToTextService.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; + +export function registerInstallDictationModelAction(): void { + const enabled = ContextKeyExpr.and( + ChatContextKeys.enabled, + ContextKeyExpr.equals('config.dictation.enabled', true), + ); + + registerAction2(class InstallDictationModelAction extends Action2 { + constructor() { + super({ + id: INSTALL_DICTATION_MODEL_COMMAND_ID, + category: CHAT_CATEGORY, + title: localize2('chat.installDictationModel', "Install Dictation Model from Local Package..."), + precondition: enabled, + menu: { + id: MenuId.CommandPalette, + when: enabled, + }, + }); + } + + async run(accessor: ServicesAccessor): Promise { + const localTranscriptionService = accessor.get(ILocalTranscriptionService); + const notificationService = accessor.get(INotificationService); + const fileDialogService = accessor.get(IFileDialogService); + const progressService = accessor.get(IProgressService); + const environmentService = accessor.get(IEnvironmentService); + if (!localTranscriptionService.isSupported) { + notificationService.warn(localize('chat.installDictationModel.unsupported', "On-device dictation is not supported on this platform.")); + return; + } + + const sources = await fileDialogService.showOpenDialog({ + title: localize('chat.installDictationModel.dialogTitle', "Select the {0} CPU model package (.zip) or folder", DEFAULT_LOCAL_TRANSCRIPTION_MODEL), + openLabel: localize('chat.installDictationModel.openLabel', "Install"), + canSelectFiles: true, + canSelectFolders: true, + canSelectMany: false, + filters: [{ name: localize('chat.installDictationModel.filter', "Foundry Local Model Package"), extensions: ['zip'] }], + }); + const source = sources?.[0]; + if (!source) { + return; + } + if (source.scheme !== Schemas.file) { + notificationService.error(localize('chat.installDictationModel.localOnly', "The dictation model package must be on the local file system.")); + return; + } + + const cacheDir = joinPath(environmentService.cacheHome, 'chatDictationModels').fsPath; + try { + const result = await progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('chat.installDictationModel.progress', "Installing dictation model..."), + }, () => localTranscriptionService.importModel({ sourcePath: source.fsPath, cacheDir })); + notificationService.info(localize( + 'chat.installDictationModel.success', + "Installed {0} version {1}.", + result.model, + result.version, + )); + } catch (error) { + notificationService.error(localize( + 'chat.installDictationModel.error', + "Failed to install the dictation model: {0}", + error instanceof Error ? error.message : String(error), + )); + } + } + }); +} 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 73d4a1b6947..c927fa1d3af 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -55,6 +55,7 @@ import { IPluginGitService } from '../common/plugins/pluginGitService.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; import { registerChatExportZipAction } from './actions/chatExportZip.js'; import { registerExportAgentTracesDbAction } from './actions/exportAgentTracesDb.js'; +import { registerInstallDictationModelAction } from './actions/installDictationModelAction.js'; import { shouldWarnForSessionShutdown } from './chatLifecycle.js'; import { HoldToVoiceChatInChatViewAction, InlineVoiceChatAction, KeywordActivationContribution, QuickVoiceChatAction, ReadChatResponseAloud, StartVoiceChatAction, StopListeningAction, StopListeningAndSubmitAction, StopReadAloud, StopReadChatItemAloud, VoiceChatInChatViewAction } from './actions/voiceChatActions.js'; import { OpenWorkspaceInAgentsWindowAction, OpenWorkspaceInAgentsContribution, OpenAgentsWindowAction, OpenChatSessionInAgentsWindowAction, AgentsHandoffInputTipContribution, ToggleOpenInAgentsWindowTitleBarAction, OpenWorkspaceInAgentsWindowChatTitleAction, OpenWorkspaceInAgentsWindowTitleBarAction } from './agentSessions/agentSessionsActions.js'; @@ -264,6 +265,7 @@ registerAction2(StopReadAloud); registerChatDeveloperActions(); registerChatExportZipAction(); registerExportAgentTracesDbAction(); +registerInstallDictationModelAction(); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(NativeBuiltinToolsContribution.ID, NativeBuiltinToolsContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/services/localTranscription/browser/localTranscriptionService.ts b/src/vs/workbench/services/localTranscription/browser/localTranscriptionService.ts index 98e8e13fd09..e4d230d7674 100644 --- a/src/vs/workbench/services/localTranscription/browser/localTranscriptionService.ts +++ b/src/vs/workbench/services/localTranscription/browser/localTranscriptionService.ts @@ -26,6 +26,10 @@ export class NullLocalTranscriptionService implements ILocalTranscriptionService return { state: LocalTranscriptionModelState.Error, error: 'unsupported' }; } + async importModel(): Promise { + throw new Error('On-device transcription is not supported in this environment.'); + } + async start(): Promise { throw new Error('On-device transcription is not supported in this environment.'); } diff --git a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts index 3c9484d9604..c15ef4b2e18 100644 --- a/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts +++ b/src/vs/workbench/services/localTranscription/electron-browser/localTranscriptionService.ts @@ -82,6 +82,7 @@ export class LocalTranscriptionService { get onDidTranscribe() { return this._getProxy().onDidTranscribe; } getModelStatus() { return this._getProxy().getModelStatus(); } + importModel(options: Parameters[0]) { return this._getProxy().importModel(options); } start(options: { cacheDir: string; model?: string; language?: string }) { const { proxyUrl, noProxy, proxyStrictSSL, proxyAuthorization } = this._resolveProxyConfig(); const runtime = this.productService.dictationRuntime; From 020db71706379070de315f407b07613a593f31ea Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 30 Jul 2026 11:22:37 -0700 Subject: [PATCH 20/86] agentHost: simplify encrypted thinking opt-in Replace the Agent Host BYOK model-options sentinel and private MIME payload with a typed provider request option while retaining encrypted thinking metadata only for opted-in requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../vscode-node/languageModelAccess.ts | 13 ++--- .../vscode-node/languageModelAccessPrompt.tsx | 26 +++------ .../api/common/extHostLanguageModels.ts | 2 +- .../agentHost/agentHostByokLmHandler.ts | 54 ++++--------------- .../contrib/chat/common/languageModels.ts | 1 + .../agentHostByokLmHandler.test.ts | 11 ++-- .../vscode.proposed.chatProvider.d.ts | 5 ++ 7 files changed, 34 insertions(+), 78 deletions(-) diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts index d4a31944600..2d0592ef1f1 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -837,18 +837,15 @@ export class CopilotLanguageModelWrapper extends Disposable { } async provideLanguageModelResponse(endpoint: IChatEndpoint, messages: Array, options: vscode.ProvideLanguageModelChatResponseOptions, extensionId: string | undefined, progress: vscode.Progress, token: vscode.CancellationToken): Promise { - const preserveAgentHostByokReasoning = options.modelOptions?._vscodeAgentHostByokReasoningBridge === true; let thinkingActive = false; const finishCallback: FinishedCallback = async (_text, index, delta): Promise => { if (delta.thinking) { if (isEncryptedThinkingDelta(delta.thinking)) { - if (preserveAgentHostByokReasoning) { - progress.report(new vscode.LanguageModelDataPart( - new TextEncoder().encode(JSON.stringify({ - id: delta.thinking.id, - encryptedContent: delta.thinking.encrypted, - })), - 'application/vnd.code.agent-host-byok-reasoning+json' + if (options.includeEncryptedThinking) { + progress.report(new vscode.LanguageModelThinkingPart( + delta.thinking.text ?? '', + delta.thinking.id, + { encrypted_content: delta.thinking.encrypted }, )); } } else { diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx index 90813e90921..e62f6a0e39b 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx @@ -15,16 +15,6 @@ import { EditorIntegrationRules } from '../../prompts/node/panel/editorIntegrati import { imageDataPartToTSX, ToolResult } from '../../prompts/node/panel/toolCalling'; import { isImageDataPart } from '../common/languageModelChatMessageHelpers'; -const AGENT_HOST_BYOK_REASONING_MIME_TYPE = 'application/vnd.code.agent-host-byok-reasoning+json'; - -function decodeAgentHostByokReasoning(data: Uint8Array): { id?: string; encryptedContent: string } { - const value = JSON.parse(new TextDecoder().decode(data)) as { id?: unknown; encryptedContent?: unknown }; - if ((value.id !== undefined && typeof value.id !== 'string') || typeof value.encryptedContent !== 'string') { - throw new Error('Invalid Agent Host BYOK reasoning data'); - } - return { id: value.id, encryptedContent: value.encryptedContent }; -} - export type Props = PromptElementProps<{ noSafety: boolean; messages: Array; @@ -47,22 +37,18 @@ export class LanguageModelAccessPrompt extends PromptElement { } else if (message.role === vscode.LanguageModelChatMessageRole.Assistant) { const statefulMarkerPart = message.content.find(part => part instanceof vscode.LanguageModelDataPart && part.mimeType === CustomDataPartMimeTypes.StatefulMarker) as vscode.LanguageModelDataPart | undefined; const statefulMarker = statefulMarkerPart && decodeStatefulMarker(statefulMarkerPart.data); - const reasoningDataPart = message.content.find(part => part instanceof vscode.LanguageModelDataPart && part.mimeType === AGENT_HOST_BYOK_REASONING_MIME_TYPE) as vscode.LanguageModelDataPart | undefined; - const reasoningData = reasoningDataPart && decodeAgentHostByokReasoning(reasoningDataPart.data); const filteredContent = message.content.filter(part => !(part instanceof vscode.LanguageModelDataPart)); // There should only be one string part per message const content = filteredContent.find(part => part instanceof LanguageModelTextPart); const toolCalls = filteredContent.filter(part => part instanceof vscode.LanguageModelToolCallPart); - const thinking = filteredContent.find(part => part instanceof vscode.LanguageModelThinkingPart); + const thinkingParts = filteredContent.filter(part => part instanceof vscode.LanguageModelThinkingPart); + const thinking = thinkingParts.find(part => typeof part.metadata?.encrypted_content === 'string') ?? thinkingParts.at(-1); + const thinkingText = thinkingParts.flatMap(part => Array.isArray(part.value) ? part.value : [part.value]); + const thinkingMetadata = Object.assign({}, ...thinkingParts.map(part => part.metadata)); const statefulMarkerElement = statefulMarker && ; - const thinkingId = reasoningData?.id ?? thinking?.id; - const thinkingElement = thinkingId && ; + const encrypted = typeof thinkingMetadata.encrypted_content === 'string' ? thinkingMetadata.encrypted_content : undefined; + const thinkingElement = thinking && thinking.id && ; chatMessages.push( ({ id: tc.callId, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.input) } }))}>{statefulMarkerElement}{content?.value}{thinkingElement}); } else if (message.role === vscode.LanguageModelChatMessageRole.User) { for (const part of message.content) { diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index f9382c2e943..db5b0006326 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -344,7 +344,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { knownModel.info, messages.value.map(typeConvert.LanguageModelChatMessage2.to), // todo@connor4312: move `core` -> `undefined` after 1.111 Insiders is out - { ...options, modelOptions: options.modelOptions ?? {}, modelConfiguration: options.configuration, requestInitiator: from ? ExtensionIdentifier.toKey(from) : 'core', toolMode: options.toolMode ?? extHostTypes.LanguageModelChatToolMode.Auto }, + { ...options, modelOptions: options.modelOptions ?? {}, modelConfiguration: options.configuration, requestInitiator: from ? ExtensionIdentifier.toKey(from) : 'core', toolMode: options.toolMode ?? extHostTypes.LanguageModelChatToolMode.Auto, includeEncryptedThinking: options.includeEncryptedThinking }, progress, providerToken ); 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 99faae000b3..e7bad34c7c3 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -27,8 +27,6 @@ import { const STATEFUL_MARKER_MIME_TYPE = 'stateful_marker'; const USAGE_MIME_TYPE = 'usage'; -const AGENT_HOST_BYOK_REASONING_MIME_TYPE = 'application/vnd.code.agent-host-byok-reasoning+json'; -const AGENT_HOST_BYOK_REASONING_MODEL_OPTION = '_vscodeAgentHostByokReasoningBridge'; /** * Renderer-side {@link IAgentHostByokLmHandler}. Services BYOK chat requests @@ -76,10 +74,8 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok })) : undefined; const options: ILanguageModelChatRequestOptions = { - modelOptions: { - ...request.modelOptions, - [AGENT_HOST_BYOK_REASONING_MODEL_OPTION]: true, - }, + modelOptions: request.modelOptions, + includeEncryptedThinking: true, ...(request.reasoningEffort ? { configuration: { reasoningEffort: request.reasoningEffort } } : {}), ...(tools ? { tools } : {}), }; @@ -117,8 +113,6 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok } } else if (p.type === 'data' && p.mimeType === STATEFUL_MARKER_MIME_TYPE) { responseId = this._decodeStatefulMarker(p.data, request.modelId); - } else if (p.type === 'data' && p.mimeType === AGENT_HOST_BYOK_REASONING_MIME_TYPE) { - this._appendEncryptedReasoningOutput(output, p.data); } else if (p.type === 'data' && p.mimeType === USAGE_MIME_TYPE) { usage = this._decodeUsage(p.data); } @@ -201,25 +195,17 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok content: item.content.map(part => ({ type: 'text', value: part.text })), }; case 'reasoning': { - const content: IChatMessagePart[] = [{ - type: 'thinking', - value: item.summary, - id: item.id, - metadata: item.metadata, - }]; - if (item.encryptedContent) { - content.push({ - type: 'data', - mimeType: AGENT_HOST_BYOK_REASONING_MIME_TYPE, - data: VSBuffer.fromString(JSON.stringify({ - id: item.id, - encryptedContent: item.encryptedContent, - })), - }); - } return { role: ChatMessageRole.Assistant, - content, + content: [{ + type: 'thinking', + value: item.summary, + id: item.id, + metadata: { + ...item.metadata, + ...(item.encryptedContent ? { encrypted_content: item.encryptedContent } : {}), + }, + }], }; } case 'function_call': @@ -293,24 +279,6 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok } } - private _appendEncryptedReasoningOutput(output: IByokLmOutputItem[], data: VSBuffer): void { - const value = JSON.parse(data.toString()) as { id?: unknown; encryptedContent?: unknown }; - if ((value.id !== undefined && typeof value.id !== 'string') || typeof value.encryptedContent !== 'string') { - throw new Error('Invalid Agent Host BYOK reasoning data'); - } - const previous = output.at(-1); - if (previous?.type === 'reasoning' && previous.id === value.id) { - output[output.length - 1] = { ...previous, encryptedContent: value.encryptedContent }; - } else { - output.push({ - type: 'reasoning', - id: value.id, - summary: [], - encryptedContent: value.encryptedContent, - }); - } - } - private _customToolInput(parameters: unknown): string { if (typeof parameters === 'object' && parameters !== null) { const input = Object.getOwnPropertyDescriptor(parameters, 'input')?.value; diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 4ec1e618c19..8ef773aaed3 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -485,6 +485,7 @@ export interface ILanguageModelChatInfoOptions { export interface ILanguageModelChatRequestOptions { readonly modelOptions?: IStringDictionary; readonly configuration?: IStringDictionary; + readonly includeEncryptedThinking?: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly [name: string]: any; } 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 3a54e5347f9..594e30b7dcf 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 @@ -134,8 +134,7 @@ suite('AgentHostByokLmHandler', () => { new Map([['id-acme-claude', byokModel('acme', 'claude')]]), () => responseOf([ { type: 'thinking', value: 'considered ', id: 'rs_1' }, - { type: 'thinking', value: ['options'], id: 'rs_1' }, - { type: 'data', mimeType: 'application/vnd.code.agent-host-byok-reasoning+json', data: VSBuffer.fromString('{"id":"rs_1","encryptedContent":"opaque"}') }, + { type: 'thinking', value: ['options'], id: 'rs_1', metadata: { encrypted_content: 'opaque' } }, { type: 'text', value: 'hello ' }, { type: 'text', value: 'world' }, { type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }, @@ -162,7 +161,7 @@ suite('AgentHostByokLmHandler', () => { assert.strictEqual(service.captured?.modelId, 'id-acme-claude'); assert.deepStrictEqual(result, { output: [ - { type: 'reasoning', id: 'rs_1', summary: ['considered ', 'options'], encryptedContent: 'opaque', metadata: undefined }, + { type: 'reasoning', id: 'rs_1', summary: ['considered ', 'options'], encryptedContent: 'opaque', metadata: { encrypted_content: 'opaque' } }, { type: 'message', content: [{ type: 'text', text: 'hello ' }, { type: 'text', text: 'world' }] }, { type: 'function_call', callId: 't1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }, { type: 'custom_tool_call', callId: 't2', name: 'apply_patch', input: 'patch' }, @@ -218,8 +217,7 @@ suite('AgentHostByokLmHandler', () => { { role: ChatMessageRole.Assistant, content: [ - { type: 'thinking', value: ['thought'], id: 'rs_1', metadata: undefined }, - { type: 'data', mimeType: 'application/vnd.code.agent-host-byok-reasoning+json', data: '{"id":"rs_1","encryptedContent":"opaque"}' }, + { type: 'thinking', value: ['thought'], id: 'rs_1', metadata: { encrypted_content: 'opaque' } }, ], }, { role: ChatMessageRole.Assistant, content: [{ type: 'text', value: 'checking' }] }, @@ -230,7 +228,8 @@ suite('AgentHostByokLmHandler', () => { { role: ChatMessageRole.User, content: [{ type: 'text', value: 'hi' }] }, ], options: { - modelOptions: { temperature: 0.5, _vscodeAgentHostByokReasoningBridge: true }, + modelOptions: { temperature: 0.5 }, + includeEncryptedThinking: true, configuration: { reasoningEffort: 'high' }, tools: [ { name: 'getWeather', description: '', inputSchema: { type: 'object' } }, diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index c200f22f1fb..653dec5cb33 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -24,6 +24,11 @@ declare module 'vscode' { readonly modelConfiguration?: { readonly [key: string]: any; }; + + /** + * Whether encrypted thinking state should be included in the response. + */ + readonly includeEncryptedThinking?: boolean; } /** From 243697fa7024e193332eb0f06d148dd39413cd68 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 30 Jul 2026 11:39:15 -0700 Subject: [PATCH 21/86] agentHost: preserve BYOK reasoning continuation fidelity Keep assistant text complete, group reasoning chunks by ID, and round-trip provider-specific continuation metadata through Responses encrypted content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../vscode-node/languageModelAccessPrompt.tsx | 38 ++++++++-- .../test/languageModelAccessPrompt.spec.ts | 74 +++++++++++++++++++ .../node/copilot/byokResponsesTranslation.ts | 2 +- .../node/byokResponsesTranslation.test.ts | 6 +- .../agentHost/agentHostByokLmHandler.ts | 46 ++++++++++-- .../agentHostByokLmHandler.test.ts | 14 ++-- 6 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccessPrompt.spec.ts diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx index e62f6a0e39b..bb8ee4c7d63 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccessPrompt.tsx @@ -20,6 +20,29 @@ export type Props = PromptElementProps<{ messages: Array; }>; +interface IThinkingGroup { + readonly id: string; + readonly text: string[]; + readonly metadata: Record; +} + +function groupThinkingParts(parts: readonly vscode.LanguageModelThinkingPart[]): IThinkingGroup[] { + const groups = new Map(); + for (const part of parts) { + if (!part.id) { + continue; + } + const previous = groups.get(part.id); + const text = Array.isArray(part.value) ? part.value : [part.value]; + groups.set(part.id, { + id: part.id, + text: [...previous?.text ?? [], ...text], + metadata: { ...previous?.metadata, ...part.metadata }, + }); + } + return [...groups.values()]; +} + export class LanguageModelAccessPrompt extends PromptElement { async render() { @@ -38,18 +61,17 @@ export class LanguageModelAccessPrompt extends PromptElement { const statefulMarkerPart = message.content.find(part => part instanceof vscode.LanguageModelDataPart && part.mimeType === CustomDataPartMimeTypes.StatefulMarker) as vscode.LanguageModelDataPart | undefined; const statefulMarker = statefulMarkerPart && decodeStatefulMarker(statefulMarkerPart.data); const filteredContent = message.content.filter(part => !(part instanceof vscode.LanguageModelDataPart)); - // There should only be one string part per message - const content = filteredContent.find(part => part instanceof LanguageModelTextPart); + const content = filteredContent.filter(part => part instanceof LanguageModelTextPart).map(part => part.value).join(''); const toolCalls = filteredContent.filter(part => part instanceof vscode.LanguageModelToolCallPart); const thinkingParts = filteredContent.filter(part => part instanceof vscode.LanguageModelThinkingPart); - const thinking = thinkingParts.find(part => typeof part.metadata?.encrypted_content === 'string') ?? thinkingParts.at(-1); - const thinkingText = thinkingParts.flatMap(part => Array.isArray(part.value) ? part.value : [part.value]); - const thinkingMetadata = Object.assign({}, ...thinkingParts.map(part => part.metadata)); + const thinkingGroups = groupThinkingParts(thinkingParts); const statefulMarkerElement = statefulMarker && ; - const encrypted = typeof thinkingMetadata.encrypted_content === 'string' ? thinkingMetadata.encrypted_content : undefined; - const thinkingElement = thinking && thinking.id && ; - chatMessages.push( ({ id: tc.callId, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.input) } }))}>{statefulMarkerElement}{content?.value}{thinkingElement}); + const thinkingElements = thinkingGroups.map(group => { + const encrypted = typeof group.metadata.encrypted_content === 'string' ? group.metadata.encrypted_content : undefined; + return ; + }); + chatMessages.push( ({ id: tc.callId, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.input) } }))}>{statefulMarkerElement}{content}{thinkingElements}); } else if (message.role === vscode.LanguageModelChatMessageRole.User) { for (const part of message.content) { if (part instanceof vscode.LanguageModelToolResultPart2 || part instanceof vscode.LanguageModelToolResultPart) { diff --git a/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccessPrompt.spec.ts b/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccessPrompt.spec.ts new file mode 100644 index 00000000000..e38c529784c --- /dev/null +++ b/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccessPrompt.spec.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { describe, expect, 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 { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; +import { LanguageModelChatMessageRole, LanguageModelTextPart, LanguageModelThinkingPart } from '../../../../vscodeTypes'; +import { createExtensionUnitTestingServices } from '../../../test/node/services'; +import { renderPromptElement } from '../../../prompts/node/base/promptRenderer'; +import { LanguageModelAccessPrompt } from '../languageModelAccessPrompt'; + +describe('LanguageModelAccessPrompt', () => { + test('preserves all assistant text and groups thinking by id', async () => { + const services = createExtensionUnitTestingServices(); + services.define(IChatMLFetcher, new StaticChatMLFetcher([])); + const accessor = services.createTestingAccessor(); + const endpoint = accessor.get(IInstantiationService).createInstance(MockEndpoint, 'gpt-5'); + const message = { + role: LanguageModelChatMessageRole.Assistant, + content: [ + new LanguageModelTextPart('first'), + new LanguageModelThinkingPart('a1', 'rs_a', { encrypted_content: 'opaque-a' }), + new LanguageModelThinkingPart('b', 'rs_b', { encrypted_content: 'opaque-b' }), + new LanguageModelThinkingPart('a2', 'rs_a'), + new LanguageModelTextPart('second'), + ], + name: undefined, + }; + + const { messages } = await renderPromptElement( + accessor.get(IInstantiationService), + endpoint, + LanguageModelAccessPrompt, + { noSafety: true, messages: [message] }, + ); + const assistant = messages.find(candidate => candidate.role === Raw.ChatRole.Assistant); + const text = assistant?.content + .filter(part => part.type === Raw.ChatCompletionContentPartKind.Text) + .map(part => part.text) + .join(''); + const thinking = assistant?.content + .filter(part => part.type === Raw.ChatCompletionContentPartKind.Opaque) + .map(part => part.value); + + expect({ text, thinking }).toEqual({ + text: 'firstsecond', + thinking: [ + { + type: 'thinking', + thinking: { + id: 'rs_a', + text: ['a1', 'a2'], + metadata: { encrypted_content: 'opaque-a' }, + encrypted: 'opaque-a', + }, + }, + { + type: 'thinking', + thinking: { + id: 'rs_b', + text: ['b'], + metadata: { encrypted_content: 'opaque-b' }, + encrypted: 'opaque-b', + }, + }, + ], + }); + }); +}); diff --git a/src/vs/platform/agentHost/node/copilot/byokResponsesTranslation.ts b/src/vs/platform/agentHost/node/copilot/byokResponsesTranslation.ts index 5c28e59bec6..acd2767a83a 100644 --- a/src/vs/platform/agentHost/node/copilot/byokResponsesTranslation.ts +++ b/src/vs/platform/agentHost/node/copilot/byokResponsesTranslation.ts @@ -249,7 +249,7 @@ function toResponsesOutputItem(item: IByokLmOutputItem): ResponsesOutputItem { type: 'reasoning', status: 'completed', summary: item.summary.map(text => ({ type: 'summary_text', text })), - encrypted_content: item.id?.startsWith('rs') ? item.encryptedContent ?? null : null, + encrypted_content: item.encryptedContent ?? null, }; case 'function_call': return { diff --git a/src/vs/platform/agentHost/test/node/byokResponsesTranslation.test.ts b/src/vs/platform/agentHost/test/node/byokResponsesTranslation.test.ts index 5f625d131ee..df68148e7bb 100644 --- a/src/vs/platform/agentHost/test/node/byokResponsesTranslation.test.ts +++ b/src/vs/platform/agentHost/test/node/byokResponsesTranslation.test.ts @@ -155,7 +155,7 @@ suite('byokResponsesTranslation', () => { const body = JSON.parse(bridgeResultToResponsesBody({ responseId: 'resp_provider', output: [ - { type: 'reasoning', id: 'rs_1', summary: ['thought'], encryptedContent: 'opaque' }, + { type: 'reasoning', id: 'thinking_1', summary: ['thought'], encryptedContent: 'vscode-reasoning-metadata:{"signature":"sig"}' }, { type: 'message', content: [{ type: 'text', text: 'answer' }] }, ], usage: { inputTokens: 3, outputTokens: 2, reasoningTokens: 1 }, @@ -163,7 +163,7 @@ suite('byokResponsesTranslation', () => { id: string; created_at: number; status: string; - output: Array<{ type: string }>; + output: Array<{ id: string; type: string; encrypted_content?: string | null }>; output_text: string; usage: unknown; }; @@ -193,5 +193,7 @@ suite('byokResponsesTranslation', () => { }, }); assert.deepStrictEqual(body.output.map(item => item.type), ['reasoning', 'message']); + assert.match(body.output[0].id, /^rs_byok_/); + assert.strictEqual(body.output[0].encrypted_content, 'vscode-reasoning-metadata:{"signature":"sig"}'); }); }); 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 e7bad34c7c3..79c85a3c4a9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -27,6 +27,7 @@ import { const STATEFUL_MARKER_MIME_TYPE = 'stateful_marker'; const USAGE_MIME_TYPE = 'usage'; +const REASONING_METADATA_PREFIX = 'vscode-reasoning-metadata:'; /** * Renderer-side {@link IAgentHostByokLmHandler}. Services BYOK chat requests @@ -182,7 +183,16 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok }); } for (const item of request.input) { - messages.push(this._toChatMessage(item)); + const message = this._toChatMessage(item); + const previous = messages.at(-1); + if (message.role === ChatMessageRole.Assistant && previous?.role === ChatMessageRole.Assistant) { + messages[messages.length - 1] = { + ...previous, + content: [...previous.content, ...message.content], + }; + } else { + messages.push(message); + } } return messages; } @@ -192,7 +202,7 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok case 'message': return { role: this._toChatRole(item.role), - content: item.content.map(part => ({ type: 'text', value: part.text })), + content: [{ type: 'text', value: item.content.map(part => part.text).join('') }], }; case 'reasoning': { return { @@ -203,7 +213,7 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok id: item.id, metadata: { ...item.metadata, - ...(item.encryptedContent ? { encrypted_content: item.encryptedContent } : {}), + ...(item.encryptedContent ? this._decodeReasoningMetadata(item.encryptedContent) : {}), }, }], }; @@ -246,7 +256,7 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok if (previous?.type === 'message') { output[output.length - 1] = { ...previous, - content: [...previous.content, { type: 'text', text: value }], + content: [{ type: 'text', text: previous.content.map(part => part.text).join('') + value }], }; } else { output.push({ type: 'message', content: [{ type: 'text', text: value }] }); @@ -258,7 +268,7 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok return; } const summary = Array.isArray(part.value) ? part.value : [part.value]; - const encryptedContent = this._stringMetadata(part.metadata, 'encrypted_content') ?? this._stringMetadata(part.metadata, 'encrypted'); + const encryptedContent = this._encodeReasoningMetadata(part.metadata); const reasoning: IByokLmReasoningItem = { type: 'reasoning', id: part.id, @@ -279,6 +289,32 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok } } + private _encodeReasoningMetadata(metadata: Readonly> | undefined): string | undefined { + const encryptedContent = this._stringMetadata(metadata, 'encrypted_content') ?? this._stringMetadata(metadata, 'encrypted'); + if (encryptedContent) { + return encryptedContent; + } + const continuationMetadata = { + ...(this._stringMetadata(metadata, 'signature') ? { signature: this._stringMetadata(metadata, 'signature') } : {}), + ...(this._stringMetadata(metadata, '_completeThinking') ? { _completeThinking: this._stringMetadata(metadata, '_completeThinking') } : {}), + ...(this._stringMetadata(metadata, 'redactedData') ? { redactedData: this._stringMetadata(metadata, 'redactedData') } : {}), + }; + return Object.keys(continuationMetadata).length > 0 + ? `${REASONING_METADATA_PREFIX}${JSON.stringify(continuationMetadata)}` + : undefined; + } + + private _decodeReasoningMetadata(value: string): Record { + if (!value.startsWith(REASONING_METADATA_PREFIX)) { + return { encrypted_content: value }; + } + const metadata = JSON.parse(value.slice(REASONING_METADATA_PREFIX.length)); + if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) { + throw new Error('Invalid Agent Host BYOK reasoning metadata'); + } + return metadata as Record; + } + private _customToolInput(parameters: unknown): string { if (typeof parameters === 'object' && parameters !== null) { const input = Object.getOwnPropertyDescriptor(parameters, 'input')?.value; 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 594e30b7dcf..72293a054df 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 @@ -135,6 +135,7 @@ suite('AgentHostByokLmHandler', () => { () => responseOf([ { type: 'thinking', value: 'considered ', id: 'rs_1' }, { type: 'thinking', value: ['options'], id: 'rs_1', metadata: { encrypted_content: 'opaque' } }, + { type: 'thinking', value: '', id: 'thinking_2', metadata: { signature: 'sig', _completeThinking: 'full thought' } }, { type: 'text', value: 'hello ' }, { type: 'text', value: 'world' }, { type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }, @@ -162,7 +163,8 @@ suite('AgentHostByokLmHandler', () => { assert.deepStrictEqual(result, { output: [ { type: 'reasoning', id: 'rs_1', summary: ['considered ', 'options'], encryptedContent: 'opaque', metadata: { encrypted_content: 'opaque' } }, - { type: 'message', content: [{ type: 'text', text: 'hello ' }, { type: 'text', text: 'world' }] }, + { type: 'reasoning', id: 'thinking_2', summary: [''], encryptedContent: 'vscode-reasoning-metadata:{"signature":"sig","_completeThinking":"full thought"}', metadata: { signature: 'sig', _completeThinking: 'full thought' } }, + { type: 'message', content: [{ type: 'text', text: 'hello world' }] }, { type: 'function_call', callId: 't1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }, { type: 'custom_tool_call', callId: 't2', name: 'apply_patch', input: 'patch' }, ], @@ -192,7 +194,8 @@ suite('AgentHostByokLmHandler', () => { ], input: [ { type: 'reasoning', id: 'rs_1', summary: ['thought'], encryptedContent: 'opaque' }, - { type: 'message', role: 'assistant', content: [{ type: 'text', text: 'checking' }] }, + { type: 'reasoning', id: 'rs_2', summary: ['other thought'], encryptedContent: 'vscode-reasoning-metadata:{"signature":"sig-2","_completeThinking":"other complete thought"}' }, + { type: 'message', role: 'assistant', content: [{ type: 'text', text: 'check' }, { type: 'text', text: 'ing' }] }, { type: 'function_call', callId: 't1', name: 'getWeather', argumentsJson: '{"city":"NYC"}' }, { type: 'custom_tool_call', callId: 't2', name: 'apply_patch', input: 'patch' }, { type: 'function_call_output', callId: 't1', output: 'sunny' }, @@ -218,11 +221,12 @@ suite('AgentHostByokLmHandler', () => { role: ChatMessageRole.Assistant, content: [ { type: 'thinking', value: ['thought'], id: 'rs_1', metadata: { encrypted_content: 'opaque' } }, + { type: 'thinking', value: ['other thought'], id: 'rs_2', metadata: { signature: 'sig-2', _completeThinking: 'other complete thought' } }, + { type: 'text', value: 'checking' }, + { type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }, + { type: 'tool_use', name: 'apply_patch', toolCallId: 't2', parameters: { input: 'patch' } }, ], }, - { role: ChatMessageRole.Assistant, content: [{ type: 'text', value: 'checking' }] }, - { role: ChatMessageRole.Assistant, content: [{ type: 'tool_use', name: 'getWeather', toolCallId: 't1', parameters: { city: 'NYC' } }] }, - { role: ChatMessageRole.Assistant, content: [{ type: 'tool_use', name: 'apply_patch', toolCallId: 't2', parameters: { input: 'patch' } }] }, { role: ChatMessageRole.User, content: [{ type: 'tool_result', toolCallId: 't1', value: [{ type: 'text', value: 'sunny' }] }] }, { role: ChatMessageRole.User, content: [{ type: 'tool_result', toolCallId: 't2', value: [{ type: 'text', value: 'Done!' }] }] }, { role: ChatMessageRole.User, content: [{ type: 'text', value: 'hi' }] }, From 3622f412eaefd56d9aaa98ff2ba34f9d4e59de81 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 30 Jul 2026 12:23:39 -0700 Subject: [PATCH 22/86] Enable conditional execution of Copilot BYOK Responses integration tests based on REAL_SDK environment variable --- .../copilotByokResponses.integrationTest.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/agentHost/test/node/providerIntegration/copilotByokResponses.integrationTest.ts b/src/vs/platform/agentHost/test/node/providerIntegration/copilotByokResponses.integrationTest.ts index 80e86f407d9..dbc000188cb 100644 --- a/src/vs/platform/agentHost/test/node/providerIntegration/copilotByokResponses.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/providerIntegration/copilotByokResponses.integrationTest.ts @@ -14,7 +14,9 @@ import type { IByokLmChatRequest, IByokLmModelInfo } from '../../../common/agent import { ByokLmBridgeRegistry } from '../../../node/byokLmBridgeRegistry.js'; import { ByokLmProxyService } from '../../../node/copilot/byokLmProxyService.js'; -suite('Agent Host Provider Integration - Copilot BYOK Responses', function () { +const REAL_SDK_ENABLED = process.env['AGENT_HOST_REAL_SDK'] === '1'; + +(REAL_SDK_ENABLED ? suite : suite.skip)('Agent Host Provider Integration - Copilot BYOK Responses', function () { const store = ensureNoDisposablesAreLeakedInTestSuite(); From d81b554dff2a72ff07019f67aa4bb597f7c8d664 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 30 Jul 2026 15:37:58 -0400 Subject: [PATCH 23/86] Fix pricing category possibly crashing model picker (#328263) --- .../widget/input/modelPicker/modelPickerPresentation.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelPickerPresentation.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelPickerPresentation.ts index f9cfa11a805..429999ea3f6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelPickerPresentation.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelPickerPresentation.ts @@ -15,10 +15,11 @@ export function isMultiplierPricing(model: ILanguageModelChatMetadataAndIdentifi } export function getPriceCategoryLabel(priceCategory: string | undefined): string | undefined { + // The value originates from extension provided metadata, so it may not be a string at runtime + if (typeof priceCategory !== 'string' || priceCategory.length === 0) { + return undefined; + } switch (priceCategory) { - case undefined: - case '': - return undefined; case 'low': return localize('chat.priceCategory.low', "Low cost"); case 'medium': From 522933430613240a1bc794bf8dfbd1d163abe6ce Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 30 Jul 2026 13:16:54 -0700 Subject: [PATCH 24/86] Fix Agent Host draft loss when switching sessions (#328264) * agentHost: preserve drafts when switching sessions Flush pending debounced input state when a chat session is disposed so switching away cannot lose recently typed draft text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: refine draft disposal flush Read the final input state directly during disposal and keep the regression test independent of fake timers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/agentHostSessionHandler.ts | 45 ++++++++++--------- .../agentHostChatContribution.test.ts | 37 +++++++++++++++ 2 files changed, 62 insertions(+), 20 deletions(-) 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 23c4ed76b87..5d12dbe508c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts @@ -4284,28 +4284,29 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC // (each streaming delta), not just draft changes. let lastRemoteDraft = syncedDraft; let appliedRemoteDraft: Message | undefined; + const syncDraft = (state: IChatModelInputState | undefined): void => { + if (state?.origin === ChatInputStateOrigin.Remote) { + return; + } + const draft = this._inputStateToDraft(sessionResource, state); + if (equals(syncedDraft, draft)) { + return; + } + if (appliedRemoteDraft && sameDraftUserContent(draft, appliedRemoteDraft)) { + syncedDraft = draft; + return; + } + appliedRemoteDraft = undefined; + syncedDraft = draft; + + this._config.connection.dispatch(chatKey, { + type: ActionType.ChatDraftChanged, + draft, + }); + }; store.add(autorun(reader => { const state = inputModel.state.read(reader); - delayer.trigger(() => { - if (state?.origin === ChatInputStateOrigin.Remote) { - return; - } - const draft = this._inputStateToDraft(sessionResource, state); - if (equals(syncedDraft, draft)) { - return; - } - if (appliedRemoteDraft && sameDraftUserContent(draft, appliedRemoteDraft)) { - syncedDraft = draft; - return; - } - appliedRemoteDraft = undefined; - syncedDraft = draft; - - this._config.connection.dispatch(chatKey, { - type: ActionType.ChatDraftChanged, - draft, - }); - }).catch(() => { /* delayer disposed */ }); + delayer.trigger(() => syncDraft(state)).catch(() => { /* delayer disposed */ }); })); store.add(chatSubscription.onDidChange(() => { const remoteDraft = readRemoteDraft(); @@ -4325,6 +4326,10 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC appliedRemoteDraft = remoteDraft; this._applyRemoteDraft(inputModel, sessionResource, remoteDraft); })); + store.add(toDisposable(() => { + delayer.cancel(); + syncDraft(inputModel.state.get()); + })); } /** Applies a remote draft without replacing local input state the protocol does not carry. */ diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts index 4aded1ff042..babbed5ca99 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts @@ -1981,6 +1981,43 @@ suite('AgentHostChatContribution', () => { })); + test('flushes pending chat input draft when the session is disposed', async () => { + const { sessionHandler, agentHostService, chatService } = createContribution(disposables); + const backendSession = AgentSession.uri('copilot', 'draft-sync-dispose'); + const sessionResource = URI.from({ scheme: 'agent-host-copilot', path: '/draft-sync-dispose' }); + seedDraftSession(agentHostService, backendSession, 'Draft Sync Dispose'); + const { inputModel } = createDraftInputModel({ + attachments: [], + mode: { id: 'agent', kind: ChatModeKind.Agent }, + selectedModel: undefined, + inputText: '', + selections: [], + contrib: {}, + }); + chatService.setSession(sessionResource, upcastPartial({ + sessionResource, + inputModel, + onDidChangePendingRequests: Event.None, + getPendingRequests: () => [], + })); + const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None); + agentHostService.dispatchedActions.length = 0; + inputModel.setState({ inputText: 'typed before switching away' }); + chatSession.dispose(); + chatSession.dispose(); + + assert.deepStrictEqual(agentHostService.dispatchedActions.map(d => ({ channel: d.channel, action: d.action })), [{ + channel: buildDefaultChatUri(backendSession.toString()), + action: { + type: ActionType.ChatDraftChanged, + draft: { + text: 'typed before switching away', + origin: { kind: MessageKind.User }, + }, + }, + }]); + }); + test('applies a remote draft to a clean live input', async () => { const modelMetadata = upcastPartial({ id: 'opus-4.7', name: 'Opus 4.7' }); const languageModels = new Map([ From 31308e8dceb6b46524ae26d9a7bb31c5f4d31632 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:32:44 -0700 Subject: [PATCH 25/86] bump github/copilot 1.0.76 + SDK 1.0.9-preview.0 (#328161) * Try bump github/copilot 1.0.76 + SDK 1.0.9-preview.0 * Exclude Copilot mediaremote-adapter from product packaging CLI 1.0.76 ships a nested macOS MediaRemote helper under prebuilds/*/mediaremote-adapter that broke darwin universal merge. Strip it from app/runtime packaging and built-in extension materialization; voice media-pause detection degrades without it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 255220ee-f851-47ce-a71d-45db46de336d --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 255220ee-f851-47ce-a71d-45db46de336d --- build/lib/copilot.ts | 10 ++- build/lib/test/copilot.test.ts | 10 +++ package-lock.json | 82 +++++++++---------- package.json | 4 +- remote/package-lock.json | 82 +++++++++---------- remote/package.json | 4 +- .../node/copilot/copilotSystemNotification.ts | 7 ++ .../test/node/copilotAgentSession.test.ts | 11 +++ 8 files changed, 123 insertions(+), 87 deletions(-) diff --git a/build/lib/copilot.ts b/build/lib/copilot.ts index 412e862d7dc..c24662a0f08 100644 --- a/build/lib/copilot.ts +++ b/build/lib/copilot.ts @@ -116,6 +116,10 @@ function getCopilotOptionalNativePayloadFiles(platform: string): string[] { 'prebuilds/*/Copilot Computer Use.app/**', 'prebuilds/*/CopilotComputerUse.exe', 'prebuilds/*/keytar.node', + // macOS voice media-pause helper (MediaRemote adapter). Optional and + // nested under prebuilds; keep it out of the product so universal + // merge does not need to special-case the framework binary tree. + 'prebuilds/*/mediaremote-adapter/**', ]; if (platform !== 'win32') { @@ -304,11 +308,15 @@ function materializeBuiltInCopilotSdkPlatformFiles(copilotPackagePlatformArch: s const extVersion = readCopilotPackageVersion(copilotBase); const { dir: platformPackageDir, cleanup } = resolveVersionMatchedCopilotPlatformPackage(copilotPackagePlatformArch, extVersion, appNodeModulesDir, options); try { + const sdkPrebuildsTarget = path.join(copilotBase, 'sdk', 'prebuilds', copilotPackagePlatformArch); copyRequiredDirectory( path.join(platformPackageDir, 'prebuilds', copilotPackagePlatformArch), - path.join(copilotBase, 'sdk', 'prebuilds', copilotPackagePlatformArch), + sdkPrebuildsTarget, `Copilot SDK native prebuilds for ${copilotPackagePlatformArch}` ); + // Built-in materialization copies the whole prebuilds tree (not the gulp + // exclude globs above), so drop mediaremote-adapter explicitly afterward. + fs.rmSync(path.join(sdkPrebuildsTarget, 'mediaremote-adapter'), { recursive: true, force: true }); if (!copilotTgrepPlatforms.includes(tgrepPlatformArch)) { return; diff --git a/build/lib/test/copilot.test.ts b/build/lib/test/copilot.test.ts index 043ed3c45bc..870cced4041 100644 --- a/build/lib/test/copilot.test.ts +++ b/build/lib/test/copilot.test.ts @@ -50,6 +50,7 @@ suite('copilot', () => { '!node_modules/@github/copilot-linux-x64/prebuilds/*/Copilot Computer Use.app/**', '!node_modules/@github/copilot-linux-x64/prebuilds/*/CopilotComputerUse.exe', '!node_modules/@github/copilot-linux-x64/prebuilds/*/keytar.node', + '!node_modules/@github/copilot-linux-x64/prebuilds/*/mediaremote-adapter/**', '!node_modules/@github/copilot-linux-x64/prebuilds/*/cli-native.node', ]); assertCopilotPlatformPackageIncludes(files, 'node_modules/@github/copilot-linux-x64', [ @@ -80,6 +81,7 @@ suite('copilot', () => { '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/Copilot Computer Use.app/**', '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/CopilotComputerUse.exe', '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/keytar.node', + '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/mediaremote-adapter/**', '!node_modules/@github/copilot-linuxmusl-x64/prebuilds/*/cli-native.node', ]); assertCopilotPlatformPackageIncludes(files, 'node_modules/@github/copilot-linuxmusl-x64', [ @@ -107,6 +109,7 @@ suite('copilot', () => { '!node_modules/@github/copilot-win32-x64/prebuilds/*/Copilot Computer Use.app/**', '!node_modules/@github/copilot-win32-x64/prebuilds/*/CopilotComputerUse.exe', '!node_modules/@github/copilot-win32-x64/prebuilds/*/keytar.node', + '!node_modules/@github/copilot-win32-x64/prebuilds/*/mediaremote-adapter/**', ]); assertCopilotPlatformPackageIncludes(getCopilotRuntimePrebuildFiles('win32', 'x64'), 'node_modules/@github/copilot-win32-x64', [ 'index.js', @@ -135,6 +138,7 @@ suite('copilot', () => { '!node_modules/@github/copilot-win32-arm64/prebuilds/*/Copilot Computer Use.app/**', '!node_modules/@github/copilot-win32-arm64/prebuilds/*/CopilotComputerUse.exe', '!node_modules/@github/copilot-win32-arm64/prebuilds/*/keytar.node', + '!node_modules/@github/copilot-win32-arm64/prebuilds/*/mediaremote-adapter/**', ]); assertOptionalCopilotNativeDependenciesExcluded(getCopilotRuntimePrebuildFiles('win32', 'x64'), 'node_modules/@github/copilot-win32-x64'); assertCopilotStandaloneExecutableExcluded(getCopilotRuntimePrebuildFiles('win32', 'arm64'), 'node_modules/@github/copilot-win32-arm64'); @@ -209,9 +213,12 @@ suite('copilot', () => { fs.writeFileSync(path.join(extensionCopilotDir, 'package.json'), JSON.stringify({ version: '1.0.73' })); fs.mkdirSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty'), { recursive: true }); fs.writeFileSync(path.join(platformPackageDir, 'package.json'), JSON.stringify({ version: '1.0.73' })); + fs.mkdirSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'mediaremote-adapter', 'MediaRemoteAdapter.framework'), { recursive: true }); fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'runtime.node'), ''); fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty.node'), ''); fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'conpty', 'OpenConsole.exe'), ''); + fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'mediaremote-adapter', 'mediaremote-adapter.pl'), ''); + fs.writeFileSync(path.join(platformPackageDir, 'prebuilds', 'win32-x64', 'mediaremote-adapter', 'MediaRemoteAdapter.framework', 'MediaRemoteAdapter'), ''); fs.mkdirSync(path.join(platformPackageDir, 'tgrep', 'bin', 'win32-x64'), { recursive: true }); fs.writeFileSync(path.join(platformPackageDir, 'tgrep', 'bin', 'win32-x64', 'tgrep.exe'), ''); fs.mkdirSync(path.join(appNodeModulesDir, '@vscode', 'ripgrep-universal', 'bin', 'win32-x64'), { recursive: true }); @@ -222,6 +229,7 @@ suite('copilot', () => { assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'runtime.node'))); assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'conpty.node'))); assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'conpty', 'OpenConsole.exe'))); + assert(!fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'win32-x64', 'mediaremote-adapter'))); assert(!fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'prebuilds', 'linux-x64'))); assert(fs.existsSync(path.join(extensionCopilotDir, 'tgrep', 'bin', 'win32-x64', 'tgrep.exe'))); assert(fs.existsSync(path.join(extensionCopilotDir, 'sdk', 'tgrep', 'bin', 'win32-x64', 'tgrep.exe'))); @@ -384,6 +392,8 @@ function assertOptionalCopilotNativeDependenciesExcluded(patterns: string[], pac assert(!matchesGlob(`${packageDir}/prebuilds/win32-x64/CopilotComputerUse.exe`, patterns), 'CopilotComputerUse.exe'); assert(patterns.includes(`!${packageDir}/prebuilds/*/keytar.node`), 'keytar.node'); assert(!matchesGlob(`${packageDir}/prebuilds/linux-x64/keytar.node`, patterns), 'keytar.node'); + assert(patterns.includes(`!${packageDir}/prebuilds/*/mediaremote-adapter/**`), 'mediaremote-adapter'); + assert(!matchesGlob(`${packageDir}/prebuilds/darwin-arm64/mediaremote-adapter/MediaRemoteAdapter.framework/MediaRemoteAdapter`, patterns), 'mediaremote-adapter'); if (!packageDir.includes('win32')) { assert(patterns.includes(`!${packageDir}/prebuilds/*/cli-native.node`), 'cli-native.node'); diff --git a/package-lock.json b/package-lock.json index 6ab302055e3..e47d53e34cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.82.0", - "@github/copilot": "^1.0.73", - "@github/copilot-sdk": "^1.0.8", + "@github/copilot": "^1.0.76", + "@github/copilot-sdk": "^1.0.9-preview.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/dev-tunnels-connections": "^1.3.41", @@ -1113,9 +1113,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.73.tgz", - "integrity": "sha512-8I2Ejg2CX/PQA3c2H8W1zuqhniCeR1q1/bD8CrV53/ZLw8GF7DAV0xQpwa8ELYvFgjXb6AADojafCKwdbVef+A==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.76.tgz", + "integrity": "sha512-5aP3y9lTTGEx0JeaCnNLlHU0Y+pgq/FS74R6Q6VniOBya8jqv2hulraWNN1sOhGD7CyLcOeOPSIrNTANcQiM5A==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -1124,20 +1124,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.73", - "@github/copilot-darwin-x64": "1.0.73", - "@github/copilot-linux-arm64": "1.0.73", - "@github/copilot-linux-x64": "1.0.73", - "@github/copilot-linuxmusl-arm64": "1.0.73", - "@github/copilot-linuxmusl-x64": "1.0.73", - "@github/copilot-win32-arm64": "1.0.73", - "@github/copilot-win32-x64": "1.0.73" + "@github/copilot-darwin-arm64": "1.0.76", + "@github/copilot-darwin-x64": "1.0.76", + "@github/copilot-linux-arm64": "1.0.76", + "@github/copilot-linux-x64": "1.0.76", + "@github/copilot-linuxmusl-arm64": "1.0.76", + "@github/copilot-linuxmusl-x64": "1.0.76", + "@github/copilot-win32-arm64": "1.0.76", + "@github/copilot-win32-x64": "1.0.76" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.73.tgz", - "integrity": "sha512-5jv7t2sw35/zI0cPze38hG6239NT5/q/Emjx6gLibYkolDqMDJjpm17Ps7tc8oafUEOiMQMb+ar7+qi6rSiGJA==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.76.tgz", + "integrity": "sha512-A0Izj4xZRm4syCaHXcAdHXF1IDuwLGCQiDdriGhennvGbGck5Ku+cDbLEgoBGb6Eqk2VcToV0Aik5YQrAlfRlw==", "cpu": [ "arm64" ], @@ -1151,9 +1151,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.73.tgz", - "integrity": "sha512-l794k6Ahb11AG2FQT/P4TEWxWblzM1h8aQQCzG8jBWp8dfwjhyYjJ+d+0CWQzM3Fc1ddNUZRjKXCUsfvFjiZhQ==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.76.tgz", + "integrity": "sha512-F/I+F6oLBvKoSjxgRLytjxyRk/e+Zi01dsE9KT95qg29ntdAM2MGplrpmmk08eQly7IyfJT2eZcfceOHZFPvUQ==", "cpu": [ "x64" ], @@ -1167,9 +1167,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.73.tgz", - "integrity": "sha512-Zu0W5nupJjNeem0brqU/pG+VY0IWr6EWr/FsC90g5SEDiaM4VhVNVWcz8t0E3DQCSYetV6IBaNMtjs/3uIIiDQ==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.76.tgz", + "integrity": "sha512-gI0ZdgIcL5bMj3yM25GIlfw1pIIZAYwMux/gSb406OlAOcBlkssLgcioltZV1ifHe/344FsO+BhZ5zAS6tEn7g==", "cpu": [ "arm64" ], @@ -1186,9 +1186,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.73.tgz", - "integrity": "sha512-k33XIr6/PVp+K+5F/zv3No4PPaNImvHz73mcbIw63oxh5iiacXjgr0WqbBIS5s/rkhOWjNPIkbof/TTPZ7mQjA==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.76.tgz", + "integrity": "sha512-mZXoaiOW6SZD++YEonprGLsesxRFiUQme1K17Q7x7jycD4FMgexGFPOzF6KwfxnPBIPwQu/RXvz1VUPYY3n3DQ==", "cpu": [ "x64" ], @@ -1205,9 +1205,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.73.tgz", - "integrity": "sha512-HJWzhfD3oaiIgfRAHkNWzp17fELtshqM9HVN5n+lFEmSO2EETCEh0P1lhJc4m+FYfXSJnL0raAqVuyaNMuPoPw==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.76.tgz", + "integrity": "sha512-6r9IsqQZfWvGOl5viz0nXLjHw4WMpITkkOv0odaIhENTWk28yVI/nXGLdI/FXKA0MLhcW2odNVc2yL3Z3igDWA==", "cpu": [ "arm64" ], @@ -1224,9 +1224,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.73.tgz", - "integrity": "sha512-/BpOXSb16wHEu8I1SaKiLszQ4Kvu4+Z4uCn7W0bv4xI4fPZwTEG0u3zgaI2W9Ao3+aBl0XRpPmpWzE9ziYEq+w==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.76.tgz", + "integrity": "sha512-YHpphnuSRu/T0fYoFVIu6AeutmMWPiOqDo9Fk4WKtPOT4Sn69SZbI1LLlbnk0JzU4QhjpH0CWQyuHCc6yRDgAg==", "cpu": [ "x64" ], @@ -1243,12 +1243,12 @@ } }, "node_modules/@github/copilot-sdk": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.8.tgz", - "integrity": "sha512-dbahVsyt2aX8qqtOOtmYNe40MnvzSvOSHYFFgoFK7gHZSTNz9QgOht8b1sCCJlcXaFAn/w+5qNc7CwWoCjpQ0g==", + "version": "1.0.9-preview.0", + "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.9-preview.0.tgz", + "integrity": "sha512-0k8GHW0ix1e5MtoHp797f75Xxea4WLp1LE3cB1J6vcEXf5fl57qgGICZEw6w5boYahbthYTW48+P85ILd7upTQ==", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.73", + "@github/copilot": "^1.0.76-5", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -1267,9 +1267,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.73.tgz", - "integrity": "sha512-DbPeXiYzQjpOy9oboaBvuCzjRwfcL987c3bG09cK1crdCDrKfkTJ7NXpcp1KWRPIRFO1FQm1qToNE89J+L3uvg==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.76.tgz", + "integrity": "sha512-c4FJP/7TV3qiGeSXFVC3dtNIC2D2awq6XSC1FTYkyBWVZSG8ZByWfweltUlB//iyzvHmVoHeUfu6r8E6utp1sQ==", "cpu": [ "arm64" ], @@ -1283,9 +1283,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.73.tgz", - "integrity": "sha512-8D3E1l5i+N5Eq8HIOQpx+Zbcb3MXdFxszksM2gqq175Z1S7Zna67oY4GoR3psxlbIpSyHKiLEBWYiaps6ayHWw==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.76.tgz", + "integrity": "sha512-twVo1UnnIYx77NF9E7qYKWRuo6IX0UOEIT+8ZIF4FO/9uEoPRDUX+C5MLkHFufDROr/bW/dhVRbZocJ1Rwy7Ew==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index b8a8952ef4a..6603d8bc5a0 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,8 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.82.0", - "@github/copilot": "^1.0.73", - "@github/copilot-sdk": "^1.0.8", + "@github/copilot": "^1.0.76", + "@github/copilot-sdk": "^1.0.9-preview.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/dev-tunnels-connections": "^1.3.41", diff --git a/remote/package-lock.json b/remote/package-lock.json index 840bc7a80b2..a1997fa4473 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -8,8 +8,8 @@ "name": "vscode-reh", "version": "0.0.0", "dependencies": { - "@github/copilot": "^1.0.73", - "@github/copilot-sdk": "^1.0.8", + "@github/copilot": "^1.0.76", + "@github/copilot-sdk": "^1.0.9-preview.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/mxc-sdk": "0.6.1", @@ -61,9 +61,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.73.tgz", - "integrity": "sha512-8I2Ejg2CX/PQA3c2H8W1zuqhniCeR1q1/bD8CrV53/ZLw8GF7DAV0xQpwa8ELYvFgjXb6AADojafCKwdbVef+A==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.76.tgz", + "integrity": "sha512-5aP3y9lTTGEx0JeaCnNLlHU0Y+pgq/FS74R6Q6VniOBya8jqv2hulraWNN1sOhGD7CyLcOeOPSIrNTANcQiM5A==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -72,20 +72,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.73", - "@github/copilot-darwin-x64": "1.0.73", - "@github/copilot-linux-arm64": "1.0.73", - "@github/copilot-linux-x64": "1.0.73", - "@github/copilot-linuxmusl-arm64": "1.0.73", - "@github/copilot-linuxmusl-x64": "1.0.73", - "@github/copilot-win32-arm64": "1.0.73", - "@github/copilot-win32-x64": "1.0.73" + "@github/copilot-darwin-arm64": "1.0.76", + "@github/copilot-darwin-x64": "1.0.76", + "@github/copilot-linux-arm64": "1.0.76", + "@github/copilot-linux-x64": "1.0.76", + "@github/copilot-linuxmusl-arm64": "1.0.76", + "@github/copilot-linuxmusl-x64": "1.0.76", + "@github/copilot-win32-arm64": "1.0.76", + "@github/copilot-win32-x64": "1.0.76" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.73.tgz", - "integrity": "sha512-5jv7t2sw35/zI0cPze38hG6239NT5/q/Emjx6gLibYkolDqMDJjpm17Ps7tc8oafUEOiMQMb+ar7+qi6rSiGJA==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.76.tgz", + "integrity": "sha512-A0Izj4xZRm4syCaHXcAdHXF1IDuwLGCQiDdriGhennvGbGck5Ku+cDbLEgoBGb6Eqk2VcToV0Aik5YQrAlfRlw==", "cpu": [ "arm64" ], @@ -99,9 +99,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.73.tgz", - "integrity": "sha512-l794k6Ahb11AG2FQT/P4TEWxWblzM1h8aQQCzG8jBWp8dfwjhyYjJ+d+0CWQzM3Fc1ddNUZRjKXCUsfvFjiZhQ==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.76.tgz", + "integrity": "sha512-F/I+F6oLBvKoSjxgRLytjxyRk/e+Zi01dsE9KT95qg29ntdAM2MGplrpmmk08eQly7IyfJT2eZcfceOHZFPvUQ==", "cpu": [ "x64" ], @@ -115,9 +115,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.73.tgz", - "integrity": "sha512-Zu0W5nupJjNeem0brqU/pG+VY0IWr6EWr/FsC90g5SEDiaM4VhVNVWcz8t0E3DQCSYetV6IBaNMtjs/3uIIiDQ==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.76.tgz", + "integrity": "sha512-gI0ZdgIcL5bMj3yM25GIlfw1pIIZAYwMux/gSb406OlAOcBlkssLgcioltZV1ifHe/344FsO+BhZ5zAS6tEn7g==", "cpu": [ "arm64" ], @@ -134,9 +134,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.73.tgz", - "integrity": "sha512-k33XIr6/PVp+K+5F/zv3No4PPaNImvHz73mcbIw63oxh5iiacXjgr0WqbBIS5s/rkhOWjNPIkbof/TTPZ7mQjA==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.76.tgz", + "integrity": "sha512-mZXoaiOW6SZD++YEonprGLsesxRFiUQme1K17Q7x7jycD4FMgexGFPOzF6KwfxnPBIPwQu/RXvz1VUPYY3n3DQ==", "cpu": [ "x64" ], @@ -153,9 +153,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.73.tgz", - "integrity": "sha512-HJWzhfD3oaiIgfRAHkNWzp17fELtshqM9HVN5n+lFEmSO2EETCEh0P1lhJc4m+FYfXSJnL0raAqVuyaNMuPoPw==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.76.tgz", + "integrity": "sha512-6r9IsqQZfWvGOl5viz0nXLjHw4WMpITkkOv0odaIhENTWk28yVI/nXGLdI/FXKA0MLhcW2odNVc2yL3Z3igDWA==", "cpu": [ "arm64" ], @@ -172,9 +172,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.73.tgz", - "integrity": "sha512-/BpOXSb16wHEu8I1SaKiLszQ4Kvu4+Z4uCn7W0bv4xI4fPZwTEG0u3zgaI2W9Ao3+aBl0XRpPmpWzE9ziYEq+w==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.76.tgz", + "integrity": "sha512-YHpphnuSRu/T0fYoFVIu6AeutmMWPiOqDo9Fk4WKtPOT4Sn69SZbI1LLlbnk0JzU4QhjpH0CWQyuHCc6yRDgAg==", "cpu": [ "x64" ], @@ -191,12 +191,12 @@ } }, "node_modules/@github/copilot-sdk": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.8.tgz", - "integrity": "sha512-dbahVsyt2aX8qqtOOtmYNe40MnvzSvOSHYFFgoFK7gHZSTNz9QgOht8b1sCCJlcXaFAn/w+5qNc7CwWoCjpQ0g==", + "version": "1.0.9-preview.0", + "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.9-preview.0.tgz", + "integrity": "sha512-0k8GHW0ix1e5MtoHp797f75Xxea4WLp1LE3cB1J6vcEXf5fl57qgGICZEw6w5boYahbthYTW48+P85ILd7upTQ==", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.73", + "@github/copilot": "^1.0.76-5", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -215,9 +215,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.73.tgz", - "integrity": "sha512-DbPeXiYzQjpOy9oboaBvuCzjRwfcL987c3bG09cK1crdCDrKfkTJ7NXpcp1KWRPIRFO1FQm1qToNE89J+L3uvg==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.76.tgz", + "integrity": "sha512-c4FJP/7TV3qiGeSXFVC3dtNIC2D2awq6XSC1FTYkyBWVZSG8ZByWfweltUlB//iyzvHmVoHeUfu6r8E6utp1sQ==", "cpu": [ "arm64" ], @@ -231,9 +231,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.73", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.73.tgz", - "integrity": "sha512-8D3E1l5i+N5Eq8HIOQpx+Zbcb3MXdFxszksM2gqq175Z1S7Zna67oY4GoR3psxlbIpSyHKiLEBWYiaps6ayHWw==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.76.tgz", + "integrity": "sha512-twVo1UnnIYx77NF9E7qYKWRuo6IX0UOEIT+8ZIF4FO/9uEoPRDUX+C5MLkHFufDROr/bW/dhVRbZocJ1Rwy7Ew==", "cpu": [ "x64" ], diff --git a/remote/package.json b/remote/package.json index 04951a553f7..8d0bbc6280c 100644 --- a/remote/package.json +++ b/remote/package.json @@ -3,8 +3,8 @@ "version": "0.0.0", "private": true, "dependencies": { - "@github/copilot": "^1.0.73", - "@github/copilot-sdk": "^1.0.8", + "@github/copilot": "^1.0.76", + "@github/copilot-sdk": "^1.0.9-preview.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/mxc-sdk": "0.6.1", diff --git a/src/vs/platform/agentHost/node/copilot/copilotSystemNotification.ts b/src/vs/platform/agentHost/node/copilot/copilotSystemNotification.ts index 6cb9dfc6d0a..56653c579f0 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotSystemNotification.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotSystemNotification.ts @@ -55,6 +55,13 @@ export function buildCopilotSystemNotification(event: SessionEventPayload<'syste messageText: localize('agentHost.copilot.systemNotification.instructionDiscovered', "Instruction discovered: {0}", kind.description ?? kind.sourcePath), startsTurn: false, }; + case 'unclassified': + // External-host notifications that do not match a runtime-owned kind. + // Use the cleaned content and wake the agent when idle. + return { + messageText: content, + startsTurn: true, + }; default: softAssertNever(kind); return undefined; diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index a7212ba4e20..91a78a88a60 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -3690,6 +3690,17 @@ suite('CopilotAgentSession', () => { startsTurn: false, }); + assert.deepStrictEqual(buildCopilotSystemNotification({ + ...base, + data: { + content: '\nExternal host ping\n', + kind: { type: 'unclassified', metadata: { source: 'host' } }, + }, + }), { + messageText: 'External host ping', + startsTurn: true, + }); + assert.strictEqual(buildCopilotSystemNotification({ ...base, data: { From 7c89d2a51e9733f394d6ca133050683b9d2cff63 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 30 Jul 2026 22:36:06 +0200 Subject: [PATCH 26/86] Fix component fixture font token leakage --- src/vs/sessions/browser/parts/agentsPartCard.ts | 2 +- src/vs/sessions/browser/parts/customViewNode.ts | 2 +- src/vs/sessions/browser/parts/panelPart.ts | 2 +- src/vs/sessions/browser/parts/sessionView.ts | 2 +- src/vs/sessions/common/layoutConstants.ts | 7 +++++++ src/vs/sessions/common/sizes.ts | 11 +---------- 6 files changed, 12 insertions(+), 14 deletions(-) create mode 100644 src/vs/sessions/common/layoutConstants.ts diff --git a/src/vs/sessions/browser/parts/agentsPartCard.ts b/src/vs/sessions/browser/parts/agentsPartCard.ts index 4c7fc4791b9..baa8fde67ae 100644 --- a/src/vs/sessions/browser/parts/agentsPartCard.ts +++ b/src/vs/sessions/browser/parts/agentsPartCard.ts @@ -5,7 +5,7 @@ import { IColorTheme } from '../../../platform/theme/common/themeService.js'; import { agentsPanelBackground, agentsPanelBorder, agentsPanelForeground } from '../../common/theme.js'; -import { AGENTS_FLOATING_PANEL_GAP } from '../../common/sizes.js'; +import { AGENTS_FLOATING_PANEL_GAP } from '../../common/layoutConstants.js'; /** * Marks a part as a floating content card. Carries the shared background, diff --git a/src/vs/sessions/browser/parts/customViewNode.ts b/src/vs/sessions/browser/parts/customViewNode.ts index 83f91d1f1a8..918c10cf301 100644 --- a/src/vs/sessions/browser/parts/customViewNode.ts +++ b/src/vs/sessions/browser/parts/customViewNode.ts @@ -13,7 +13,7 @@ import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../platform/acti import { MenuItemAction } from '../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { asCssVariable } from '../../../platform/theme/common/colorUtils.js'; -import { AGENTS_CENTERED_CONTENT_MAX_WIDTH } from '../../common/sizes.js'; +import { AGENTS_CENTERED_CONTENT_MAX_WIDTH } from '../../common/layoutConstants.js'; import { activeSessionViewBackground, activeSessionViewForeground } from '../../common/theme.js'; import { AbstractCustomView, ICustomViewDescriptor } from '../../services/customView/browser/customView.js'; import { SessionHeaderMetaActionViewItem } from './sessionHeaderMetaActionViewItem.js'; diff --git a/src/vs/sessions/browser/parts/panelPart.ts b/src/vs/sessions/browser/parts/panelPart.ts index 776e9341e26..67aefd4dcd1 100644 --- a/src/vs/sessions/browser/parts/panelPart.ts +++ b/src/vs/sessions/browser/parts/panelPart.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from '../../../platform/instantiation/common/in import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { PANEL_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BORDER } from '../../../workbench/common/theme.js'; import { agentsBadgeBackground, agentsBadgeForeground, agentsPanelBackground, agentsPanelBorder, agentsPanelForeground } from '../../common/theme.js'; -import { AGENTS_FLOATING_PANEL_GAP } from '../../common/sizes.js'; +import { AGENTS_FLOATING_PANEL_GAP } from '../../common/layoutConstants.js'; import { INotificationService } from '../../../platform/notification/common/notification.js'; import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; import { assertReturnsDefined } from '../../../base/common/types.js'; diff --git a/src/vs/sessions/browser/parts/sessionView.ts b/src/vs/sessions/browser/parts/sessionView.ts index 6cf624cf6f1..9675440817d 100644 --- a/src/vs/sessions/browser/parts/sessionView.ts +++ b/src/vs/sessions/browser/parts/sessionView.ts @@ -26,7 +26,7 @@ import { ISessionContext, SessionContext } from '../../services/sessions/browser import { autorun, observableFromEvent, observableValue } from '../../../base/common/observable.js'; import { SessionIsMaximizedContext } from '../../common/contextkeys.js'; import { UNARCHIVE_SESSION_COMMAND_ID } from '../../common/sessionCommands.js'; -import { AGENTS_CENTERED_CONTENT_MAX_WIDTH } from '../../common/sizes.js'; +import { AGENTS_CENTERED_CONTENT_MAX_WIDTH } from '../../common/layoutConstants.js'; import { setActiveSessionContextKeys } from '../../services/sessions/common/sessionContextKeys.js'; import { activeSessionViewBackground, activeSessionViewForeground, inactiveSessionViewBackground, inactiveSessionViewForeground } from '../../common/theme.js'; import { ChatInteractivity, SessionStatus } from '../../services/sessions/common/session.js'; diff --git a/src/vs/sessions/common/layoutConstants.ts b/src/vs/sessions/common/layoutConstants.ts new file mode 100644 index 00000000000..b89a90efcd8 --- /dev/null +++ b/src/vs/sessions/common/layoutConstants.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const AGENTS_FLOATING_PANEL_GAP = 5; +export const AGENTS_CENTERED_CONTENT_MAX_WIDTH = 950; diff --git a/src/vs/sessions/common/sizes.ts b/src/vs/sessions/common/sizes.ts index 5a28cd0967a..1691e52ed6a 100644 --- a/src/vs/sessions/common/sizes.ts +++ b/src/vs/sessions/common/sizes.ts @@ -12,27 +12,18 @@ import { localize } from '../../nls.js'; import { registerSize, sizeForAllThemes } from '../../platform/theme/common/sizeUtils.js'; +import { AGENTS_FLOATING_PANEL_GAP } from './layoutConstants.js'; // ============================================================================ // Agents window — layout // ============================================================================ -export const AGENTS_FLOATING_PANEL_GAP = 5; - /** Gap between floating panels in the Agents window. */ export const agentsLayoutFloatingPanelGap = registerSize( 'agents.layout.floatingPanelGap', sizeForAllThemes(AGENTS_FLOATING_PANEL_GAP, 'px'), localize('agents.layout.floatingPanelGap', "Gap between floating panels in the Agents window.") ); - -/** - * Width the centered content band of a content part is capped to (session - * views and custom views), so every content surface in the Agents window keeps - * the same measure. - */ -export const AGENTS_CENTERED_CONTENT_MAX_WIDTH = 950; - // ============================================================================ // Agents window — font ramp // ============================================================================ From ecb377984e968c6f85d90e943611df49001301f3 Mon Sep 17 00:00:00 2001 From: Arthur Cnops Date: Thu, 30 Jul 2026 22:17:36 +0100 Subject: [PATCH 27/86] Voice: queue concurrent question forms instead of swapping them (#328205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Voice: queue concurrent question forms instead of swapping them Two forms can be pending on one request at once - askQuestions can open one while an MCP elicitation attaches another outside the agent loop. Voice is a serial channel, but both functions that decide which form is "current" picked the newest one independently, so a form arriving silently retargeted the answer the user was in the middle of giving, and dropped the first form's draft. Both now go through one selector that returns the oldest still-open actionable part, which is what the chat model itself does in _pendingInfo. Resolved parts are already skipped, so the oldest open part is by construction the one voice already published: the queue needs no stored state, and payload/detail disagreement becomes unrepresentable. Fixing only the payload would be worse than the bug: the detail would still flip to the new form, isDetailTransition would fire on the detail alone, and the narration path would read the OLD form aloud again. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c4672e9-182f-4a2f-8a79-bd8b770961a1 * voice: send each session's label so the backend can tell two apart The backend renders [SESSIONS] for the model and its prompt says "Do NOT guess" when several sessions are waiting — but every entry we sent was anonymous, so two concurrent question forms arrived as two identical lines and there was nothing to disambiguate on. Send the label that already exists on both session kinds: `label` for agent sessions, `title` for plain chats. Drop `active_session` while here. It was declared on the wire type and diffed on every context send, but the only place that would have assigned it was an empty try/catch, so it has always been undefined: `activeChanged` was permanently false and the delta never carried the field. The per-entry `is_active` flag is what actually names the focused session. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c4672e9-182f-4a2f-8a79-bd8b770961a1 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * voice: cover the session label on the wire, and trim a JSDoc The label is the only human-readable handle the backend has for a session, so without it two waiting forms cannot be told apart by name. It was emitted from two branches with no test on either; a refactor could have made sessions anonymous again silently. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c4672e9-182f-4a2f-8a79-bd8b770961a1 --------- Co-authored-by: Arthur Cnops Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Megan Rogge Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Copilot-Session: 1c4672e9-182f-4a2f-8a79-bd8b770961a1 --- .../browser/voiceClient/voiceClientService.ts | 11 +- .../voiceClient/voiceSessionController.ts | 242 +++++++++-------- .../common/voiceClient/voiceClientService.ts | 6 +- .../voiceSessionController.test.ts | 248 ++++++++++++++++-- 4 files changed, 363 insertions(+), 144 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts index 5b77c7608bf..69086befb30 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts @@ -92,7 +92,6 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic // state-change event needs to fire before the timer expires. private _pendingContext: IVoiceSessionContext | undefined; private _lastSentById = new Map>(); // session id → last-sent field values - private _lastSentActive = ''; // --- Events --- private readonly _onTranscription = this._register(new Emitter()); @@ -534,7 +533,6 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic this._lastSessionId = undefined; this._isResuming = false; this._lastSentById.clear(); - this._lastSentActive = ''; this._setConnected(false); } @@ -636,8 +634,6 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic private _sendDelta(context: IVoiceSessionContext): void { const currentIds = new Set(context.sessions.map(s => s.id)); const removes = [...this._lastSentById.keys()].filter(id => !currentIds.has(id)); - const activeKey = context.active_session ? stableStringify(context.active_session) : ''; - const activeChanged = activeKey !== this._lastSentActive; // Compute per-session field-level patches (JSON Merge Patch style) const upserts: Record[] = []; @@ -691,7 +687,7 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic } } - if (upserts.length === 0 && removes.length === 0 && !activeChanged) { + if (upserts.length === 0 && removes.length === 0) { return; } @@ -704,16 +700,14 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic this._lastSentById.set(session.id, obj); } for (const id of removes) { this._lastSentById.delete(id); } - this._lastSentActive = activeKey; this._ws!.send(JSON.stringify({ type: 'session_context', mode: 'delta', upserts, removes, - ...(activeChanged && context.active_session ? { active_session: context.active_session } : {}), })); - this._logService.trace(`[voice] _sendDelta upserts=[${upserts.map(u => `${String(u.id).slice(-8)}:${u.agent_state ?? '(no-state)'}${Object.prototype.hasOwnProperty.call(u, 'agent_state_detail') ? '+detail' : ''}${Object.prototype.hasOwnProperty.call(u, 'last_response_summary') && u.last_response_summary ? '+summary' : ''}`).join(', ')}] removes=${removes.length} activeChanged=${activeChanged}`); + this._logService.trace(`[voice] _sendDelta upserts=[${upserts.map(u => `${String(u.id).slice(-8)}:${u.agent_state ?? '(no-state)'}${Object.prototype.hasOwnProperty.call(u, 'agent_state_detail') ? '+detail' : ''}${Object.prototype.hasOwnProperty.call(u, 'last_response_summary') && u.last_response_summary ? '+summary' : ''}`).join(', ')}] removes=${removes.length}`); } private _seedTracking(context: IVoiceSessionContext): void { @@ -725,7 +719,6 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic } this._lastSentById.set(session.id, obj); } - this._lastSentActive = context.active_session ? stableStringify(context.active_session) : ''; } sendToolResult(callId: string, result: string | IVoiceDispatchResult): void { diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts index fb9744a9ba0..39168350f76 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts @@ -32,7 +32,7 @@ import { IChatService, IChatToolInvocation, ToolConfirmKind, IChatModelReference import { getDisplayedQuestionText, getOptionsWithDefaultsFirst } from '../../common/chatService/chatQuestionCarouselHelpers.js'; import { formatQuestionPrompt } from '../../common/voiceClient/voicePendingNarration.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; -import { IChatModel } from '../../common/model/chatModel.js'; +import { IChatModel, IChatProgressResponseContent } from '../../common/model/chatModel.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; @@ -5335,6 +5335,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const cachedSummary = fallbackState === 'idle' ? this._lastResponseSummaryById.get(sessionIdStr) : undefined; return { id: sessionIdStr, + ...(s.label ? { label: s.label } : {}), is_active: isActive, agent_state: scoped.state, ...(cachedSummary ? { last_response_summary: cachedSummary } : {}), @@ -5360,6 +5361,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const pending = this._buildPendingPayload(model); return { id: s.resource.toString(), + ...(s.label ? { label: s.label } : {}), is_active: isActive, agent_state: scoped.state, ...(!scoped.hideConfirmationDetail && stateInfo.detail ? { agent_state_detail: stateInfo.detail } : {}), @@ -5386,6 +5388,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const pending = this._buildPendingPayload(chatModel); sessionList.push({ id: key, + ...(chatModel.title ? { label: chatModel.title } : {}), is_active: isActive, agent_state: scoped.state, ...(!scoped.hideConfirmationDetail && stateInfo.detail ? { agent_state_detail: stateInfo.detail } : {}), @@ -5394,24 +5397,12 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC }); } - // Try to get active session from chatViewPane via command - let activeSession: { id: string; last_message: string | null } | undefined; - try { - // This is fire-and-forget; the sync command bridge populates active_session - // For now, we omit active_session when called from controller - // (the chatViewPane's context already had this, the floating window didn't) - } catch { - // ignore - } - - const context: IVoiceSessionContext = { + // `active_session` is not sent: the per-session `is_active` flag already + // names the focused session, and the backend keys the marker off it. + return { sessions: sessionList, display_locale: this._window?.navigator.language || 'en-US', }; - if (activeSession) { - context.active_session = activeSession; - } - return context; } /** @@ -5550,6 +5541,82 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC return stateInfo.state; } + /** + * Returns the oldest still-open pending part of the last request. + * + * Answer routing and narration prose MUST both come from here, or they can + * name different forms and a spoken answer lands on the wrong one. + */ + private _selectPendingPart(model: IChatModel | undefined | null): { requestId: string; part: IChatProgressResponseContent } | undefined { + const lastRequest = model?.getRequests().at(-1); + const parts = lastRequest?.response?.response.value; + if (!lastRequest || !parts) { + return undefined; + } + for (const part of parts) { + if (this._isOpenPendingPart(part)) { + return { requestId: lastRequest.id, part }; + } + } + return undefined; + } + + /** Whether a response part is still waiting on the user. */ + private _isOpenPendingPart(part: IChatProgressResponseContent): boolean { + if (part.kind === 'questionCarousel') { + const carousel = part as IChatQuestionCarousel; + // A form with no questions can't be answered by voice or by mouse, so + // it must not hold the queue. + return !carousel.isUsed && !carousel.answeredExternally && carousel.questions.length > 0; + } + if (part.kind === 'planReview' || part.kind === 'confirmation') { + return !(part as { isUsed?: boolean }).isUsed; + } + if (part.kind === 'elicitation2') { + return (part as { state: IObservable }).state.get() === 'pending'; + } + if (part.kind === 'toolInvocation') { + return (part as IChatToolInvocation).state.get().type === IChatToolInvocation.StateKind.WaitingForConfirmation; + } + return false; + } + + /** Prose for the selected pending part, for `agent_state_detail`. */ + private _describePendingPart(part: IChatProgressResponseContent, fallbackDetail: string | undefined): string { + if (part.kind === 'questionCarousel') { + const carousel = part as IChatQuestionCarousel; + const titles = carousel.questions.map(question => question.title).filter(Boolean); + if (titles.length > 0) { + return `questions: ${titles.join(', ')}`; + } + return this._plainText(carousel.message) || 'asking clarifying questions'; + } + if (part.kind === 'planReview') { + return 'review the plan to continue'; + } + if (part.kind === 'elicitation2') { + return this._plainText((part as { title?: string | IMarkdownString }).title) || 'needs input'; + } + if (part.kind === 'confirmation') { + return (part as { title?: string }).title ?? 'needs approval'; + } + if (part.kind === 'toolInvocation') { + const state = (part as IChatToolInvocation).state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { + return ''; + } + const params = state.parameters as Record | undefined; + const command = params?.['command'] ?? params?.['input']; + const explanation = params?.['explanation'] ?? params?.['goal']; + if (typeof command !== 'string' || !command) { + return fallbackDetail ?? ''; + } + const reason = typeof explanation === 'string' && explanation ? `\nreason: ${explanation}` : ''; + return `command: ${command}${reason}`; + } + return ''; + } + private _getAgentStateInfo(model: IChatModel | undefined | null): { state: string; detail?: string; last_response_summary?: string } { if (!model) { return { state: 'unknown' }; @@ -5564,51 +5631,11 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const pendingConfirmation = lastRequest?.response?.isPendingConfirmation.get(); if (pendingConfirmation) { - // Scan ALL response parts to find the most recent pending item. - // We iterate the full list and keep overwriting `confirmDetail` so - // the LAST match wins — response parts are ordered chronologically, - // so earlier tools (already confirmed) will have left - // WaitingForConfirmation while the newest pending item is last. - let confirmDetail = ''; - for (const part of lastRequest?.response?.response.value ?? []) { - if (part.kind === 'questionCarousel' && !(part as { isUsed?: boolean }).isUsed) { - const carousel = part as { questions?: { title?: string }[]; message?: string | { value: string } }; - const titles = (carousel.questions ?? []).map(q => q.title).filter(Boolean); - if (titles.length > 0) { - confirmDetail = `questions: ${titles.join(', ')}`; - } else { - const msg = carousel.message; - confirmDetail = msg ? (typeof msg === 'string' ? msg : msg.value) : 'asking clarifying questions'; - } - } else if (part.kind === 'planReview' && !(part as { isUsed?: boolean }).isUsed) { - confirmDetail = 'review the plan to continue'; - } else if (part.kind === 'elicitation2') { - const elicitation = part as { state: IObservable; title?: string | { value: string } }; - if (elicitation.state.get() === 'pending') { - const title = elicitation.title; - confirmDetail = title ? (typeof title === 'string' ? title : title.value) : 'needs input'; - } - } else if (part.kind === 'confirmation' && !(part as { isUsed?: boolean }).isUsed) { - const conf = part as { title?: string }; - confirmDetail = conf.title ?? 'needs approval'; - } else if (part.kind === 'toolInvocation') { - const state = part.state.get(); - if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { - const params = state.parameters as Record | undefined; - const command = params?.['command'] ?? params?.['input']; - const explanation = params?.['explanation'] ?? params?.['goal']; - if (typeof command === 'string' && command) { - confirmDetail = `command: ${command}`; - if (typeof explanation === 'string' && explanation) { - confirmDetail += `\nreason: ${explanation}`; - } - } else { - confirmDetail = pendingConfirmation.detail ?? ''; - } - } - } - } - + // Same part `_buildPendingPayload` publishes, so the prose the model + // hears and the form an answer routes to can never name different + // forms. See `_selectPendingPart`. + const selected = this._selectPendingPart(model); + const confirmDetail = selected ? this._describePendingPart(selected.part, pendingConfirmation.detail) : ''; return { state: 'waiting_for_confirmation', detail: confirmDetail || pendingConfirmation.detail || '', @@ -5670,64 +5697,55 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC * `questions: `, losing the options, their values and the ids. This * returns what the backend needs to route an answer back to the exact part. * - * Scans newest-first and returns the first still-open part, so an - * already-answered earlier form can't shadow the live one. Plan review is - * deliberately not typed here; it stays on the legacy string path. + * The part is chosen by `_selectPendingPart`, shared with + * `_getAgentStateInfo` so the routable payload and the spoken detail can + * never name different forms. Only carousels and tool confirmations have a + * typed shape; anything else the selector lands on publishes nothing, and the + * session simply has no voice-answerable pending until it is resolved. */ private _buildPendingPayload(model: IChatModel | undefined | null): IVoiceSessionPending | undefined { - const lastRequest = model?.getRequests().at(-1); - const parts = lastRequest?.response?.response.value; - if (!lastRequest || !parts) { + const selected = this._selectPendingPart(model); + if (!selected) { return undefined; } + const { requestId, part } = selected; + // Minted lazily: an id is issued only once the part is confirmed to be + // a live pending request, so a part the backend can never answer never + // gets an identity that a stale id could collide with. + const routing = () => ({ pending_id: derivePendingId(requestId, part), request_id: requestId }); - for (let index = parts.length - 1; index >= 0; index--) { - const part = parts[index]; - // Minted lazily: an id is issued only once the part is confirmed to be - // a live pending request, so a part the backend can never answer never - // gets an identity that a stale id could collide with. - const routing = () => ({ pending_id: derivePendingId(lastRequest.id, part), request_id: lastRequest.id }); - - if (part.kind === 'questionCarousel') { - const carousel = part as IChatQuestionCarousel; - if (carousel.isUsed || carousel.answeredExternally || carousel.questions.length === 0) { - continue; - } - return { - type: 'questions', - ...routing(), - allow_skip: carousel.allowSkip === true, - ...(carousel.message ? { message: this._plainText(carousel.message) } : {}), - questions: carousel.questions.map((question): IVoicePendingQuestion => ({ - id: question.id, - type: question.type, - // The same text the widget shows, so voice reads the question - // rather than its header. - title: this._plainText(getDisplayedQuestionText(question)), - allow_freeform: question.allowFreeformInput !== false, - // The ordinal the user hears has to be the one they see, so the - // list is in the same order the widget renders, and both sides - // number it by position. - options: getOptionsWithDefaultsFirst(question).map(({ option }) => ({ - label: option.label, - value: option.value, - })), + if (part.kind === 'questionCarousel') { + const carousel = part as IChatQuestionCarousel; + return { + type: 'questions', + ...routing(), + allow_skip: carousel.allowSkip === true, + ...(carousel.message ? { message: this._plainText(carousel.message) } : {}), + questions: carousel.questions.map((question): IVoicePendingQuestion => ({ + id: question.id, + type: question.type, + // The same text the widget shows, so voice reads the question + // rather than its header. + title: this._plainText(getDisplayedQuestionText(question)), + allow_freeform: question.allowFreeformInput !== false, + // The ordinal the user hears has to be the one they see, so the + // list is in the same order the widget renders, and both sides + // number it by position. + options: getOptionsWithDefaultsFirst(question).map(({ option }) => ({ + label: option.label, + value: option.value, })), - }; - } + })), + }; + } - if (part.kind === 'toolInvocation') { - const state = (part as IChatToolInvocation).state.get(); - if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { - continue; - } - const message = this._plainText((part as { invocationMessage?: string | IMarkdownString }).invocationMessage); - return { - type: 'approval', - ...routing(), - ...(message ? { message } : {}), - }; - } + if (part.kind === 'toolInvocation') { + const message = this._plainText((part as { invocationMessage?: string | IMarkdownString }).invocationMessage); + return { + type: 'approval', + ...routing(), + ...(message ? { message } : {}), + }; } return undefined; diff --git a/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts b/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts index c87645f623d..db6d2dc4267 100644 --- a/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts +++ b/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts @@ -97,16 +97,14 @@ export function peekPendingId(requestId: string, part: object): string | undefin export interface IVoiceSessionContext { sessions: { id: string; + /** Human-readable name, so the backend can tell two sessions apart. */ + label?: string; is_active: boolean; agent_state: string; agent_state_detail?: string; last_response_summary?: string; pending?: IVoiceSessionPending; }[]; - active_session?: { - id: string; - last_message: string | null; - }; display_locale: string; } diff --git a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts index b62a29d4d5b..6589bdf7ac8 100644 --- a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts @@ -25,14 +25,14 @@ import { IAuthenticationService } from '../../../../../services/authentication/c import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; import { IVoiceTranscriptStore, IVoiceTranscriptTurn } from '../../../../agentsVoice/common/voiceTranscriptStore.js'; -import { IAgentSessionsModel } from '../../../browser/agentSessions/agentSessionsModel.js'; +import { AgentSessionStatus, IAgentSessionsModel } from '../../../browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../browser/agentSessions/agentSessionsService.js'; import { IChatWidgetService } from '../../../browser/chat.js'; import { IMicCaptureService } from '../../../browser/voiceClient/micCaptureService.js'; import { ITtsPlaybackService } from '../../../browser/voiceClient/ttsPlaybackService.js'; import { IVoiceSessionController, VoiceSessionController } from '../../../browser/voiceClient/voiceSessionController.js'; import { IVoiceToolDispatchService } from '../../../browser/voiceClient/voiceToolDispatchService.js'; -import { IChatService } from '../../../common/chatService/chatService.js'; +import { IChatService, IChatToolInvocation } from '../../../common/chatService/chatService.js'; import { IPromptsService } from '../../../common/promptSyntax/service/promptsService.js'; import { derivePendingId, IVoiceAudioResponse, IVoiceBargeIn, IVoiceClientService, IVoiceNarrationSignal, IVoiceSpeechStarted, IVoiceToolCall, IVoiceTranscription, VoiceNarrationKind, IVoiceDispatchResult } from '../../../common/voiceClient/voiceClientService.js'; import { IChatModel } from '../../../common/model/chatModel.js'; @@ -208,22 +208,40 @@ class TestMicCaptureService extends mock() { class TestAgentSessionsService extends mock() { override readonly onDidChangeSessionArchivedState = Event.None; - override readonly model: IAgentSessionsModel = { - onWillResolve: Event.None, - onDidResolve: Event.None, - sessions: [], - onDidChangeSessions: Event.None, - onDidChangeSessionArchivedState: Event.None, - resolved: true, - getSession: () => undefined, - observeSession: () => observableValue('session', undefined), - resolve: async () => { }, + override readonly model: IAgentSessionsModel; + + constructor(sessions: readonly unknown[] = []) { + super(); + this.model = { + onWillResolve: Event.None, + onDidResolve: Event.None, + sessions: sessions as IAgentSessionsModel['sessions'], + onDidChangeSessions: Event.None, + onDidChangeSessionArchivedState: Event.None, + resolved: true, + getSession: () => undefined, + observeSession: () => observableValue('session', undefined), + resolve: async () => { }, + }; + } +} + +/** An agent session entry as `_buildSessionContext` reads it. */ +function agentSessionEntry(id: string, label: string | undefined, status: AgentSessionStatus) { + return { + resource: URI.parse(id), + label, + status, + isArchived: () => false, + timing: { created: Date.now(), lastRequestEnded: Date.now() }, }; } class TestChatService extends mock() { override readonly chatModels = observableValue('chatModels', []); override getSession(): undefined { return undefined; } + /** A session that never loads: the controller eagerly loads models for waiting sessions. */ + override async acquireOrLoadSession(): Promise { return undefined; } } /** @@ -244,8 +262,18 @@ class ControllableChatService extends mock() { } /** Minimal chat model whose last request carries one unanswered question form. */ -function questionCarouselModel(part: object, requestId = 'req-1'): IChatModel { - const lastRequest = { id: requestId, response: { response: { value: [part] } } }; +function pendingPartsModel(parts: object | object[], requestId = 'req-1', pendingDetail?: string): IChatModel { + const value = Array.isArray(parts) ? parts : [parts]; + const lastRequest = { + id: requestId, + response: { + response: { value }, + isPendingConfirmation: observableValue<{ detail?: string } | undefined>( + 'pending', + pendingDetail === undefined ? undefined : { detail: pendingDetail }, + ), + }, + }; return { getRequests: () => [lastRequest], } as unknown as IChatModel; @@ -339,6 +367,7 @@ suite('VoiceSessionController', () => { promptsService: IPromptsService = new class extends mock() { override async getVoiceInstructions(): Promise { return undefined; } }(), + agentSessionsService: IAgentSessionsService = new TestAgentSessionsService(), ): IVoiceSessionController { store.add({ dispose: () => voiceClientService.dispose() }); store.add(ttsPlaybackService); @@ -354,7 +383,7 @@ suite('VoiceSessionController', () => { override notifyPlaybackStart(): void { } override notifyPlaybackEnd(): void { } }(), - new TestAgentSessionsService(), + agentSessionsService, chatService, commandService, new class extends mock() { @@ -511,7 +540,7 @@ suite('VoiceSessionController', () => { }], }; - const payload = buildPendingPayload.call(controller, questionCarouselModel(part)); + const payload = buildPendingPayload.call(controller, pendingPartsModel(part)); assert.deepStrictEqual(payload, { type: 'questions', @@ -538,9 +567,190 @@ suite('VoiceSessionController', () => { const buildPendingPayload = Reflect.get(controller, '_buildPendingPayload') as (model: IChatModel) => unknown; const questions = [{ id: 'region', type: 'singleSelect', title: 'Which region?', options: [{ id: 'west', label: 'West US', value: 'westus' }] }]; - assert.strictEqual(buildPendingPayload.call(controller, questionCarouselModel({ kind: 'questionCarousel', isUsed: true, questions })), undefined); - assert.strictEqual(buildPendingPayload.call(controller, questionCarouselModel({ kind: 'questionCarousel', answeredExternally: true, questions })), undefined); - assert.strictEqual(buildPendingPayload.call(controller, questionCarouselModel({ kind: 'questionCarousel', questions: [] })), undefined); + assert.strictEqual(buildPendingPayload.call(controller, pendingPartsModel({ kind: 'questionCarousel', isUsed: true, questions })), undefined); + assert.strictEqual(buildPendingPayload.call(controller, pendingPartsModel({ kind: 'questionCarousel', answeredExternally: true, questions })), undefined); + assert.strictEqual(buildPendingPayload.call(controller, pendingPartsModel({ kind: 'questionCarousel', questions: [] })), undefined); + }); + + test('selects the oldest still-open pending part, not the newest', () => { + // Voice is a serial channel: a second form arriving must not take the turn + // from the one the user was just read out and is part-way through + // answering. Oldest-first is also what the chat model itself does when it + // decides what a response is waiting on. + const controller = createController(new TestVoiceClientService()); + const selectPendingPart = Reflect.get(controller, '_selectPendingPart') as (model: IChatModel) => { requestId: string; part: { kind: string } } | undefined; + const older = { kind: 'questionCarousel', questions: [{ id: 'a', type: 'singleSelect', title: 'A?', options: [] }] }; + const newer = { kind: 'questionCarousel', questions: [{ id: 'b', type: 'singleSelect', title: 'B?', options: [] }] }; + + const selected = selectPendingPart.call(controller, pendingPartsModel([older, newer])); + + assert.strictEqual(selected?.part, older); + assert.strictEqual(selected?.requestId, 'req-1'); + }); + + test('moves on once the oldest pending part is resolved', () => { + const controller = createController(new TestVoiceClientService()); + const selectPendingPart = Reflect.get(controller, '_selectPendingPart') as (model: IChatModel) => { part: { kind: string } } | undefined; + const answered = { kind: 'questionCarousel', isUsed: true, questions: [{ id: 'a', type: 'singleSelect', title: 'A?', options: [] }] }; + const newer = { kind: 'questionCarousel', questions: [{ id: 'b', type: 'singleSelect', title: 'B?', options: [] }] }; + + assert.strictEqual(selectPendingPart.call(controller, pendingPartsModel([answered, newer]))?.part, newer); + assert.strictEqual(selectPendingPart.call(controller, pendingPartsModel([answered]))?.part, undefined); + }); + + test('an executing tool does not shadow the form it opened', () => { + // askQuestions appends its carousel from inside invoke(), so its own tool + // part is always earlier in the list. It declares no confirmationMessages + // and therefore sits in Executing, not WaitingForConfirmation - if that + // ever changed, oldest-first would publish an approval for a question form + // and the form would never reach voice. + const controller = createController(new TestVoiceClientService()); + const selectPendingPart = Reflect.get(controller, '_selectPendingPart') as (model: IChatModel) => { part: { kind: string } } | undefined; + const executingTool = { + kind: 'toolInvocation', + state: observableValue('state', { type: IChatToolInvocation.StateKind.Executing }), + }; + const carousel = { kind: 'questionCarousel', questions: [{ id: 'a', type: 'singleSelect', title: 'A?', options: [] }] }; + + assert.strictEqual(selectPendingPart.call(controller, pendingPartsModel([executingTool, carousel]))?.part, carousel); + }); + + test('keeps publishing the older form when a second one arrives', () => { + // Without this the payload flips to the newest form with no narration, so + // an answer meant for the first form is applied to the second. + const controller = createController(new TestVoiceClientService()); + const buildPendingPayload = Reflect.get(controller, '_buildPendingPayload') as (model: IChatModel) => { pending_id?: string; questions?: { id: string }[] } | undefined; + const older = { kind: 'questionCarousel', questions: [{ id: 'region', type: 'singleSelect', title: 'Which region?', options: [{ id: 'w', label: 'West US', value: 'westus' }] }] }; + const newer = { kind: 'questionCarousel', questions: [{ id: 'tier', type: 'singleSelect', title: 'Which tier?', options: [{ id: 'p', label: 'Premium', value: 'premium' }] }] }; + + const payload = buildPendingPayload.call(controller, pendingPartsModel([older, newer])); + + assert.deepStrictEqual(payload?.questions?.map(question => question.id), ['region']); + assert.strictEqual(payload?.pending_id, derivePendingId('req-1', older)); + }); + + test('payload and spoken detail name the same form when two are open', () => { + // If these two disagree, the newer form flips the detail, that counts as a + // transition, and the narration path then reads the OLDER form aloud again. + const controller = createController(new TestVoiceClientService()); + const buildPendingPayload = Reflect.get(controller, '_buildPendingPayload') as (model: IChatModel) => { questions?: { title: string }[] } | undefined; + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { state: string; detail?: string }; + const older = { kind: 'questionCarousel', questions: [{ id: 'region', type: 'singleSelect', title: 'Which region?', options: [] }] }; + const newer = { kind: 'questionCarousel', questions: [{ id: 'tier', type: 'singleSelect', title: 'Which tier?', options: [] }] }; + const model = pendingPartsModel([older, newer], 'req-1', 'Answer questions to continue...'); + + const info = getAgentStateInfo.call(controller, model); + + assert.strictEqual(info.state, 'waiting_for_confirmation'); + assert.strictEqual(info.detail, 'questions: Which region?'); + assert.deepStrictEqual(buildPendingPayload.call(controller, model)?.questions?.map(question => question.title), ['Which region?']); + }); + + test('sends each agent session label so two waiting sessions can be told apart', () => { + // The label is the only human-readable handle the backend has. Without it + // every session is "Untitled" and naming one out loud cannot disambiguate + // which of two open forms an answer is for. + const controller = createController( + new TestVoiceClientService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, + new TestAgentSessionsService([ + agentSessionEntry('vscode-chat://a', 'Auth fix', AgentSessionStatus.NeedsInput), + agentSessionEntry('vscode-chat://b', 'Billing refactor', AgentSessionStatus.InProgress), + ]), + ); + const buildSessionContext = Reflect.get(controller, '_buildSessionContext') as () => { sessions: { id: string; label?: string }[] }; + + const labels = buildSessionContext.call(controller).sessions.map(session => session.label); + + assert.deepStrictEqual(labels, ['Auth fix', 'Billing refactor']); + }); + + test('omits the label for an unlabelled agent session rather than sending an empty one', () => { + // An empty string would render as a nameless label the model might try to + // quote back at the user; absent lets the backend fall back to "Untitled". + const controller = createController( + new TestVoiceClientService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, + new TestAgentSessionsService([agentSessionEntry('vscode-chat://a', undefined, AgentSessionStatus.NeedsInput)]), + ); + const buildSessionContext = Reflect.get(controller, '_buildSessionContext') as () => { sessions: { id: string; label?: string }[] }; + + const [session] = buildSessionContext.call(controller).sessions; + + assert.strictEqual(session.id, 'vscode-chat://a'); + assert.ok(!Object.hasOwn(session, 'label')); + }); + + test('sends the agent session label once its model is resident too', () => { + // The label is emitted from two branches - model resident or not - and a + // session flips between them as VS Code loads and disposes models. Only + // covering the unloaded branch would let the loaded one lose the label + // silently, which is exactly when a form is on screen to disambiguate. + const chatService = new ControllableChatService(); + const resource = URI.parse('vscode-chat://a'); + chatService.setModels([pendingConfirmationModel(resource)]); + const controller = createController( + new TestVoiceClientService(), undefined, undefined, undefined, undefined, undefined, chatService, undefined, + new TestAgentSessionsService([agentSessionEntry(resource.toString(), 'Auth fix', AgentSessionStatus.NeedsInput)]), + ); + const buildSessionContext = Reflect.get(controller, '_buildSessionContext') as () => { sessions: { id: string; label?: string; agent_state: string }[] }; + // Make it the active session: a background confirmation is deliberately + // downgraded to `thinking`, which would hide whether the resident branch + // ran at all. + controller.setTargetSession(resource); + + const [session] = buildSessionContext.call(controller).sessions; + + assert.strictEqual(session.agent_state, 'waiting_for_confirmation'); + assert.strictEqual(session.label, 'Auth fix'); + }); + + test('an older tool confirmation holds the turn ahead of a newer form', () => { + // Queue semantics applied uniformly: approve the command you were asked + // about, then answer the questions. + const controller = createController(new TestVoiceClientService()); + const buildPendingPayload = Reflect.get(controller, '_buildPendingPayload') as (model: IChatModel) => { type?: string } | undefined; + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { detail?: string }; + const approval = { + kind: 'toolInvocation', + invocationMessage: 'Run a command', + state: observableValue('state', { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: { command: 'docker push myapp:latest' }, + }), + }; + const form = { kind: 'questionCarousel', questions: [{ id: 'tier', type: 'singleSelect', title: 'Which tier?', options: [] }] }; + const model = pendingPartsModel([approval, form], 'req-1', 'Run command?'); + + assert.strictEqual(buildPendingPayload.call(controller, model)?.type, 'approval'); + assert.strictEqual(getAgentStateInfo.call(controller, model).detail, 'command: docker push myapp:latest'); + }); + + test('an older confirmation suppresses a newer form payload but still speaks', () => { + // `confirmation` has no typed wire shape, so the queue costs the newer form + // its structured payload until the confirmation is resolved. Deliberate. + const controller = createController(new TestVoiceClientService()); + const buildPendingPayload = Reflect.get(controller, '_buildPendingPayload') as (model: IChatModel) => unknown; + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { detail?: string }; + const confirmation = { kind: 'confirmation', title: 'Delete the branch?' }; + const form = { kind: 'questionCarousel', questions: [{ id: 'tier', type: 'singleSelect', title: 'Which tier?', options: [] }] }; + const model = pendingPartsModel([confirmation, form], 'req-1', 'Delete the branch?'); + + assert.strictEqual(buildPendingPayload.call(controller, model), undefined); + assert.strictEqual(getAgentStateInfo.call(controller, model).detail, 'Delete the branch?'); + }); + + test('a newer form answered by mouse leaves the focused form untouched', () => { + // Resolving B out of order must not move the turn, and must not change the + // detail either - a detail change alone counts as a transition and would + // read A aloud a second time. + const controller = createController(new TestVoiceClientService()); + const buildPendingPayload = Reflect.get(controller, '_buildPendingPayload') as (model: IChatModel) => { questions?: { id: string }[] } | undefined; + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { detail?: string }; + const older = { kind: 'questionCarousel', questions: [{ id: 'region', type: 'singleSelect', title: 'Which region?', options: [] }] }; + const newerAnswered = { kind: 'questionCarousel', isUsed: true, questions: [{ id: 'tier', type: 'singleSelect', title: 'Which tier?', options: [] }] }; + const model = pendingPartsModel([older, newerAnswered], 'req-1', 'Answer questions to continue...'); + + assert.deepStrictEqual(buildPendingPayload.call(controller, model)?.questions?.map(question => question.id), ['region']); + assert.strictEqual(getAgentStateInfo.call(controller, model).detail, 'questions: Which region?'); }); test('fatal disconnect clears routing target and pending confirmations and the tracker cannot repopulate them before reconnect', () => { From 16c072c648d0f82f49211cab4725deb49d03ee09 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:18:22 +0200 Subject: [PATCH 28/86] sessions: give browsers their own pill above the chat input (#328274) Browsers and subagents were merged into a single background-activities pill. Split browsers out into an independent pill so each surface stands on its own, and keep the background-activities pill for subagents plus the activity kinds that will be added to it later. - Add `SessionActivityPill`, a content-agnostic widget owning only the button, single-vs-many behavior, visibility and the picker. Consumers supply their activities, per-activity icons, category titles and multi-activity summary. - Add `SessionBrowsersControl` for live browsers, including the preference for a browser already sharing with the agent. - Trim `SessionBackgroundActivitiesControl` to subagents. - Keep the turn pills at their natural width in the toolbar row: they set `flex-shrink: 0` on their inner pills, so squeezing them made those pills paint over a neighbouring pill. The activity pills absorb the shrink and ellipsize instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/SESSIONS.md | 2 +- ...iesControl.css => sessionActivityPill.css} | 15 +- .../browser/media/sessionChatInputToolbar.css | 13 +- .../chat/browser/sessionActivityPill.ts | 169 ++++++++++ .../sessionBackgroundActivitiesControl.ts | 267 +++------------- .../chat/browser/sessionBrowsersControl.ts | 149 +++++++++ .../chat/browser/sessionChatInputToolbar.ts | 9 +- .../browser/sessionsChatAccessibilityHelp.ts | 2 +- ...sessionBackgroundActivitiesControl.test.ts | 203 ++---------- .../browser/sessionBrowsersControl.test.ts | 301 ++++++++++++++++++ .../sessionChatInputToolbar.fixture.ts | 2 +- 11 files changed, 729 insertions(+), 403 deletions(-) rename src/vs/sessions/contrib/chat/browser/media/{sessionBackgroundActivitiesControl.css => sessionActivityPill.css} (56%) create mode 100644 src/vs/sessions/contrib/chat/browser/sessionActivityPill.ts create mode 100644 src/vs/sessions/contrib/chat/browser/sessionBrowsersControl.ts create mode 100644 src/vs/sessions/contrib/chat/test/browser/sessionBrowsersControl.test.ts diff --git a/src/vs/sessions/SESSIONS.md b/src/vs/sessions/SESSIONS.md index cd8c20f783a..5f5b880abee 100644 --- a/src/vs/sessions/SESSIONS.md +++ b/src/vs/sessions/SESSIONS.md @@ -181,7 +181,7 @@ History restoration must also repair parent tool calls whose persisted `_meta`/s **Subagents in the Chats menu.** Subagents spawned by the **currently-active** chat are shown as a separate group (`2_subagents`) at the bottom of the **Chats** (Conversations) submenu, below the session's regular chats (`1_chats`); a separator divides the two groups. Per-chat association uses `IChatOrigin.parentChat` — the sessions-layer origin carries the spawning chat's resource (mapped from the protocol `ChatOrigin.chat` by the agent host provider's `_resolveParentChatResource`) — so the group changes as the active chat changes. Selecting a subagent entry toggles its read-only tab open/closed like any other chat entry. The entries are populated per session by `SessionConversationsMenuContribution` (only when the active chat has subagents). Subagents on their own do **not** show the chat tab strip: `IActiveSession.shouldShowChatTabs` is shown only when there is more than one visible tab (e.g. a subagent explicitly opened as a tab alongside the main chat) — a subagent that has not been opened as a tab is ignored. The **Chats** menu is always surfaced in the **session header meta row** (at the end of the pills), independent of the strip's visibility, kept available by `SessionActiveChatHasSubagentsContext` even when the parent is the only committed chat. -**Background activities above the chat input.** `SessionChatInputToolbar` combines live integrated browsers and active subagents into one background-activities pill. Browsers come from `IBrowserViewWorkbenchService.getKnownBrowserViews()` and belong to the viewed chat when their `IBrowserViewOwner.sessionId` matches that chat or one of its direct tool-origin subagents; subagents come from the owning session's tool-origin chats whose `origin.parentChat` is the viewed chat and whose status is active (`InProgress` or `NeedsInput`). Keeping `NeedsInput` visible is important because a pending tool or input confirmation does not end the subagent's active turn. A single activity shows its kind icon and label (browser page title, falling back to "Browser"; subagent title truncated after 30 characters with `...`). Multiple activities of one kind show **N Active Browsers/Subagents**; mixed kinds show **N Background Activities** with the session-in-progress icon. Any multi-item pill opens `IActionWidgetService` with categorized **Browsers** and **Subagents** sections (browser section first), where every selectable row has its kind icon and label. Opening a browser activity prefers a contextual browser page already **Sharing with Agent** for the same destination (exact URL first, then the browser tools' same-host rule), so the user sees the page the agent is driving; when no shared match exists, it opens the activity's normal browser input. The boolean `chat.turnStatusPills` setting gates the entire status-pills surface; for compatibility, any `true` member in the former per-pill object form enables the whole surface. When enabled, completed-turn pills replace the older checkpoint file-changes summary. `ChatView` mounts the toolbar in `ChatInputPart.persistentContentContainerElement`, which remains in layout when `ChatWidget.setReadOnly(true)` hides the rest of the composer, so these pills also remain available on read-only chats. +**Browsers and background activities above the chat input.** `SessionChatInputToolbar` mounts two independent activity pills, both rendered by the shared `SessionActivityPill` widget (which owns only the button, picker, and visibility — each control supplies its own activities, category titles, icons, and multi-activity summary): a **browsers** pill (`SessionBrowsersControl`) for live integrated browsers, and a **background activities** pill (`SessionBackgroundActivitiesControl`) for the viewed chat's active subagents — the latter is the extension point for further background-activity kinds. Browsers come from `IBrowserViewWorkbenchService.getKnownBrowserViews()` and belong to the viewed chat when their `IBrowserViewOwner.sessionId` matches that chat or one of its direct tool-origin subagents; subagents come from the owning session's tool-origin chats whose `origin.parentChat` is the viewed chat and whose status is active (`InProgress` or `NeedsInput`). Keeping `NeedsInput` visible is important because a pending tool or input confirmation does not end the subagent's active turn. A pill with a single activity shows its kind icon and label (browser page title, falling back to "Browser"; subagent title truncated after 30 characters with `...`). Multiple activities of one kind show **N Active Browsers/Subagents**; a pill holding mixed kinds shows **N Background Activities** with the session-in-progress icon. Any multi-item pill opens `IActionWidgetService` with categorized **Browsers** and **Subagents** sections (browser section first), where every selectable row has its kind icon and label. Opening a browser activity prefers a contextual browser page already **Sharing with Agent** for the same destination (exact URL first, then the browser tools' same-host rule), so the user sees the page the agent is driving; when no shared match exists, it opens the activity's normal browser input. The boolean `chat.turnStatusPills` setting gates the entire status-pills surface; for compatibility, any `true` member in the former per-pill object form enables the whole surface. When enabled, completed-turn pills replace the older checkpoint file-changes summary. `ChatView` mounts the toolbar in `ChatInputPart.persistentContentContainerElement`, which remains in layout when `ChatWidget.setReadOnly(true)` hides the rest of the composer, so these pills also remain available on read-only chats. **Debugging chat input UI without a live session.** Outside stable quality, the Developer command **Configure Fake Session Chat UI** is contributed to the Command Palette only while the active concrete session view is `ChatView` (not the new-session or new-chat composer). `SessionChatPillsDebugService` owns the command, active-view registration, and modal form. The form accepts non-negative files/insertions/deletions counts, failed/pending CI check counts, PR/agent feedback-to-address counts, plus comma- or newline-separated Markdown file names, subagent names, and browser labels. Its changes section also offers an auto-increment checkbox: while enabled, a disposable two-second interval independently increases insertions and deletions by values from 0 through 15. Each increment is the minimum of two uniform samples, giving strictly decreasing probabilities (0 most likely, 15 least likely). **Apply** forces the active toolbar and `SessionInputBanners` host to render those values independently of provider state, dismissal state, and `chat.turnStatusPills`; **Clear** removes the override; **Cancel** leaves it unchanged. Fake banner actions and dismiss controls are inert so they cannot invoke real CI or feedback operations. Applying again replaces the previous interval; Clear, active chat/view changes, and service disposal cancel it through the service-owned `MutableDisposable`. All debug-only coordination is isolated in `sessionChatInputToolbarDebug.ts`; the production widgets expose only the small override seams consumed by that service. diff --git a/src/vs/sessions/contrib/chat/browser/media/sessionBackgroundActivitiesControl.css b/src/vs/sessions/contrib/chat/browser/media/sessionActivityPill.css similarity index 56% rename from src/vs/sessions/contrib/chat/browser/media/sessionBackgroundActivitiesControl.css rename to src/vs/sessions/contrib/chat/browser/media/sessionActivityPill.css index d2634639720..56cd36a24ae 100644 --- a/src/vs/sessions/contrib/chat/browser/media/sessionBackgroundActivitiesControl.css +++ b/src/vs/sessions/contrib/chat/browser/media/sessionActivityPill.css @@ -3,16 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.session-background-activities { +/* Several pills can share the row above the input (browsers, background + activities, turn status), so a pill shrinks below its content and ellipsizes + its label rather than pushing its neighbours out of the row. The cap keeps a + single long label from crowding out the other pills when there is room. */ +.session-activity-pill { display: inline-flex; + flex: 0 1 auto; min-width: 0; } -.session-background-activities.hidden { +.session-activity-pill.hidden { display: none; } -.session-background-activities .session-background-activities-button { +.session-activity-pill .session-activity-pill-button { display: inline-flex; width: fit-content; min-width: 0; @@ -23,14 +28,14 @@ touch-action: manipulation; } -.session-background-activities .session-background-activities-button > span:not(.codicon) { +.session-activity-pill .session-activity-pill-button > span:not(.codicon) { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } -.session-background-activities .session-background-activities-button .codicon { +.session-activity-pill .session-activity-pill-button .codicon { font-size: var(--vscode-codiconFontSize-compact); flex-shrink: 0; } diff --git a/src/vs/sessions/contrib/chat/browser/media/sessionChatInputToolbar.css b/src/vs/sessions/contrib/chat/browser/media/sessionChatInputToolbar.css index e71832c3a7d..431c40ba336 100644 --- a/src/vs/sessions/contrib/chat/browser/media/sessionChatInputToolbar.css +++ b/src/vs/sessions/contrib/chat/browser/media/sessionChatInputToolbar.css @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ /* Floating status pills centered above the chat input. The pills themselves are - the shared `.chat-turn-pills` widget (styled in chatTurnPills.css); this file - only positions and centers that widget above the input. */ + the shared `.chat-turn-pills` widget (styled in chatTurnPills.css) and the + session activity pills (sessionActivityPill.css); this file only positions and + centers them above the input. */ .session-chat-input-toolbar { display: flex; @@ -16,6 +17,14 @@ padding: var(--vscode-spacing-size20) 0 var(--vscode-spacing-size60) 0; } +/* The turn pills size to their content and deliberately don't shrink internally, + so squeezing them would spill their pills over the activity pills next to + them. Keep them at their natural width and let the activity pills, which + ellipsize their labels, absorb the shrinking instead. */ +.session-chat-input-toolbar > .chat-turn-pills { + flex-shrink: 0; +} + .session-chat-input-toolbar.hidden { display: none; } diff --git a/src/vs/sessions/contrib/chat/browser/sessionActivityPill.ts b/src/vs/sessions/contrib/chat/browser/sessionActivityPill.ts new file mode 100644 index 00000000000..03ef6d2f233 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/sessionActivityPill.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from '../../../../base/browser/dom.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { onUnexpectedError } from '../../../../base/common/errors.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IObservable, observableValue } from '../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; +import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; +import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import './media/sessionActivityPill.css'; + +/** One entry of a pill, rendered as the button label or as a picker row. */ +export interface ISessionActivity { + readonly label: string; + readonly icon: ThemeIcon; +} + +/** A named section of the picker; sections without activities are skipped. */ +export interface ISessionActivityCategory { + readonly title: string; + readonly activities: readonly T[]; +} + +/** The button content when a pill stands for more than one activity. */ +export interface ISessionActivitySummary { + readonly label: string; + readonly icon: ThemeIcon; + readonly ariaLabel: string; +} + +export interface ISessionActivityPillOptions { + /** Extra class on the pill root, for fixtures and per-pill styling. */ + readonly className: string; + /** Identifies the pill's picker to the action widget service. */ + readonly widgetId: string; + /** Accessible name of the picker shown for more than one activity. */ + readonly getWidgetAriaLabel: () => string; + /** Button content for more than one activity; a single activity renders itself. */ + readonly getSummary: (activities: readonly T[]) => ISessionActivitySummary; + readonly openActivity: (activity: T) => void | Promise; +} + +/** + * A compact button standing for a set of activities. A single activity is shown + * with its own icon and label and is opened directly; more than one shows the + * consumer's summary and opens a picker grouped by category. The widget owns + * only the presentation — which activities exist, how they are grouped, and how + * they are labelled is up to the consumer. + */ +export class SessionActivityPill extends Disposable { + + readonly element: HTMLElement; + readonly isVisible: IObservable; + + private readonly _button: Button; + private readonly _isVisible = observableValue(this, false); + private _categories: readonly ISessionActivityCategory[] = []; + private _activities: readonly T[] = []; + + constructor( + private readonly _options: ISessionActivityPillOptions, + private readonly _actionWidgetService: IActionWidgetService, + ) { + super(); + + this.element = $(`.session-activity-pill.${_options.className}.hidden`); + this.isVisible = this._isVisible; + this._button = this._register(new Button(this.element, { secondary: true, small: true, supportIcons: true, ...defaultButtonStyles })); + this._button.element.classList.add('session-activity-pill-button'); + this._register(this._button.onDidClick(() => this._onDidClick())); + } + + setCategories(categories: readonly ISessionActivityCategory[]): void { + this._categories = categories.filter(category => category.activities.length > 0); + this._activities = this._categories.flatMap(category => category.activities); + this._render(); + } + + private _render(): void { + const count = this._activities.length; + this._isVisible.set(count > 0, undefined); + this.element.classList.toggle('hidden', count === 0); + if (count === 0) { + return; + } + + let label: string; + let accessibleLabel: string; + if (count === 1) { + const activity = this._activities[0]; + label = `$(${activity.icon.id}) ${activity.label}`; + accessibleLabel = localize('sessionActivityPill.open', "Open {0}", activity.label); + } else { + const summary = this._options.getSummary(this._activities); + label = `$(${summary.icon.id}) ${summary.label} $(${Codicon.chevronDown.id})`; + accessibleLabel = summary.ariaLabel; + } + + this._button.label = label; + this._button.setTitle(accessibleLabel); + this._button.setAriaLabel(accessibleLabel); + } + + private _onDidClick(): void { + if (this._activities.length === 1) { + this._openActivity(this._activities[0]); + return; + } + if (this._activities.length > 1) { + this._showPicker(); + } + } + + private _openActivity(activity: T): void { + Promise.resolve(this._options.openActivity(activity)).catch(onUnexpectedError); + } + + private _showPicker(): void { + if (this._actionWidgetService.isVisible) { + return; + } + + const items: IActionListItem[] = []; + for (const category of this._categories) { + if (items.length > 0) { + items.push({ kind: ActionListItemKind.Separator, label: '' }); + } + items.push({ kind: ActionListItemKind.Header, label: category.title, group: { title: category.title } }); + for (const activity of category.activities) { + items.push({ + kind: ActionListItemKind.Action, + label: activity.label, + group: { title: '', icon: activity.icon }, + item: activity, + }); + } + } + + const triggerElement = this._button.element; + const delegate: IActionListDelegate = { + onSelect: activity => { + this._actionWidgetService.hide(); + this._openActivity(activity); + }, + onHide: () => triggerElement.focus(), + }; + this._actionWidgetService.show( + this._options.widgetId, + false, + items, + delegate, + triggerElement, + undefined, + [], + { + getAriaLabel: item => item.label ?? '', + getWidgetAriaLabel: () => this._options.getWidgetAriaLabel(), + }, + { minWidth: 220, maxWidth: 420 }, + ); + } +} diff --git a/src/vs/sessions/contrib/chat/browser/sessionBackgroundActivitiesControl.ts b/src/vs/sessions/contrib/chat/browser/sessionBackgroundActivitiesControl.ts index 71f8b51bd1f..982539f7d57 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionBackgroundActivitiesControl.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionBackgroundActivitiesControl.ts @@ -3,94 +3,81 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $ } from '../../../../base/browser/dom.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, IObservable, IReader, observableValue } from '../../../../base/common/observable.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable, IReader } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { localize } from '../../../../nls.js'; -import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; -import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { BrowserEditorInput } from '../../../../workbench/contrib/browserView/common/browserEditorInput.js'; -import { browserViewUrlMatches, BrowserViewSharingState, IBrowserViewWorkbenchService } from '../../../../workbench/contrib/browserView/common/browserView.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; import { ChatOriginKind, IChat, isActiveSessionStatus } from '../../../services/sessions/common/session.js'; import { IActiveSession } from '../../../services/sessions/common/sessionsManagement.js'; +import { ISessionActivity, ISessionActivitySummary, SessionActivityPill } from './sessionActivityPill.js'; import type { ISessionChatPillsDebugData } from './sessionChatInputToolbarDebug.js'; -import './media/sessionBackgroundActivitiesControl.css'; const SUBAGENT_LABEL_MAX_LENGTH = 30; -interface IBackgroundBrowserActivity { - readonly source: 'browser'; - readonly kind: 'browser'; - readonly input: BrowserEditorInput; - readonly label: string; +interface ISubagentActivity extends ISessionActivity { + /** The subagent chat to open, or `undefined` for a fake activity from debug data. */ + readonly chat: IChat | undefined; } -interface IBackgroundSubagentActivity { - readonly source: 'subagent'; - readonly kind: 'subagent'; - readonly chat: IChat; - readonly label: string; -} +/** + * The activities this pill lists. Further kinds join this union; once more than + * one kind can be listed at once, the summary needs a generic mixed-kind label. + */ +type IBackgroundActivity = ISubagentActivity; -interface IDebugBackgroundActivity { - readonly source: 'debug'; - readonly kind: 'browser' | 'subagent'; - readonly label: string; -} - -type IBackgroundActivity = IBackgroundBrowserActivity | IBackgroundSubagentActivity | IDebugBackgroundActivity; - -/** Combines live browsers and running subagents for the viewed chat into one compact control. */ +/** + * Lists the background activities of the viewed chat as one compact pill. Today + * those are the chat's running subagents. Browsers have their own pill, see + * `SessionBrowsersControl`. + */ export class SessionBackgroundActivitiesControl extends Disposable { readonly element: HTMLElement; readonly isVisible: IObservable; - private readonly _button: Button; - private readonly _browserListeners = this._register(new MutableDisposable()); - private readonly _isVisible = observableValue(this, false); + private readonly _pill: SessionActivityPill; private _currentSession: IActiveSession | undefined; - private _runningSubagents: readonly IBackgroundSubagentActivity[] = []; - private _activities: readonly IBackgroundActivity[] = []; - private _activitiesEnabled = false; + private _runningSubagents: readonly ISubagentActivity[] = []; private _debugData: ISessionChatPillsDebugData | undefined; constructor( private readonly _session: IObservable, private readonly _chat: IObservable, private readonly _enabled: IObservable, - @IBrowserViewWorkbenchService private readonly _browserViewService: IBrowserViewWorkbenchService, - @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService, - @IEditorService private readonly _editorService: IEditorService, + @IActionWidgetService actionWidgetService: IActionWidgetService, @ISessionsService private readonly _sessionsService: ISessionsService, ) { super(); - this.element = $('.session-background-activities.hidden'); - this.isVisible = this._isVisible; - this._button = this._register(new Button(this.element, { secondary: true, small: true, supportIcons: true, ...defaultButtonStyles })); - this._button.element.classList.add('session-background-activities-button'); - this._register(this._button.onDidClick(() => this._onDidClick())); + this._pill = this._register(new SessionActivityPill({ + className: 'session-background-activities', + widgetId: 'sessionBackgroundActivities', + getWidgetAriaLabel: () => localize('backgroundActivities.ariaLabel', "Background Activities"), + getSummary: activities => this._summary(activities), + openActivity: activity => this._openActivity(activity), + }, actionWidgetService)); + this.element = this._pill.element; + this.isVisible = this._pill.isVisible; this._register(autorun(reader => { const session = this._session.read(reader); const chat = this._chat.read(reader); + const enabled = this._enabled.read(reader); this._currentSession = session; - this._activitiesEnabled = this._enabled.read(reader); - this._runningSubagents = this._activitiesEnabled && session && chat ? this._collectRunningSubagents(session, chat, reader) : []; + this._runningSubagents = enabled && session && chat ? this._collectRunningSubagents(session, chat, reader) : []; this._refresh(); })); - this._register(this._browserViewService.onDidChangeBrowserViews(() => this._refreshBrowserListeners())); - this._refreshBrowserListeners(); } - private _collectRunningSubagents(session: IActiveSession, parentChat: IChat, reader: IReader): IBackgroundSubagentActivity[] { + setDebugData(data: ISessionChatPillsDebugData | undefined): void { + this._debugData = data; + this._refresh(); + } + + private _collectRunningSubagents(session: IActiveSession, parentChat: IChat, reader: IReader): ISubagentActivity[] { return session.chats.read(reader) .filter(chat => chat.origin?.kind === ChatOriginKind.Tool && @@ -98,9 +85,8 @@ export class SessionBackgroundActivitiesControl extends Disposable { isEqual(chat.origin.parentChat, parentChat.resource) && isActiveSessionStatus(chat.status.read(reader))) .map(chat => ({ - source: 'subagent', - kind: 'subagent', chat, + icon: Codicon.agent, label: this._subagentLabel(chat.title.read(reader)), })); } @@ -110,181 +96,24 @@ export class SessionBackgroundActivitiesControl extends Disposable { return label.length > SUBAGENT_LABEL_MAX_LENGTH ? `${label.slice(0, SUBAGENT_LABEL_MAX_LENGTH)}...` : label; } - private _refreshBrowserListeners(): void { - const store = new DisposableStore(); - this._browserListeners.value = store; - for (const input of this._browserViewService.getKnownBrowserViews().values()) { - store.add(input.onDidChangeLabel(() => this._refresh())); - } - this._refresh(); - } - private _refresh(): void { - if (this._debugData) { - this._activities = [ - ...this._debugData.browsers.map(label => ({ source: 'debug', kind: 'browser', label }) as const), - ...this._debugData.subagents.map(label => ({ source: 'debug', kind: 'subagent', label }) as const), - ]; - this._render(); - return; - } - const browserActivities = this._activitiesEnabled ? this._collectBrowserActivities() : []; - this._activities = [...browserActivities, ...this._runningSubagents]; - this._render(); + const subagents: readonly ISubagentActivity[] = this._debugData + ? this._debugData.subagents.map(label => ({ label, icon: Codicon.agent, chat: undefined })) + : this._runningSubagents; + this._pill.setCategories([{ title: localize('backgroundActivities.subagents', "Subagents"), activities: subagents }]); } - private _collectBrowserActivities(): IBackgroundBrowserActivity[] { - const session = this._currentSession; - const chat = this._chat.get(); - if (!session || !chat) { - return []; - } - - const ownerIds = new Set([chat.resource.toString()]); - for (const candidate of session.chats.get()) { - if (candidate.origin?.kind === ChatOriginKind.Tool && candidate.origin.parentChat && isEqual(candidate.origin.parentChat, chat.resource)) { - ownerIds.add(candidate.resource.toString()); - } - } - - const activities: IBackgroundBrowserActivity[] = []; - for (const input of this._browserViewService.getKnownBrowserViews().values()) { - const ownerId = input.model?.owner.sessionId; - if (ownerId && ownerIds.has(ownerId)) { - activities.push({ - source: 'browser', - kind: 'browser', - input, - label: input.title?.trim() || localize('backgroundActivities.browser', "Browser"), - }); - } - } - return activities; - } - - private _render(): void { - const count = this._activities.length; - this._isVisible.set(count > 0, undefined); - this.element.classList.toggle('hidden', count === 0); - if (count === 0) { - return; - } - - let label: string; - if (count === 1) { - const activity = this._activities[0]; - const icon = activity.kind === 'browser' ? Codicon.globe : Codicon.agent; - label = `$(${icon.id}) ${activity.label}`; - } else if (this._activities.every(activity => activity.kind === 'browser')) { - label = `$(${Codicon.globe.id}) ${localize('backgroundActivities.activeBrowsers', "{0} Active Browsers", count)} $(${Codicon.chevronDown.id})`; - } else if (this._activities.every(activity => activity.kind === 'subagent')) { - label = `$(${Codicon.agent.id}) ${localize('backgroundActivities.activeSubagents', "{0} Active Subagents", count)} $(${Codicon.chevronDown.id})`; - } else { - label = `$(${Codicon.sessionInProgress.id}) ${localize('backgroundActivities.mixed', "{0} Background Activities", count)} $(${Codicon.chevronDown.id})`; - } - - this._button.label = label; - const accessibleLabel = count === 1 - ? localize('backgroundActivities.open', "Open {0}", this._activities[0].label) - : localize('backgroundActivities.show', "Show {0} background activities", count); - this._button.setTitle(accessibleLabel); - this._button.setAriaLabel(accessibleLabel); - } - - private _onDidClick(): void { - if (this._activities.length === 1) { - void this._openActivity(this._activities[0]); - return; - } - if (this._activities.length > 1) { - this._showPicker(); - } - } - - private _showPicker(): void { - if (this._actionWidgetService.isVisible) { - return; - } - - const browsers = this._activities.filter(activity => activity.kind === 'browser'); - const subagents = this._activities.filter(activity => activity.kind === 'subagent'); - const items: IActionListItem[] = []; - const addCategory = (title: string, icon: typeof Codicon.globe, activities: readonly IBackgroundActivity[]) => { - if (activities.length === 0) { - return; - } - if (items.length > 0) { - items.push({ kind: ActionListItemKind.Separator, label: '' }); - } - items.push({ kind: ActionListItemKind.Header, label: title, group: { title } }); - for (const activity of activities) { - items.push({ - kind: ActionListItemKind.Action, - label: activity.label, - group: { title: '', icon }, - item: activity, - }); - } + private _summary(activities: readonly IBackgroundActivity[]): ISessionActivitySummary { + return { + icon: Codicon.agent, + label: localize('backgroundActivities.activeSubagents', "{0} Active Subagents", activities.length), + ariaLabel: localize('backgroundActivities.show', "Show {0} background activities", activities.length), }; - - addCategory(localize('backgroundActivities.browsers', "Browsers"), Codicon.globe, browsers); - addCategory(localize('backgroundActivities.subagents', "Subagents"), Codicon.agent, subagents); - - const triggerElement = this._button.element; - const delegate: IActionListDelegate = { - onSelect: activity => { - this._actionWidgetService.hide(); - void this._openActivity(activity); - }, - onHide: () => triggerElement.focus(), - }; - this._actionWidgetService.show( - 'sessionBackgroundActivities', - false, - items, - delegate, - triggerElement, - undefined, - [], - { - getAriaLabel: item => item.label ?? '', - getWidgetAriaLabel: () => localize('backgroundActivities.ariaLabel', "Background Activities"), - }, - { minWidth: 220, maxWidth: 420 }, - ); } - private async _openActivity(activity: IBackgroundActivity): Promise { - if (activity.source === 'debug') { - return; - } - if (activity.source === 'browser') { - const input = this._getBrowserInputToOpen(activity.input); - const existing = this._editorService.findEditors(input.resource) - .find(identifier => identifier.editor instanceof BrowserEditorInput && identifier.editor.id === input.id); - const targetGroup = existing?.groupId ?? await this._browserViewService.getPreferredGroup(); - await this._editorService.openEditor(input, undefined, targetGroup); - return; - } - if (this._currentSession) { + private _openActivity(activity: IBackgroundActivity): void { + if (activity.chat && this._currentSession) { this._sessionsService.openChat(this._currentSession, activity.chat.resource); } } - - setDebugData(data: ISessionChatPillsDebugData | undefined): void { - this._debugData = data; - this._refresh(); - } - - private _getBrowserInputToOpen(input: BrowserEditorInput): BrowserEditorInput { - const url = input.url; - if (input.model?.sharingState === BrowserViewSharingState.Shared || !url) { - return input; - } - - const activeSessionId = this._chat.get()?.resource.toString(); - const shared = [...this._browserViewService.getContextualBrowserViews({ activeSessionId }).values()] - .filter(candidate => candidate.model?.sharingState === BrowserViewSharingState.Shared && browserViewUrlMatches(candidate.url, url)); - return shared.find(candidate => candidate.url === url) ?? shared.at(0) ?? input; - } } diff --git a/src/vs/sessions/contrib/chat/browser/sessionBrowsersControl.ts b/src/vs/sessions/contrib/chat/browser/sessionBrowsersControl.ts new file mode 100644 index 00000000000..2f46abdd931 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/sessionBrowsersControl.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable, IReader } from '../../../../base/common/observable.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { localize } from '../../../../nls.js'; +import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; +import { BrowserEditorInput } from '../../../../workbench/contrib/browserView/common/browserEditorInput.js'; +import { browserViewUrlMatches, BrowserViewSharingState, IBrowserViewWorkbenchService } from '../../../../workbench/contrib/browserView/common/browserView.js'; +import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; +import { ChatOriginKind, IChat } from '../../../services/sessions/common/session.js'; +import { IActiveSession } from '../../../services/sessions/common/sessionsManagement.js'; +import { ISessionActivity, ISessionActivitySummary, SessionActivityPill } from './sessionActivityPill.js'; +import type { ISessionChatPillsDebugData } from './sessionChatInputToolbarDebug.js'; + +interface IBrowserActivity extends ISessionActivity { + /** The browser to open, or `undefined` for a fake activity from debug data. */ + readonly input: BrowserEditorInput | undefined; +} + +/** Lists the live browsers of the viewed chat (and its subagents) as one compact pill. */ +export class SessionBrowsersControl extends Disposable { + + readonly element: HTMLElement; + readonly isVisible: IObservable; + + private readonly _pill: SessionActivityPill; + private readonly _browserListeners = this._register(new MutableDisposable()); + /** Chats whose browsers belong to this pill: the viewed chat and its subagents. */ + private _ownerIds: ReadonlySet = new Set(); + private _currentChat: IChat | undefined; + private _enabledValue = false; + private _debugData: ISessionChatPillsDebugData | undefined; + + constructor( + private readonly _session: IObservable, + private readonly _chat: IObservable, + private readonly _enabled: IObservable, + @IBrowserViewWorkbenchService private readonly _browserViewService: IBrowserViewWorkbenchService, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IEditorService private readonly _editorService: IEditorService, + ) { + super(); + + this._pill = this._register(new SessionActivityPill({ + className: 'session-browsers', + widgetId: 'sessionBrowsers', + getWidgetAriaLabel: () => localize('browsers.ariaLabel', "Browsers"), + getSummary: activities => this._summary(activities), + openActivity: activity => this._openActivity(activity), + }, actionWidgetService)); + this.element = this._pill.element; + this.isVisible = this._pill.isVisible; + + this._register(autorun(reader => { + const session = this._session.read(reader); + const chat = this._chat.read(reader); + this._currentChat = chat; + this._enabledValue = this._enabled.read(reader); + // Read the chat list through the reader so browsers registered by a + // subagent show up as soon as that subagent joins the session. + this._ownerIds = session && chat ? this._collectOwnerIds(session, chat, reader) : new Set(); + this._refresh(); + })); + this._register(this._browserViewService.onDidChangeBrowserViews(() => this._refreshBrowserListeners())); + this._refreshBrowserListeners(); + } + + setDebugData(data: ISessionChatPillsDebugData | undefined): void { + this._debugData = data; + this._refresh(); + } + + private _refreshBrowserListeners(): void { + const store = new DisposableStore(); + this._browserListeners.value = store; + for (const input of this._browserViewService.getKnownBrowserViews().values()) { + store.add(input.onDidChangeLabel(() => this._refresh())); + } + this._refresh(); + } + + private _refresh(): void { + const activities = this._debugData + ? this._debugData.browsers.map(label => ({ label, icon: Codicon.globe, input: undefined })) + : this._enabledValue ? this._collectBrowserActivities() : []; + this._pill.setCategories([{ title: localize('browsers.browsers', "Browsers"), activities }]); + } + + private _summary(activities: readonly IBrowserActivity[]): ISessionActivitySummary { + return { + icon: Codicon.globe, + label: localize('browsers.activeBrowsers', "{0} Active Browsers", activities.length), + ariaLabel: localize('browsers.show', "Show {0} browsers", activities.length), + }; + } + + private _collectOwnerIds(session: IActiveSession, chat: IChat, reader: IReader): ReadonlySet { + const ownerIds = new Set([chat.resource.toString()]); + for (const candidate of session.chats.read(reader)) { + if (candidate.origin?.kind === ChatOriginKind.Tool && candidate.origin.parentChat && isEqual(candidate.origin.parentChat, chat.resource)) { + ownerIds.add(candidate.resource.toString()); + } + } + return ownerIds; + } + + private _collectBrowserActivities(): IBrowserActivity[] { + const activities: IBrowserActivity[] = []; + for (const input of this._browserViewService.getKnownBrowserViews().values()) { + const ownerId = input.model?.owner.sessionId; + if (ownerId && this._ownerIds.has(ownerId)) { + activities.push({ + input, + icon: Codicon.globe, + label: input.title?.trim() || localize('browsers.browser', "Browser"), + }); + } + } + return activities; + } + + private async _openActivity(activity: IBrowserActivity): Promise { + if (!activity.input) { + return; + } + const input = this._getBrowserInputToOpen(activity.input); + const existing = this._editorService.findEditors(input.resource) + .find(identifier => identifier.editor instanceof BrowserEditorInput && identifier.editor.id === input.id); + const targetGroup = existing?.groupId ?? await this._browserViewService.getPreferredGroup(); + await this._editorService.openEditor(input, undefined, targetGroup); + } + + private _getBrowserInputToOpen(input: BrowserEditorInput): BrowserEditorInput { + const url = input.url; + if (input.model?.sharingState === BrowserViewSharingState.Shared || !url) { + return input; + } + + const activeSessionId = this._currentChat?.resource.toString(); + const shared = [...this._browserViewService.getContextualBrowserViews({ activeSessionId }).values()] + .filter(candidate => candidate.model?.sharingState === BrowserViewSharingState.Shared && browserViewUrlMatches(candidate.url, url)); + return shared.find(candidate => candidate.url === url) ?? shared.at(0) ?? input; + } +} diff --git a/src/vs/sessions/contrib/chat/browser/sessionChatInputToolbar.ts b/src/vs/sessions/contrib/chat/browser/sessionChatInputToolbar.ts index 11e24670d33..df7ece01e19 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionChatInputToolbar.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionChatInputToolbar.ts @@ -23,6 +23,7 @@ import { IChat, isActiveSessionStatus } from '../../../services/sessions/common/ import { IActiveSession } from '../../../services/sessions/common/sessionsManagement.js'; import { LastTurnChangesMultiDiffSourceResolver } from './lastTurnChangesMultiDiffSourceResolver.js'; import { SessionBackgroundActivitiesControl } from './sessionBackgroundActivitiesControl.js'; +import { SessionBrowsersControl } from './sessionBrowsersControl.js'; import type { ISessionChatPillsDebugData } from './sessionChatInputToolbarDebug.js'; import './media/sessionChatInputToolbar.css'; @@ -87,6 +88,7 @@ export class SessionChatInputToolbar extends Disposable { /** The chat whose last-turn changes are reflected. */ private readonly _chat = observableValue('chat', undefined); private readonly _debugData = observableValue(this, undefined); + private readonly _browsers: SessionBrowsersControl; private readonly _backgroundActivities: SessionBackgroundActivitiesControl; /** The session that owns the reflected chat, from an explicit override or resolved from the chat. */ @@ -158,11 +160,15 @@ export class SessionChatInputToolbar extends Disposable { const pills = this._register(instantiationService.createInstance(ChatTurnPillsWidget, model)); this.element.appendChild(pills.element); + this._browsers = this._register(instantiationService.createInstance(SessionBrowsersControl, this._session, this._chat, turnStatusPillsEnabled)); + this.element.appendChild(this._browsers.element); + this._backgroundActivities = this._register(instantiationService.createInstance(SessionBackgroundActivitiesControl, this._session, this._chat, turnStatusPillsEnabled)); this.element.appendChild(this._backgroundActivities.element); this._register(autorun(reader => { - this.element.classList.toggle('hidden', !pills.isVisible.read(reader) && !this._backgroundActivities.isVisible.read(reader)); + const anyVisible = pills.isVisible.read(reader) || this._browsers.isVisible.read(reader) || this._backgroundActivities.isVisible.read(reader); + this.element.classList.toggle('hidden', !anyVisible); })); } @@ -190,6 +196,7 @@ export class SessionChatInputToolbar extends Disposable { setDebugData(data: ISessionChatPillsDebugData | undefined): void { this._debugData.set(data, undefined); + this._browsers.setDebugData(data); this._backgroundActivities.setDebugData(data); } diff --git a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts index 1b791f6aea9..c526a888c64 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts @@ -40,7 +40,7 @@ export class SessionsChatAccessibilityHelp implements IAccessibleViewImplementat content.push(localize('sessionsChat.voiceMode', "Start or stop Voice Mode to interact with the agent using your microphone{0}.", '')); content.push(localize('sessionsChat.micContextMenu', "To choose a microphone or turn off dictation or Voice Mode, focus the microphone button in the input toolbar and open its context menu (for example Shift+F10).")); content.push(localize('sessionsChat.contextReferences', "Type # in the chat input to attach context. Use #file to reference a file or folder, or #session to reference another agent session. Referencing a session together with the /troubleshoot command analyzes that session's logs instead of the current one. Accept a suggestion with Tab or Enter; the reference appears as a pill above the input that you can remove.")); - content.push(localize('sessionsChat.backgroundActivities', "Press Shift+Tab from the chat input to reach status pills above it, then press Enter or Space to activate a pill. A pill with multiple background activities opens a picker; use the up and down arrows to navigate, Enter to open an activity, and Escape to dismiss the picker and return focus to the pill.")); + content.push(localize('sessionsChat.backgroundActivities', "Press Shift+Tab from the chat input to reach status pills above it, then press Enter or Space to activate a pill. Live browsers appear in their own pill, and background activities such as running subagents in another. A pill with more than one entry opens a picker; use the up and down arrows to navigate, Enter to open an entry, and Escape to dismiss the picker and return focus to the pill.")); content.push(localize('sessionsChat.conversations', "When a session supports multiple chats, a New Chat button is always shown: as a labeled button in the session header while the session has a single visible chat tab, and as a compact button at the end of the chat tab strip once the session has more than one visible chat tab. Activate it to start a new chat. A Chats menu is also shown in the session header meta row, at the end of the pills, once the session has more than one committed chat or the active chat has subagents. Open it to reopen a closed chat or open a subagent: each chat is listed with a checkbox, where checked chats are shown as tabs and unchecked chats are closed (hidden).")); content.push(localize('sessionsChat.closeChat', "Activate a chat tab's close button to close (hide) that chat from the tab strip without deleting it; reopen it later from the Chats menu. The session's main chat cannot be closed.")); content.push(localize('sessionsChat.deleteChat', "To permanently delete a chat, open the chat tab's context menu and choose Delete Chat. This is destructive and cannot be undone.")); diff --git a/src/vs/sessions/contrib/chat/test/browser/sessionBackgroundActivitiesControl.test.ts b/src/vs/sessions/contrib/chat/test/browser/sessionBackgroundActivitiesControl.test.ts index 7062ad44b60..1dcdbde8785 100644 --- a/src/vs/sessions/contrib/chat/test/browser/sessionBackgroundActivitiesControl.test.ts +++ b/src/vs/sessions/contrib/chat/test/browser/sessionBackgroundActivitiesControl.test.ts @@ -5,16 +5,12 @@ import assert from 'assert'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { Event } from '../../../../../base/common/event.js'; import { constObservable } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../../platform/actionWidget/browser/actionList.js'; import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; -import { BrowserEditorInput } from '../../../../../workbench/contrib/browserView/common/browserEditorInput.js'; -import { BrowserViewSharingState, IBrowserViewModel, IBrowserViewWorkbenchService } from '../../../../../workbench/contrib/browserView/common/browserView.js'; -import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; import { ChatOriginKind, IChat, ISession, SessionStatus } from '../../../../services/sessions/common/session.js'; import { IActiveSession } from '../../../../services/sessions/common/sessionsManagement.js'; @@ -22,12 +18,6 @@ import { SessionBackgroundActivitiesControl } from '../../browser/sessionBackgro import { isNonNegativeIntegerInput, weightedRandomDebugIncrement } from '../../browser/sessionChatInputToolbarDebug.js'; interface IControlSpec { - readonly browsers?: readonly { - readonly title?: string; - readonly url?: string; - readonly owner?: 'main' | 'subagent' | 'other' | 'unowned'; - readonly sharingState?: BrowserViewSharingState; - }[]; readonly subagents?: readonly string[]; readonly subagentStatus?: SessionStatus; readonly enabled?: boolean; @@ -36,9 +26,6 @@ interface IControlSpec { interface IControlHarness { readonly control: SessionBackgroundActivitiesControl; readonly getPickerItems: () => readonly ICapturedPickerItem[]; - readonly selectPickerItem: (label: string) => void; - readonly getBrowserOpenCount: () => number; - readonly getOpenedBrowserId: () => string | undefined; readonly getOpenedChat: () => URI | undefined; } @@ -47,7 +34,6 @@ interface ICapturedPickerItem { readonly label: string; readonly category: string; readonly icon: string; - readonly select?: () => void; } function createControl(spec: IControlSpec, store: ReturnType): IControlHarness { @@ -67,66 +53,20 @@ function createControl(spec: IControlSpec, store: ReturnType { - const ownerId = browser.owner === 'subagent' - ? subagents[0]?.resource.toString() - : browser.owner === 'other' ? 'chat:other' : browser.owner === 'unowned' ? undefined : mainChat.resource.toString(); - const model = new class extends mock() { - override readonly owner = ownerId ? { mainWindowId: 1, sessionId: ownerId } : { mainWindowId: 1 }; - override readonly sharingState = browser.sharingState ?? BrowserViewSharingState.NotShared; - }(); - return new class extends mock() { - override get id(): string { return `browser-${index}`; } - override get model(): IBrowserViewModel { return model; } - override get title(): string | undefined { return browser.title; } - override get url(): string | undefined { return browser.url; } - override readonly onDidChangeLabel = Event.None; - }(); - }); - const knownBrowsers = new Map(inputs.map(input => [input.id, input])); - const browserViewService = new class extends mock() { - override readonly onDidChangeBrowserViews = Event.None; - override getKnownBrowserViews() { return knownBrowsers; } - override getContextualBrowserViews() { return knownBrowsers; } - override async getPreferredGroup() { return undefined; } - }(); - let pickerItems: ICapturedPickerItem[] = []; const actionWidgetService = new class extends mock() { override get isVisible() { return false; } override hide(): void { } - override show(_user: string, _supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate): void { - pickerItems = items.map(item => { - const value = item.item; - return { - kind: item.kind, - label: item.label ?? '', - category: item.group?.title ?? '', - icon: item.group?.icon?.id ?? '', - select: value === undefined ? undefined : () => delegate.onSelect(value), - }; - }); + override show(_user: string, _supportsPreview: boolean, items: readonly IActionListItem[], _delegate: IActionListDelegate): void { + pickerItems = items.map(item => ({ + kind: item.kind, + label: item.label ?? '', + category: item.group?.title ?? '', + icon: item.group?.icon?.id ?? '', + })); } }(); - const selectPickerItem = (label: string) => { - const item = pickerItems.find(item => item.label === label && item.select); - if (!item?.select) { - throw new Error(`Picker item '${label}' not found`); - } - item.select(); - }; - let browserOpenCount = 0; - let openedBrowserId: string | undefined; - const browserIds = new Map(inputs.map(input => [input, input.id])); - const editorService = new class extends mock() { - override findEditors() { return []; } - override async openEditor(editor: object) { - browserOpenCount++; - openedBrowserId = browserIds.get(editor); - return undefined; - } - }(); let openedChat: URI | undefined; const sessionsService = new class extends mock() { override async openChat(_session: ISession, chatUri: URI): Promise { @@ -138,24 +78,19 @@ function createControl(spec: IControlSpec, store: ReturnType pickerItems, - selectPickerItem, - getBrowserOpenCount: () => browserOpenCount, - getOpenedBrowserId: () => openedBrowserId, getOpenedChat: () => openedChat, }; } function summarize(control: SessionBackgroundActivitiesControl): { readonly text: string; readonly ariaLabel: string | null; readonly icons: readonly string[] } { - const button = control.element.querySelector('.session-background-activities-button')!; + const button = control.element.querySelector('.session-activity-pill-button')!; const knownIcons = [Codicon.globe, Codicon.agent, Codicon.sessionInProgress, Codicon.chevronDown]; return { text: button.textContent ?? '', @@ -165,6 +100,10 @@ function summarize(control: SessionBackgroundActivitiesControl): { readonly text }; } +function click(control: SessionBackgroundActivitiesControl): void { + control.element.querySelector('.session-activity-pill-button')!.click(); +} + suite('SessionBackgroundActivitiesControl', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -200,28 +139,22 @@ suite('SessionBackgroundActivitiesControl', () => { }); }); - test('renders single and aggregate labels, icons, fallback, and subagent truncation', () => { + test('renders single and aggregate labels, icons, and subagent truncation', () => { const cases: IControlSpec[] = [ - { browsers: [{ title: 'Visual Studio Code' }] }, - { browsers: [{}] }, + { subagents: ['Research'] }, { subagents: ['Investigate the authentication failure in production'] }, - { browsers: [{ title: 'Docs' }, { title: 'Preview' }] }, { subagents: ['Research', 'Review'] }, - { browsers: [{ title: 'Preview' }], subagents: ['Research'] }, ]; - const disabled = createControl({ browsers: [{ title: 'Hidden browser' }], subagents: ['Research'], enabled: false }, store); + const disabled = createControl({ subagents: ['Research'], enabled: false }, store); assert.deepStrictEqual({ enabled: cases.map(spec => summarize(createControl(spec, store).control)), disabledVisible: disabled.control.isVisible.get(), }, { enabled: [ - { text: 'Visual Studio Code', ariaLabel: 'Open Visual Studio Code', icons: ['globe'] }, - { text: 'Browser', ariaLabel: 'Open Browser', icons: ['globe'] }, + { text: 'Research', ariaLabel: 'Open Research', icons: ['agent'] }, { text: 'Investigate the authentication...', ariaLabel: 'Open Investigate the authentication...', icons: ['agent'] }, - { text: '2 Active Browsers', ariaLabel: 'Show 2 background activities', icons: ['globe', 'chevron-down'] }, { text: '2 Active Subagents', ariaLabel: 'Show 2 background activities', icons: ['agent', 'chevron-down'] }, - { text: '2 Background Activities', ariaLabel: 'Show 2 background activities', icons: ['session-in-progress', 'chevron-down'] }, ], disabledVisible: false, }); @@ -239,7 +172,7 @@ suite('SessionBackgroundActivitiesControl', () => { }); }); - test('debug data forces activities while disabled and clears cleanly', () => { + test('ignores browsers from debug data and shows only fake subagents', () => { const harness = createControl({ enabled: false }, store); harness.control.setDebugData({ stats: { files: 2, insertions: 10, deletions: 3 }, @@ -256,104 +189,28 @@ suite('SessionBackgroundActivitiesControl', () => { harness.control.setDebugData(undefined); assert.deepStrictEqual({ forced, visibleAfterClear: harness.control.isVisible.get() }, { - forced: { - text: '2 Background Activities', - ariaLabel: 'Show 2 background activities', - icons: ['session-in-progress', 'chevron-down'], - }, + forced: { text: 'Debug Subagent', ariaLabel: 'Open Debug Subagent', icons: ['agent'] }, visibleAfterClear: false, }); }); - test('groups browsers before subagents with category headers, icons, and labels', async () => { - const harness = createControl({ - browsers: [ - { title: 'Docs' }, - { title: 'Subagent Preview', owner: 'subagent' }, - { title: 'Other Session', owner: 'other' }, - ], - subagents: ['Research', 'Review'], - }, store); + test('lists subagents in a picker under a category header', () => { + const harness = createControl({ subagents: ['Research', 'Review'] }, store); - harness.control.element.querySelector('.session-background-activities-button')!.click(); - harness.selectPickerItem('Subagent Preview'); - await Promise.resolve(); + click(harness.control); - assert.deepStrictEqual({ - items: harness.getPickerItems().map(({ select: _select, ...item }) => item), - openedBrowser: harness.getOpenedBrowserId(), - }, { - items: [ - { kind: ActionListItemKind.Header, label: 'Browsers', category: 'Browsers', icon: '' }, - { kind: ActionListItemKind.Action, label: 'Docs', category: '', icon: Codicon.globe.id }, - { kind: ActionListItemKind.Action, label: 'Subagent Preview', category: '', icon: Codicon.globe.id }, - { kind: ActionListItemKind.Separator, label: '', category: '', icon: '' }, - { kind: ActionListItemKind.Header, label: 'Subagents', category: 'Subagents', icon: '' }, - { kind: ActionListItemKind.Action, label: 'Research', category: '', icon: Codicon.agent.id }, - { kind: ActionListItemKind.Action, label: 'Review', category: '', icon: Codicon.agent.id }, - ], - openedBrowser: 'browser-1', - }); + assert.deepStrictEqual(harness.getPickerItems(), [ + { kind: ActionListItemKind.Header, label: 'Subagents', category: 'Subagents', icon: '' }, + { kind: ActionListItemKind.Action, label: 'Research', category: '', icon: Codicon.agent.id }, + { kind: ActionListItemKind.Action, label: 'Review', category: '', icon: Codicon.agent.id }, + ]); }); - test('opens a single browser or subagent directly', async () => { - const browser = createControl({ browsers: [{ title: 'Preview' }] }, store); - browser.control.element.querySelector('.session-background-activities-button')!.click(); - await Promise.resolve(); + test('opens a single subagent directly', () => { + const harness = createControl({ subagents: ['Research'] }, store); - const subagent = createControl({ subagents: ['Research'] }, store); - subagent.control.element.querySelector('.session-background-activities-button')!.click(); + click(harness.control); - assert.deepStrictEqual({ - browserOpenCount: browser.getBrowserOpenCount(), - browserOpenedChat: browser.getOpenedChat()?.toString(), - subagentBrowserOpenCount: subagent.getBrowserOpenCount(), - subagentOpenedChat: subagent.getOpenedChat()?.toString(), - }, { - browserOpenCount: 1, - browserOpenedChat: undefined, - subagentBrowserOpenCount: 0, - subagentOpenedChat: 'chat:subagent-0', - }); - }); - - test('prefers a shared browser for the same destination and otherwise opens the normal browser', async () => { - const sharedHost = createControl({ - browsers: [ - { title: 'Normal', url: 'https://example.com/start' }, - { title: 'Shared Host', url: 'https://example.com/live', owner: 'unowned', sharingState: BrowserViewSharingState.Shared }, - ], - }, store); - sharedHost.control.element.querySelector('.session-background-activities-button')!.click(); - await Promise.resolve(); - - const sharedExact = createControl({ - browsers: [ - { title: 'Normal', url: 'https://example.com/start' }, - { title: 'Shared Host', url: 'https://example.com/live', owner: 'unowned', sharingState: BrowserViewSharingState.Shared }, - { title: 'Shared Exact', url: 'https://example.com/start', owner: 'unowned', sharingState: BrowserViewSharingState.Shared }, - ], - }, store); - sharedExact.control.element.querySelector('.session-background-activities-button')!.click(); - await Promise.resolve(); - - const fallback = createControl({ - browsers: [ - { title: 'Normal', url: 'https://example.com/start' }, - { title: 'Unrelated Shared', url: 'https://other.test/live', owner: 'unowned', sharingState: BrowserViewSharingState.Shared }, - ], - }, store); - fallback.control.element.querySelector('.session-background-activities-button')!.click(); - await Promise.resolve(); - - assert.deepStrictEqual({ - sharedHost: sharedHost.getOpenedBrowserId(), - sharedExact: sharedExact.getOpenedBrowserId(), - fallback: fallback.getOpenedBrowserId(), - }, { - sharedHost: 'browser-1', - sharedExact: 'browser-2', - fallback: 'browser-0', - }); + assert.deepStrictEqual(harness.getOpenedChat()?.toString(), 'chat:subagent-0'); }); }); diff --git a/src/vs/sessions/contrib/chat/test/browser/sessionBrowsersControl.test.ts b/src/vs/sessions/contrib/chat/test/browser/sessionBrowsersControl.test.ts new file mode 100644 index 00000000000..dbc12585cfb --- /dev/null +++ b/src/vs/sessions/contrib/chat/test/browser/sessionBrowsersControl.test.ts @@ -0,0 +1,301 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Codicon } from '../../../../../base/common/codicons.js'; +import { Event } from '../../../../../base/common/event.js'; +import { constObservable, observableValue } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../../platform/actionWidget/browser/actionList.js'; +import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; +import { BrowserEditorInput } from '../../../../../workbench/contrib/browserView/common/browserEditorInput.js'; +import { BrowserViewSharingState, IBrowserViewModel, IBrowserViewWorkbenchService } from '../../../../../workbench/contrib/browserView/common/browserView.js'; +import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; +import { ChatOriginKind, IChat, SessionStatus } from '../../../../services/sessions/common/session.js'; +import { IActiveSession } from '../../../../services/sessions/common/sessionsManagement.js'; +import { SessionBrowsersControl } from '../../browser/sessionBrowsersControl.js'; + +interface IControlSpec { + readonly browsers?: readonly { + readonly title?: string; + readonly url?: string; + readonly owner?: 'main' | 'subagent' | 'other' | 'unowned'; + readonly sharingState?: BrowserViewSharingState; + }[]; + readonly enabled?: boolean; + /** Start with only the main chat, so the subagent can be added later. */ + readonly withoutSubagent?: boolean; +} + +interface IControlHarness { + readonly control: SessionBrowsersControl; + readonly getPickerItems: () => readonly ICapturedPickerItem[]; + readonly selectPickerItem: (label: string) => void; + readonly getBrowserOpenCount: () => number; + readonly getOpenedBrowserId: () => string | undefined; + readonly addSubagent: () => void; +} + +interface ICapturedPickerItem { + readonly kind: ActionListItemKind; + readonly label: string; + readonly category: string; + readonly icon: string; + readonly select?: () => void; +} + +function createControl(spec: IControlSpec, store: ReturnType): IControlHarness { + const mainChat = new class extends mock() { + override readonly resource = URI.parse('chat:main'); + override readonly title = constObservable('Main'); + override readonly status = constObservable(SessionStatus.InProgress); + }(); + const subagent = new class extends mock() { + override readonly resource = URI.parse('chat:subagent-0'); + override readonly title = constObservable('Research'); + override readonly status = constObservable(SessionStatus.InProgress); + override readonly origin = { kind: ChatOriginKind.Tool, parentChat: mainChat.resource }; + }(); + const chats = observableValue('chats', spec.withoutSubagent ? [mainChat] : [mainChat, subagent]); + const session = new class extends mock() { + override readonly resource = URI.parse('session:main'); + override readonly chats = chats; + }(); + + const inputs = (spec.browsers ?? []).map((browser, index) => { + const ownerId = browser.owner === 'subagent' + ? subagent.resource.toString() + : browser.owner === 'other' ? 'chat:other' : browser.owner === 'unowned' ? undefined : mainChat.resource.toString(); + const model = new class extends mock() { + override readonly owner = ownerId ? { mainWindowId: 1, sessionId: ownerId } : { mainWindowId: 1 }; + override readonly sharingState = browser.sharingState ?? BrowserViewSharingState.NotShared; + }(); + return new class extends mock() { + override get id(): string { return `browser-${index}`; } + override get model(): IBrowserViewModel { return model; } + override get title(): string | undefined { return browser.title; } + override get url(): string | undefined { return browser.url; } + override readonly onDidChangeLabel = Event.None; + }(); + }); + const knownBrowsers = new Map(inputs.map(input => [input.id, input])); + const browserViewService = new class extends mock() { + override readonly onDidChangeBrowserViews = Event.None; + override getKnownBrowserViews() { return knownBrowsers; } + override getContextualBrowserViews() { return knownBrowsers; } + override async getPreferredGroup() { return undefined; } + }(); + + let pickerItems: ICapturedPickerItem[] = []; + const actionWidgetService = new class extends mock() { + override get isVisible() { return false; } + override hide(): void { } + override show(_user: string, _supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate): void { + pickerItems = items.map(item => { + const value = item.item; + return { + kind: item.kind, + label: item.label ?? '', + category: item.group?.title ?? '', + icon: item.group?.icon?.id ?? '', + select: value === undefined ? undefined : () => delegate.onSelect(value), + }; + }); + } + }(); + const selectPickerItem = (label: string) => { + const item = pickerItems.find(item => item.label === label && item.select); + if (!item?.select) { + throw new Error(`Picker item '${label}' not found`); + } + item.select(); + }; + + let browserOpenCount = 0; + let openedBrowserId: string | undefined; + const browserIds = new Map(inputs.map(input => [input, input.id])); + const editorService = new class extends mock() { + override findEditors() { return []; } + override async openEditor(editor: object) { + browserOpenCount++; + openedBrowserId = browserIds.get(editor); + return undefined; + } + }(); + + const control = store.add(new SessionBrowsersControl( + constObservable(session), + constObservable(mainChat), + constObservable(spec.enabled ?? true), + browserViewService, + actionWidgetService, + editorService, + )); + + return { + control, + getPickerItems: () => pickerItems, + selectPickerItem, + getBrowserOpenCount: () => browserOpenCount, + getOpenedBrowserId: () => openedBrowserId, + addSubagent: () => chats.set([mainChat, subagent], undefined), + }; +} + +function summarize(control: SessionBrowsersControl): { readonly text: string; readonly ariaLabel: string | null; readonly icons: readonly string[] } { + const button = control.element.querySelector('.session-activity-pill-button')!; + const knownIcons = [Codicon.globe, Codicon.agent, Codicon.sessionInProgress, Codicon.chevronDown]; + return { + text: button.textContent ?? '', + ariaLabel: button.getAttribute('aria-label'), + icons: [...button.querySelectorAll('.codicon')] + .map(element => knownIcons.find(icon => element.classList.contains(`codicon-${icon.id}`))?.id ?? 'unknown'), + }; +} + +function click(control: SessionBrowsersControl): void { + control.element.querySelector('.session-activity-pill-button')!.click(); +} + +suite('SessionBrowsersControl', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('renders single and aggregate labels, icons, and fallback', () => { + const cases: IControlSpec[] = [ + { browsers: [{ title: 'Visual Studio Code' }] }, + { browsers: [{}] }, + { browsers: [{ title: 'Docs' }, { title: 'Preview' }] }, + ]; + const disabled = createControl({ browsers: [{ title: 'Hidden browser' }], enabled: false }, store); + + assert.deepStrictEqual({ + enabled: cases.map(spec => summarize(createControl(spec, store).control)), + disabledVisible: disabled.control.isVisible.get(), + }, { + enabled: [ + { text: 'Visual Studio Code', ariaLabel: 'Open Visual Studio Code', icons: ['globe'] }, + { text: 'Browser', ariaLabel: 'Open Browser', icons: ['globe'] }, + { text: '2 Active Browsers', ariaLabel: 'Show 2 browsers', icons: ['globe', 'chevron-down'] }, + ], + disabledVisible: false, + }); + }); + + test('debug data forces browsers while disabled and clears cleanly', () => { + const harness = createControl({ enabled: false }, store); + harness.control.setDebugData({ + stats: { files: 2, insertions: 10, deletions: 3 }, + markdownFiles: ['README.md'], + browsers: ['Debug Browser'], + subagents: ['Debug Subagent'], + ciFailed: 2, + ciPending: 1, + prFeedback: 3, + agentFeedback: 4, + autoIncrementChanges: false, + }); + const forced = summarize(harness.control); + harness.control.setDebugData(undefined); + + assert.deepStrictEqual({ forced, visibleAfterClear: harness.control.isVisible.get() }, { + forced: { text: 'Debug Browser', ariaLabel: 'Open Debug Browser', icons: ['globe'] }, + visibleAfterClear: false, + }); + }); + + test('lists browsers of the chat and its subagents, but not of other chats', async () => { + const harness = createControl({ + browsers: [ + { title: 'Docs' }, + { title: 'Subagent Preview', owner: 'subagent' }, + { title: 'Other Session', owner: 'other' }, + ], + }, store); + + click(harness.control); + harness.selectPickerItem('Subagent Preview'); + await Promise.resolve(); + + assert.deepStrictEqual({ + items: harness.getPickerItems().map(({ select: _select, ...item }) => item), + openedBrowser: harness.getOpenedBrowserId(), + }, { + items: [ + { kind: ActionListItemKind.Header, label: 'Browsers', category: 'Browsers', icon: '' }, + { kind: ActionListItemKind.Action, label: 'Docs', category: '', icon: Codicon.globe.id }, + { kind: ActionListItemKind.Action, label: 'Subagent Preview', category: '', icon: Codicon.globe.id }, + ], + openedBrowser: 'browser-1', + }); + }); + + test('shows a subagent browser registered before the subagent joins the session', () => { + const harness = createControl({ browsers: [{ title: 'Subagent Preview', owner: 'subagent' }], withoutSubagent: true }, store); + const beforeJoin = harness.control.isVisible.get(); + harness.addSubagent(); + + assert.deepStrictEqual({ beforeJoin, afterJoin: summarize(harness.control) }, { + beforeJoin: false, + afterJoin: { text: 'Subagent Preview', ariaLabel: 'Open Subagent Preview', icons: ['globe'] }, + }); + }); + + test('opens a single browser directly', async () => { + const harness = createControl({ browsers: [{ title: 'Preview' }] }, store); + click(harness.control); + await Promise.resolve(); + + assert.deepStrictEqual({ + openCount: harness.getBrowserOpenCount(), + openedBrowser: harness.getOpenedBrowserId(), + }, { + openCount: 1, + openedBrowser: 'browser-0', + }); + }); + + test('prefers a shared browser for the same destination and otherwise opens the normal browser', async () => { + const sharedHost = createControl({ + browsers: [ + { title: 'Normal', url: 'https://example.com/start' }, + { title: 'Shared Host', url: 'https://example.com/live', owner: 'unowned', sharingState: BrowserViewSharingState.Shared }, + ], + }, store); + click(sharedHost.control); + await Promise.resolve(); + + const sharedExact = createControl({ + browsers: [ + { title: 'Normal', url: 'https://example.com/start' }, + { title: 'Shared Host', url: 'https://example.com/live', owner: 'unowned', sharingState: BrowserViewSharingState.Shared }, + { title: 'Shared Exact', url: 'https://example.com/start', owner: 'unowned', sharingState: BrowserViewSharingState.Shared }, + ], + }, store); + click(sharedExact.control); + await Promise.resolve(); + + const fallback = createControl({ + browsers: [ + { title: 'Normal', url: 'https://example.com/start' }, + { title: 'Unrelated Shared', url: 'https://other.test/live', owner: 'unowned', sharingState: BrowserViewSharingState.Shared }, + ], + }, store); + click(fallback.control); + await Promise.resolve(); + + assert.deepStrictEqual({ + sharedHost: sharedHost.getOpenedBrowserId(), + sharedExact: sharedExact.getOpenedBrowserId(), + fallback: fallback.getOpenedBrowserId(), + }, { + sharedHost: 'browser-1', + sharedExact: 'browser-2', + fallback: 'browser-0', + }); + }); +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/sessionChatInputToolbar.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/sessionChatInputToolbar.fixture.ts index 71897ecc5b4..90068f31d24 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/sessionChatInputToolbar.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/sessionChatInputToolbar.fixture.ts @@ -245,7 +245,7 @@ export default defineThemedFixtureGroup({ path: 'sessions/' }, { })), }), - // --- Background activity pill ------------------------------------------ + // --- Browser and background activity pills ------------------------------ SessionChatPills_BackgroundBrowser: defineComponentFixture({ render: (ctx) => renderPills(ctx, createMockSession({ browsers: [{ title: 'Visual Studio Code' }] })), From fa6217ecb59b750bf9fbf2644cc006b327f84e7b Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 30 Jul 2026 17:24:23 -0400 Subject: [PATCH 29/86] Suppress keybindings and filtering during IME composition (#328269) * Agent Host changes for lramos15/agents/investigate-issue-318977-fix * Use KEY_IN_COMPOSITION as the single IME signal Drop the `isComposing` member from both `IKeyboardEvent` interfaces and rely on the normalized key code instead. `StandardKeyboardEvent` already reports `KEY_IN_COMPOSITION` for every composing keystroke, so a parallel `isComposing` flag was a second source of truth for the same fact. It also broke the standalone editor build: `@internal` members are stripped from `out-editor-src`, which left `base`'s interface without the property while `platform`'s still required it, so the two `IKeyboardEvent` types stopped being assignable (5 errors in editor-distro). Using the key code fixes that build, keeps `monaco.d.ts` unchanged, and reverts the `isComposing: false` churn from the keyboard mapper tests. Also cancel any in-flight dynamic filter when a composition starts: a request issued for the previous value could otherwise resolve mid-composition and splice and re-layout the list underneath the IME candidate window. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4ba1417-a152-448b-af4b-1a5017b949e8 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4ba1417-a152-448b-af4b-1a5017b949e8 --- src/vs/base/browser/keyboardEvent.ts | 10 ++- .../base/test/browser/keyboardEvent.test.ts | 64 +++++++++++++++++++ .../contrib/find/browser/findController.ts | 4 +- .../editor/contrib/rename/browser/rename.ts | 4 +- .../actionWidget/browser/actionList.ts | 33 ++++++++-- .../test/browser/actionList.test.ts | 37 +++++++++++ .../browser/contextScopedHistoryWidget.ts | 2 - .../common/abstractKeybindingService.ts | 27 ++++++++ .../common/abstractKeybindingService.test.ts | 53 +++++++++++++-- .../quickinput/browser/quickInputActions.ts | 3 +- 10 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 src/vs/base/test/browser/keyboardEvent.test.ts diff --git a/src/vs/base/browser/keyboardEvent.ts b/src/vs/base/browser/keyboardEvent.ts index 6b675d06535..b7c24be7290 100644 --- a/src/vs/base/browser/keyboardEvent.ts +++ b/src/vs/base/browser/keyboardEvent.ts @@ -150,9 +150,17 @@ export class StandardKeyboardEvent implements IKeyboardEvent { this.altKey = e.altKey; this.metaKey = e.metaKey; this.altGraphKey = e.getModifierState?.('AltGraph'); - this.keyCode = extractKeyCode(e); this.code = e.code; + // Browsers are inconsistent while an IME composition is in flight: most keystrokes arrive as + // `keyCode: 229` (which maps to `KEY_IN_COMPOSITION`), but some platform/IME combinations + // report the real key code for keys the IME owns - notably the Enter that commits a + // composition, but also Space, Escape and the arrows used to pick candidates. Normalize to + // `KEY_IN_COMPOSITION` so that "the IME owns this keystroke" has a single representation + // that `equals()`, direct `keyCode` readers and keybinding resolution all understand, + // instead of acting on a key the user never directed at the application. + this.keyCode = e.isComposing ? KeyCode.KEY_IN_COMPOSITION : extractKeyCode(e); + // console.info(e.type + ": keyCode: " + e.keyCode + ", which: " + e.which + ", charCode: " + e.charCode + ", detail: " + e.detail + " ====> " + this.keyCode + ' -- ' + KeyCode[this.keyCode]); this.ctrlKey = this.ctrlKey || this.keyCode === KeyCode.Ctrl; diff --git a/src/vs/base/test/browser/keyboardEvent.test.ts b/src/vs/base/test/browser/keyboardEvent.test.ts new file mode 100644 index 00000000000..50746c73910 --- /dev/null +++ b/src/vs/base/test/browser/keyboardEvent.test.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 assert from 'assert'; +import { StandardKeyboardEvent } from '../../browser/keyboardEvent.js'; +import { KeyCode, KeyMod } from '../../common/keyCodes.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../common/utils.js'; + +function keydown(init: KeyboardEventInit & { keyCode: number }): StandardKeyboardEvent { + // `keyCode` is legacy but is what `StandardKeyboardEvent` reads, so it has to be set explicitly. + const event = new KeyboardEvent('keydown', init); + Object.defineProperty(event, 'keyCode', { get: () => init.keyCode }); + return new StandardKeyboardEvent(event); +} + +suite('StandardKeyboardEvent', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('reports the pressed key when no composition is in progress', () => { + const event = keydown({ keyCode: 13, isComposing: false }); + assert.deepStrictEqual( + [event.keyCode === KeyCode.Enter, event.equals(KeyCode.Enter)], + [true, true] + ); + }); + + test('normalizes the key code to KEY_IN_COMPOSITION while composing', () => { + // Some platform/IME combinations report the real key code (rather than 229) for the Enter + // that commits a composition. Normalizing means both `equals()` callers and the many + // handlers that compare `keyCode` directly stop seeing a key the user never aimed at them. + const event = keydown({ keyCode: 13, isComposing: true }); + assert.deepStrictEqual( + [event.keyCode === KeyCode.KEY_IN_COMPOSITION, event.equals(KeyCode.Enter)], + [true, false] + ); + }); + + test('normalizes modified keybindings while composing too', () => { + const event = keydown({ keyCode: 13, ctrlKey: true, isComposing: true }); + assert.strictEqual(event.equals(KeyMod.CtrlCmd | KeyCode.Enter), false); + }); + + test('keeps matching KEY_IN_COMPOSITION while composing', () => { + // Composition-aware callers ask about KEY_IN_COMPOSITION explicitly; the editor relies on + // this to detect IME input, so it has to keep working for both key code shapes. + for (const keyCode of [229, 13]) { + const event = keydown({ keyCode, isComposing: true }); + assert.deepStrictEqual( + [keyCode, event.equals(KeyCode.KEY_IN_COMPOSITION)], + [keyCode, true] + ); + } + }); + + test('resolves to a chord that matches no keybinding while composing', () => { + // Keybinding resolution goes through `toKeyCodeChord()` rather than `equals()`, so it needs + // the same normalization to avoid running commands mid-composition. + const event = keydown({ keyCode: 13, isComposing: true }); + assert.strictEqual(event.toKeyCodeChord().keyCode, KeyCode.KEY_IN_COMPOSITION); + }); +}); diff --git a/src/vs/editor/contrib/find/browser/findController.ts b/src/vs/editor/contrib/find/browser/findController.ts index c504148633d..7fb80ef3550 100644 --- a/src/vs/editor/contrib/find/browser/findController.ts +++ b/src/vs/editor/contrib/find/browser/findController.ts @@ -1074,7 +1074,7 @@ registerEditorCommand(new FindCommand({ handler: x => x.closeFindWidget(), kbOpts: { weight: KeybindingWeight.EditorContrib + 5, - kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')), + kbExpr: EditorContextKeys.focus, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] } @@ -1167,7 +1167,7 @@ registerEditorCommand(new FindCommand({ handler: x => x.replace(), kbOpts: { weight: KeybindingWeight.EditorContrib + 5, - kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED, EditorContextKeys.isComposing.negate()), + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED), primary: KeyCode.Enter } })); diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index cf675f54cf4..28b05b0d37e 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -423,7 +423,7 @@ registerEditorCommand(new RenameCommand({ handler: x => x.acceptRenameInput(false), kbOpts: { weight: KeybindingWeight.EditorContrib + 99, - kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')), + kbExpr: EditorContextKeys.focus, primary: KeyCode.Enter } })); @@ -434,7 +434,7 @@ registerEditorCommand(new RenameCommand({ handler: x => x.acceptRenameInput(true), kbOpts: { weight: KeybindingWeight.EditorContrib + 99, - kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')), + kbExpr: EditorContextKeys.focus, primary: KeyMod.CtrlCmd + KeyCode.Enter } })); diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index a5135121018..3f06fdc1eae 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -668,6 +668,7 @@ export class ActionListWidget extends Disposable { private readonly _collapsedSections = new Set(); private _filterText = ''; + private _imeSessionInProgress = false; private _suppressHover = false; private _hasLaidOut = false; private readonly _filterInput: HTMLInputElement | undefined; @@ -845,10 +846,34 @@ export class ActionListWidget extends Disposable { filterActionBar.push(filterActions, { icon: true, label: false }); } - this._register(dom.addDisposableListener(this._filterInput, 'input', () => { - this._filterText = this._filterInput!.value; + // While an IME composition is running the input holds intermediate text (e.g. pinyin) + // which must not drive the filter: re-filtering splices the list, re-highlights a row and + // re-layouts the popup, all of which disrupt the composition and the IME candidate window. + // Filter once the composition commits instead. + const onFilterValueChanged = () => { + const value = this._filterInput!.value; + // `compositionend` and the `input` event that follows it both land here (and browsers + // disagree on their order), so only filter when the text actually changed. + if (this._imeSessionInProgress || value === this._filterText) { + return; + } + this._filterText = value; this._applyOrUpdateFilter(); + }; + + this._register(dom.addDisposableListener(this._filterInput, 'compositionstart', () => { + this._imeSessionInProgress = true; + // A dynamic filter request issued for the previous value can still be in flight. + // Letting it resolve now would splice and re-layout the list underneath the IME + // candidate window - the very disruption this guard exists to prevent. The + // committed value starts a fresh request from `compositionend`. + this._filterCts.value?.cancel(); })); + this._register(dom.addDisposableListener(this._filterInput, 'compositionend', () => { + this._imeSessionInProgress = false; + onFilterValueChanged(); + })); + this._register(dom.addDisposableListener(this._filterInput, 'input', onFilterValueChanged)); } if (this._options?.secondaryHeading) { @@ -924,7 +949,7 @@ export class ActionListWidget extends Disposable { // ArrowRight opens submenu for the focused item and moves focus into it this._register(dom.addDisposableListener(this.domNode, 'keydown', (e: KeyboardEvent) => { - if (e.key === 'ArrowRight') { + if (e.key === 'ArrowRight' && !e.isComposing) { const focused = this._list.getFocus(); if (focused.length > 0) { const element = this._list.element(focused[0]); @@ -945,7 +970,7 @@ export class ActionListWidget extends Disposable { if (this._filterInput) { this._register(dom.addDisposableListener(this.domNode, 'keydown', (e: KeyboardEvent) => { if (this._filterInput && !dom.isActiveElement(this._filterInput) - && e.key.length === 1 && e.key !== ' ' && !e.ctrlKey && !e.metaKey && !e.altKey) { + && !e.isComposing && e.key.length === 1 && e.key !== ' ' && !e.ctrlKey && !e.metaKey && !e.altKey) { this._filterInput.focus(); this._filterInput.value = e.key; this._filterText = e.key; diff --git a/src/vs/platform/actionWidget/test/browser/actionList.test.ts b/src/vs/platform/actionWidget/test/browser/actionList.test.ts index 23fd5a60742..2af86401e5b 100644 --- a/src/vs/platform/actionWidget/test/browser/actionList.test.ts +++ b/src/vs/platform/actionWidget/test/browser/actionList.test.ts @@ -203,6 +203,43 @@ suite('ActionListWidget', () => { assert.ok(widget.domNode.textContent?.includes('ma-fresh-result')); }); + test('does not filter while an IME composition is in progress', () => { + const filters: string[] = []; + const widget = createActionListWidget(disposables, { + onFilter: async filter => { + filters.push(filter); + return [action(`result-${filter}`)]; + }, + }); + + assert.ok(widget.filterInput); + widget.filterInput.dispatchEvent(new Event('compositionstart')); + typeFilter(widget, 'd'); + typeFilter(widget, 'deepseek'); + widget.filterInput.value = 'DeepSeek'; + widget.filterInput.dispatchEvent(new Event('compositionend')); + // Chromium fires a trailing `input` for the committed text, which must not re-filter. + typeFilter(widget, 'DeepSeek'); + + assert.deepStrictEqual(filters, ['DeepSeek']); + }); + + test('cancels an in-flight dynamic filter when a composition starts', async () => { + const pending = new DeferredPromise[]>(); + const widget = createActionListWidget(disposables, { + onFilter: () => pending.p, + }); + + typeFilter(widget, 'd'); + assert.ok(widget.filterInput); + widget.filterInput.dispatchEvent(new Event('compositionstart')); + + // Resolving now must not splice/re-layout the list underneath the IME candidate window. + pending.complete([action('stale-result')]); + await timeout(0); + assert.ok(!widget.domNode.textContent?.includes('stale-result')); + }); + test('batches row width writes before reading layout', () => { const widget = createActionListWidget(disposables, { items: [ diff --git a/src/vs/platform/history/browser/contextScopedHistoryWidget.ts b/src/vs/platform/history/browser/contextScopedHistoryWidget.ts index 4c401062680..18eda7b938e 100644 --- a/src/vs/platform/history/browser/contextScopedHistoryWidget.ts +++ b/src/vs/platform/history/browser/contextScopedHistoryWidget.ts @@ -114,7 +114,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: ContextKeyExpr.and( ContextKeyExpr.has(HistoryNavigationWidgetFocusContext), ContextKeyExpr.equals(HistoryNavigationBackwardsEnablementContext, true), - ContextKeyExpr.not('isComposing'), historyNavigationVisible.isEqualTo(false), ), primary: KeyCode.UpArrow, @@ -130,7 +129,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: ContextKeyExpr.and( ContextKeyExpr.has(HistoryNavigationWidgetFocusContext), ContextKeyExpr.equals(HistoryNavigationForwardsEnablementContext, true), - ContextKeyExpr.not('isComposing'), historyNavigationVisible.isEqualTo(false), ), primary: KeyCode.DownArrow, diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 19ec19eaf50..6af73a017c7 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -30,6 +30,15 @@ interface CurrentChord { const HIGH_FREQ_COMMANDS = /^(cursor|delete|undo|redo|tab|editor\.action\.clipboard)/; +/** + * Whether the keystroke belongs to an in-flight IME composition. `StandardKeyboardEvent` normalizes + * every composing keystroke to {@link KeyCode.KEY_IN_COMPOSITION}, including the platform/IME + * combinations that would otherwise report the real key code for keys the IME owns. + */ +function isKeyInComposition(e: IKeyboardEvent): boolean { + return e.keyCode === KeyCode.KEY_IN_COMPOSITION; +} + export abstract class AbstractKeybindingService extends Disposable implements IKeybindingService { public _serviceBrand: undefined; @@ -139,6 +148,13 @@ export abstract class AbstractKeybindingService extends Disposable implements IK // TODO@ulugbekna: this fn doesn't seem to take into account single-modifier keybindings, eg `shift shift` public softDispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): ResolutionResult { this._log(`/ Soft dispatching keyboard event`); + if (isKeyInComposition(e)) { + // Must agree with `_dispatch`: callers use this to decide whether the workbench will + // claim the key, and a "yes" here followed by a "no" there would drop the keystroke on + // the floor - stopping the widget (e.g. the terminal) from passing it to the IME. + this._log(`\\ Keyboard event is part of an IME composition`); + return NoMatchingKb; + } const keybinding = this.resolveKeyboardEvent(e); if (keybinding.hasMultipleChords()) { console.warn('keyboard event should not be mapped to multiple chords'); @@ -219,10 +235,21 @@ export abstract class AbstractKeybindingService extends Disposable implements IK } protected _dispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean { + if (isKeyInComposition(e)) { + // The keystroke belongs to the IME, which owns Enter (commit), Space and the arrows + // (candidate selection) and Escape (cancel) for the duration of the composition. + // Dispatching would run commands the user never invoked - e.g. accepting a picker or + // submitting a form while they are still choosing characters. + this._log(`+ Ignoring keybinding dispatch because an IME composition is in progress.`); + return false; + } return this._doDispatch(this.resolveKeyboardEvent(e), target, /*isSingleModiferChord*/false); } protected _singleModifierDispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean { + if (isKeyInComposition(e)) { + return false; + } const keybinding = this.resolveKeyboardEvent(e); const [singleModifier,] = keybinding.getSingleModifierDispatchChords(); diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 372e4f312bf..bf73df3865b 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -13,7 +13,7 @@ import { ICommandService } from '../../../commands/common/commands.js'; import { ContextKeyExpr, ContextKeyExpression, IContext, IContextKeyService, IContextKeyServiceTarget } from '../../../contextkey/common/contextkey.js'; import { AbstractKeybindingService } from '../../common/abstractKeybindingService.js'; import { IKeyboardEvent } from '../../common/keybinding.js'; -import { KeybindingResolver } from '../../common/keybindingResolver.js'; +import { KeybindingResolver, ResolutionResult, ResultKind } from '../../common/keybindingResolver.js'; import { ResolvedKeybindingItem } from '../../common/resolvedKeybindingItem.js'; import { USLayoutResolvedKeybinding } from '../../common/usLayoutResolvedKeybinding.js'; import { createUSLayoutResolvedKeybinding } from './keybindingsTestUtils.js'; @@ -71,18 +71,27 @@ suite('AbstractKeybindingService', () => { return []; } - public testDispatch(kb: number): boolean { + public testDispatch(kb: number, isComposing: boolean = false): boolean { + return this._dispatch(this._toKeyboardEvent(kb, isComposing), null!); + } + + public testSoftDispatch(kb: number, isComposing: boolean = false): ResolutionResult { + return this.softDispatch(this._toKeyboardEvent(kb, isComposing), null!); + } + + private _toKeyboardEvent(kb: number, isComposing: boolean): IKeyboardEvent { const keybinding = createSimpleKeybinding(kb, OS); - return this._dispatch({ + return { _standardKeyboardEventBrand: true, ctrlKey: keybinding.ctrlKey, shiftKey: keybinding.shiftKey, altKey: keybinding.altKey, metaKey: keybinding.metaKey, altGraphKey: false, - keyCode: keybinding.keyCode, + // `StandardKeyboardEvent` normalizes composing keystrokes to KEY_IN_COMPOSITION. + keyCode: isComposing ? KeyCode.KEY_IN_COMPOSITION : keybinding.keyCode, code: null! - }, null!); + }; } public _dumpDebugInfo(): string { @@ -475,6 +484,40 @@ suite('AbstractKeybindingService', () => { kbService.dispose(); }); + test('keybindings are not dispatched while an IME composition is in progress', () => { + + const kbService = createTestKeybindingService([ + kbItem(KeyCode.Enter, 'enterCommand'), + ]); + + // Enter commits the IME composition and belongs to the input method, not to the workbench. + const shouldPreventDefaultWhileComposing = kbService.testDispatch(KeyCode.Enter, true); + assert.deepStrictEqual( + [shouldPreventDefaultWhileComposing, executeCommandCalls], + [false, []] + ); + + // `softDispatch` must agree, otherwise callers that ask "will the workbench claim this key?" + // prevent the default and then nobody handles the keystroke. + assert.strictEqual( + kbService.testSoftDispatch(KeyCode.Enter, true).kind, + ResultKind.NoMatchingKb + ); + + // Once the composition has committed, the very same key runs the command as usual. + const shouldPreventDefault = kbService.testDispatch(KeyCode.Enter, false); + assert.deepStrictEqual( + [shouldPreventDefault, executeCommandCalls], + [true, [{ commandId: 'enterCommand', args: [null] }]] + ); + assert.strictEqual( + kbService.testSoftDispatch(KeyCode.Enter, false).kind, + ResultKind.KbFound + ); + + kbService.dispose(); + }); + test('can trigger command that is sharing keybinding with chord', () => { const kbService = createTestKeybindingService([ diff --git a/src/vs/platform/quickinput/browser/quickInputActions.ts b/src/vs/platform/quickinput/browser/quickInputActions.ts index 6c705825036..b967d9fdeb3 100644 --- a/src/vs/platform/quickinput/browser/quickInputActions.ts +++ b/src/vs/platform/quickinput/browser/quickInputActions.ts @@ -217,8 +217,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // All other kinds of Quick things handle Accept, except Widget. In other words, Accepting is a detail on the things // that extend IQuickInput ContextKeyExpr.notEquals(quickInputTypeContextKeyValue, QuickInputType.QuickWidget), - inQuickInputContext, - ContextKeyExpr.not('isComposing') + inQuickInputContext ), metadata: { description: localize('nonQuickWidget', "Used while in the context of some quick input. If you change one keybinding for this command, you should change all of the other keybindings (modifier variants) of this command as well.") }, handler: (accessor) => { From a01c032942910d3fc45a1c6849bd6220bbd310ac Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 30 Jul 2026 14:33:31 -0700 Subject: [PATCH 30/86] Cover the changeset lifecycle in the agent host E2E suite (#328151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * agentHost: cover the changeset lifecycle end to end Changeset lifecycle was the top remaining item in the migration backlog and existed only in the frozen protocol suite, where it cannot describe the contract for any other AHP implementation: that test drives a mock agent with the magic prompt terminal-edit:, side-loaded via --enable-mock-agent. Adds a conformance-tier suite covering subscription and computation status, an added file, an edit to a committed file, client-owned review state, and the per-session changeset catalog. Actions move from 44/85 to 50/85 (51.8% to 58.8%), covering changeset/statusChanged, contentChanged, filesReviewChanged, operationsChanged, cleared, and chat/activityChanged. Every scenario drives real file changes through host-executed bang commands, so the changeset is computed from git rather than from what a tool reported and no scenario crosses the model boundary. The shapes asserted here were taken from a throwaway probe against a real session rather than from reading the service, which is also how the branch changeset was confirmed to be the one that reports a new file. Three changeset actions remain uncovered and are recorded in the README with what each would need: fileSet and fileRemoved are the incremental per-file updates rather than the bulk path a fresh session takes, and operationStatusChanged needs an invoked operation. Conformance 57 -> 62 passing, stable across repeated runs; Copilot 49, Claude 44, Codex 8, 0 failing. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: assert changeset operations, and harden the suite's inputs Review feedback, all three accepted. The README claimed the suite covered operations while nothing asserted them: they were reaching the coverage recorder incidentally, on the wire, which is exactly the "covered means it appeared once" floor the README warns about. A scenario now asserts them, and a probe first established when they appear at all - only on the uncommitted changeset, once there are real uncommitted changes, as commit (changeset scope) and discard-changes (resource scope). The bang command interpolated the file name and contents into a node -e script literal, so a value containing a quote or backslash could break out of it or change what ran. Both are passed as process.argv entries now. clientSeq was hard-coded per scenario, which is unsafe when the suite shares one client and sequence numbers must strictly increase. Replaced with a monotonic counter. Conformance 62 -> 63 passing, verified clean across twelve consecutive runs. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Take the reconnect cutoff from the subscribe response, not from an echo `reconnect replays only the actions a dropped client missed` timed out on Linux CI having received no notifications at all. The test learned its `lastSeenServerSeq` by dispatching an action on the second connection and waiting for the server to echo it back, but a subscription is not guaranteed to be installed before a dispatch sent immediately afterwards is handled — so the echo can be broadcast to no subscribers and the wait never completes. `SubscribeResult.snapshot.fromSeq` is the same boundary and is guaranteed by the response itself, so the test no longer needs the second client to receive anything before it drops. The contract under test is unchanged: the replay must exclude everything at or below the cutoff and include the gap above it. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/test/node/e2e/README.md | 6 +- .../node/e2e/coverage/protocol-surface.json | 9 +- .../test/node/e2e/coverage/summary.json | 568 ++++++++++-------- .../node/e2e/suites/agentHostE2ESuites.ts | 2 + .../test/node/e2e/suites/changesetSuite.ts | 278 +++++++++ .../node/e2e/suites/protocolContractsSuite.ts | 21 +- .../node/e2e/suites/stateOperationsSuite.ts | 28 +- 7 files changed, 623 insertions(+), 289 deletions(-) create mode 100644 src/vs/platform/agentHost/test/node/e2e/suites/changesetSuite.ts diff --git a/src/vs/platform/agentHost/test/node/e2e/README.md b/src/vs/platform/agentHost/test/node/e2e/README.md index 5886b0739ba..bf9df5f7088 100644 --- a/src/vs/platform/agentHost/test/node/e2e/README.md +++ b/src/vs/platform/agentHost/test/node/e2e/README.md @@ -474,10 +474,14 @@ The **Protocol symbols** column lists what each row is responsible for; check `c | Client-hosted filesystem (reverse requests) | migrated — `suites/clientFilesystemSuite.ts` | `resourceWatch/changed` still uncovered | | Turn history paging | migrated — `suites/protocolContractsSuite.ts` | `fetchTurns`, `chat/turnsLoaded` — covered | | Reconnect and multi-client fan-out | partly migrated — `suites/protocolContractsSuite.ts` covers `reconnect`; fan-out across several live clients is still only in `multiClient` | `reconnect` — covered | -| Changeset lifecycle | still only in `sessionDiffs` | all 8 `changeset/*` actions uncovered | +| Changeset lifecycle | migrated — `suites/changesetSuite.ts` | 5 of 8 `changeset/*` covered; `fileSet`, `fileRemoved`, `operationStatusChanged` still uncovered | | OTLP export | still only in `otlpLogs` | `otlp/exportLogs`, `otlp/exportMetrics`, `otlp/exportTraces` uncovered | | Liveness | migrated — `suites/protocolContractsSuite.ts` | `ping` — covered | +Changeset lifecycle followed. `suites/changesetSuite.ts` covers status, content, review state, the operations a changeset advertises, and the catalog in the conformance tier, driving real git-backed edits through host-executed bang commands so no scenario crosses the model boundary. The frozen suite's version could not be copied: it drives a mock agent with the magic prompt `terminal-edit:`, which no other AHP implementation would understand. + +The three remaining `changeset/*` actions need scenarios this suite does not yet reach: `fileSet` / `fileRemoved` are the incremental per-file updates (the bulk `contentChanged` path is what a fresh session emits), and `operationStatusChanged` needs an invoked operation — `commit` and `discard-changes` are the two that run without network access. + The filesystem family was the largest of these and is now covered by `suites/clientFilesystemSuite.ts` in the conformance tier — both the `resource*` command surface the host executes against its own filesystem, and the reverse direction where the host asks the *client* for a file it cannot otherwise reach. See [The filesystem, in both directions](#the-filesystem-in-both-directions). Some contracts are covered by **neither** suite and need new tests outright: `auth/required`, `root/progress`, and `chat/toolCallAuthRequired` / `chat/toolCallAuthResolved`. The `annotations/*` channel is now covered by `suites/annotationsSuite.ts`. diff --git a/src/vs/platform/agentHost/test/node/e2e/coverage/protocol-surface.json b/src/vs/platform/agentHost/test/node/e2e/coverage/protocol-surface.json index 15d9a764aab..c42f10067fc 100644 --- a/src/vs/platform/agentHost/test/node/e2e/coverage/protocol-surface.json +++ b/src/vs/platform/agentHost/test/node/e2e/coverage/protocol-surface.json @@ -25,18 +25,13 @@ ] }, "actions": { - "covered": 59, + "covered": 64, "total": 85, - "percentage": 69.41, + "percentage": 75.29, "uncovered": [ - "changeset/cleared", - "changeset/contentChanged", "changeset/fileRemoved", "changeset/fileSet", - "changeset/filesReviewChanged", "changeset/operationStatusChanged", - "changeset/operationsChanged", - "changeset/statusChanged", "chat/error", "chat/inputAnswerChanged", "chat/reasoning", diff --git a/src/vs/platform/agentHost/test/node/e2e/coverage/summary.json b/src/vs/platform/agentHost/test/node/e2e/coverage/summary.json index 31b498351a1..ec87420ea55 100644 --- a/src/vs/platform/agentHost/test/node/e2e/coverage/summary.json +++ b/src/vs/platform/agentHost/test/node/e2e/coverage/summary.json @@ -15,23 +15,23 @@ }, "total": { "statements": { - "covered": 65997, - "total": 93940, + "covered": 66445, + "total": 94575, "percentage": 70.25 }, "branches": { - "covered": 5648, - "total": 8960, - "percentage": 63.03 + "covered": 5740, + "total": 9095, + "percentage": 63.11 }, "functions": { - "covered": 2153, - "total": 3556, - "percentage": 60.54 + "covered": 2175, + "total": 3583, + "percentage": 60.7 }, "lines": { - "covered": 65997, - "total": 93940, + "covered": 66445, + "total": 94575, "percentage": 70.25 } }, @@ -263,9 +263,9 @@ "percentage": 54.39 }, "branches": { - "covered": 19, - "total": 32, - "percentage": 59.37 + "covered": 20, + "total": 33, + "percentage": 60.6 }, "functions": { "covered": 10, @@ -368,9 +368,9 @@ }, "src/vs/platform/agentHost/common/agentHostSchema.ts": { "statements": { - "covered": 673, - "total": 772, - "percentage": 87.17 + "covered": 684, + "total": 783, + "percentage": 87.35 }, "branches": { "covered": 46, @@ -383,9 +383,9 @@ "percentage": 68.18 }, "lines": { - "covered": 673, - "total": 772, - "percentage": 87.17 + "covered": 684, + "total": 783, + "percentage": 87.35 } }, "src/vs/platform/agentHost/common/agentHostSlashCommand.ts": { @@ -522,9 +522,9 @@ }, "src/vs/platform/agentHost/common/agentService.ts": { "statements": { - "covered": 2162, - "total": 2334, - "percentage": 92.63 + "covered": 2175, + "total": 2347, + "percentage": 92.67 }, "branches": { "covered": 24, @@ -537,9 +537,9 @@ "percentage": 42.85 }, "lines": { - "covered": 2162, - "total": 2334, - "percentage": 92.63 + "covered": 2175, + "total": 2347, + "percentage": 92.67 } }, "src/vs/platform/agentHost/common/ahpJsonlLogger.ts": { @@ -1292,14 +1292,14 @@ }, "src/vs/platform/agentHost/common/state/protocol/channels-changeset/reducer.ts": { "statements": { - "covered": 63, + "covered": 72, "total": 121, - "percentage": 52.06 + "percentage": 59.5 }, "branches": { - "covered": 7, - "total": 16, - "percentage": 43.75 + "covered": 9, + "total": 19, + "percentage": 47.36 }, "functions": { "covered": 1, @@ -1307,9 +1307,9 @@ "percentage": 100 }, "lines": { - "covered": 63, + "covered": 72, "total": 121, - "percentage": 52.06 + "percentage": 59.5 } }, "src/vs/platform/agentHost/common/state/protocol/channels-chat/reducer.ts": { @@ -1864,14 +1864,14 @@ }, "src/vs/platform/agentHost/node/agentConfigurationService.ts": { "statements": { - "covered": 369, + "covered": 366, "total": 402, - "percentage": 91.79 + "percentage": 91.04 }, "branches": { - "covered": 38, - "total": 51, - "percentage": 74.5 + "covered": 34, + "total": 47, + "percentage": 72.34 }, "functions": { "covered": 14, @@ -1879,9 +1879,9 @@ "percentage": 87.5 }, "lines": { - "covered": 369, + "covered": 366, "total": 402, - "percentage": 91.79 + "percentage": 91.04 } }, "src/vs/platform/agentHost/node/agentHostAuthenticationService.ts": { @@ -1952,14 +1952,14 @@ }, "src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts": { "statements": { - "covered": 245, + "covered": 272, "total": 333, - "percentage": 73.57 + "percentage": 81.68 }, "branches": { - "covered": 19, - "total": 39, - "percentage": 48.71 + "covered": 36, + "total": 44, + "percentage": 81.81 }, "functions": { "covered": 11, @@ -1967,31 +1967,31 @@ "percentage": 73.33 }, "lines": { - "covered": 245, + "covered": 272, "total": 333, - "percentage": 73.57 + "percentage": 81.68 } }, "src/vs/platform/agentHost/node/agentHostChangesetFileMonitorCoordinator.ts": { "statements": { - "covered": 307, + "covered": 322, "total": 368, - "percentage": 83.42 + "percentage": 87.5 }, "branches": { - "covered": 63, - "total": 75, - "percentage": 84 + "covered": 60, + "total": 76, + "percentage": 78.94 }, "functions": { - "covered": 24, + "covered": 26, "total": 27, - "percentage": 88.88 + "percentage": 96.29 }, "lines": { - "covered": 307, + "covered": 322, "total": 368, - "percentage": 83.42 + "percentage": 87.5 } }, "src/vs/platform/agentHost/node/agentHostChangesetOperationService.ts": { @@ -2001,9 +2001,9 @@ "percentage": 57.69 }, "branches": { - "covered": 23, - "total": 27, - "percentage": 85.18 + "covered": 26, + "total": 30, + "percentage": 86.66 }, "functions": { "covered": 7, @@ -2018,24 +2018,24 @@ }, "src/vs/platform/agentHost/node/agentHostChangesetService.ts": { "statements": { - "covered": 700, + "covered": 750, "total": 1122, - "percentage": 62.38 + "percentage": 66.84 }, "branches": { - "covered": 112, - "total": 145, - "percentage": 77.24 + "covered": 126, + "total": 163, + "percentage": 77.3 }, "functions": { - "covered": 31, + "covered": 34, "total": 52, - "percentage": 59.61 + "percentage": 65.38 }, "lines": { - "covered": 700, + "covered": 750, "total": 1122, - "percentage": 62.38 + "percentage": 66.84 } }, "src/vs/platform/agentHost/node/agentHostChangesetStateCache.ts": { @@ -2150,14 +2150,14 @@ }, "src/vs/platform/agentHost/node/agentHostCommitOperationProvider.ts": { "statements": { - "covered": 46, + "covered": 55, "total": 58, - "percentage": 79.31 + "percentage": 94.82 }, "branches": { - "covered": 10, + "covered": 11, "total": 13, - "percentage": 76.92 + "percentage": 84.61 }, "functions": { "covered": 4, @@ -2165,9 +2165,9 @@ "percentage": 66.66 }, "lines": { - "covered": 46, + "covered": 55, "total": 58, - "percentage": 79.31 + "percentage": 94.82 } }, "src/vs/platform/agentHost/node/agentHostCompletions.ts": { @@ -2216,14 +2216,14 @@ }, "src/vs/platform/agentHost/node/agentHostDiscardChangesOperationProvider.ts": { "statements": { - "covered": 38, + "covered": 47, "total": 47, - "percentage": 80.85 + "percentage": 100 }, "branches": { - "covered": 3, - "total": 5, - "percentage": 60 + "covered": 6, + "total": 7, + "percentage": 85.71 }, "functions": { "covered": 3, @@ -2231,9 +2231,9 @@ "percentage": 75 }, "lines": { - "covered": 38, + "covered": 47, "total": 47, - "percentage": 80.85 + "percentage": 100 } }, "src/vs/platform/agentHost/node/agentHostFileCompletionProvider.ts": { @@ -2260,24 +2260,24 @@ }, "src/vs/platform/agentHost/node/agentHostFileMonitorService.ts": { "statements": { - "covered": 141, + "covered": 165, "total": 185, - "percentage": 76.21 + "percentage": 89.18 }, "branches": { - "covered": 14, - "total": 22, - "percentage": 63.63 + "covered": 22, + "total": 34, + "percentage": 64.7 }, "functions": { - "covered": 10, + "covered": 14, "total": 14, - "percentage": 71.42 + "percentage": 100 }, "lines": { - "covered": 141, + "covered": 165, "total": 185, - "percentage": 76.21 + "percentage": 89.18 } }, "src/vs/platform/agentHost/node/agentHostGitHubEndpointService.ts": { @@ -2326,9 +2326,9 @@ }, "src/vs/platform/agentHost/node/agentHostGitService.ts": { "statements": { - "covered": 833, - "total": 1369, - "percentage": 60.84 + "covered": 840, + "total": 1413, + "percentage": 59.44 }, "branches": { "covered": 152, @@ -2337,13 +2337,13 @@ }, "functions": { "covered": 40, - "total": 66, - "percentage": 60.6 + "total": 67, + "percentage": 59.7 }, "lines": { - "covered": 833, - "total": 1369, - "percentage": 60.84 + "covered": 840, + "total": 1413, + "percentage": 59.44 } }, "src/vs/platform/agentHost/node/agentHostGitStateService.ts": { @@ -2353,9 +2353,9 @@ "percentage": 66.83 }, "branches": { - "covered": 27, - "total": 43, - "percentage": 62.79 + "covered": 28, + "total": 44, + "percentage": 63.63 }, "functions": { "covered": 5, @@ -2485,9 +2485,9 @@ "percentage": 55.55 }, "branches": { - "covered": 12, - "total": 17, - "percentage": 70.58 + "covered": 15, + "total": 20, + "percentage": 75 }, "functions": { "covered": 4, @@ -2639,9 +2639,9 @@ "percentage": 83.36 }, "branches": { - "covered": 79, - "total": 100, - "percentage": 79 + "covered": 78, + "total": 99, + "percentage": 78.78 }, "functions": { "covered": 22, @@ -2722,14 +2722,14 @@ }, "src/vs/platform/agentHost/node/agentHostStateManager.ts": { "statements": { - "covered": 1352, + "covered": 1358, "total": 1495, - "percentage": 90.43 + "percentage": 90.83 }, "branches": { - "covered": 194, + "covered": 196, "total": 235, - "percentage": 82.55 + "percentage": 83.4 }, "functions": { "covered": 51, @@ -2737,9 +2737,9 @@ "percentage": 82.25 }, "lines": { - "covered": 1352, + "covered": 1358, "total": 1495, - "percentage": 90.43 + "percentage": 90.83 } }, "src/vs/platform/agentHost/node/agentHostSyncOperationHandler.ts": { @@ -2766,14 +2766,14 @@ }, "src/vs/platform/agentHost/node/agentHostSyncOperationProvider.ts": { "statements": { - "covered": 47, + "covered": 49, "total": 61, - "percentage": 77.04 + "percentage": 80.32 }, "branches": { - "covered": 5, - "total": 9, - "percentage": 55.55 + "covered": 8, + "total": 11, + "percentage": 72.72 }, "functions": { "covered": 4, @@ -2781,9 +2781,9 @@ "percentage": 66.66 }, "lines": { - "covered": 47, + "covered": 49, "total": 61, - "percentage": 77.04 + "percentage": 80.32 } }, "src/vs/platform/agentHost/node/agentHostTelemetryReporter.ts": { @@ -3035,9 +3035,9 @@ "percentage": 61.26 }, "branches": { - "covered": 284, + "covered": 285, "total": 482, - "percentage": 58.92 + "percentage": 59.12 }, "functions": { "covered": 93, @@ -3052,14 +3052,14 @@ }, "src/vs/platform/agentHost/node/agentSideEffects.ts": { "statements": { - "covered": 1535, - "total": 1885, - "percentage": 81.43 + "covered": 1537, + "total": 1886, + "percentage": 81.49 }, "branches": { - "covered": 261, - "total": 350, - "percentage": 74.57 + "covered": 262, + "total": 351, + "percentage": 74.64 }, "functions": { "covered": 48, @@ -3067,9 +3067,9 @@ "percentage": 88.88 }, "lines": { - "covered": 1535, - "total": 1885, - "percentage": 81.43 + "covered": 1537, + "total": 1886, + "percentage": 81.49 } }, "src/vs/platform/agentHost/node/appNodeModules.ts": { @@ -3206,24 +3206,24 @@ }, "src/vs/platform/agentHost/node/claude/claudeAgentSession.ts": { "statements": { - "covered": 952, - "total": 1169, - "percentage": 81.43 + "covered": 961, + "total": 1182, + "percentage": 81.3 }, "branches": { - "covered": 52, - "total": 82, - "percentage": 63.41 + "covered": 55, + "total": 87, + "percentage": 63.21 }, "functions": { - "covered": 25, - "total": 52, - "percentage": 48.07 + "covered": 26, + "total": 53, + "percentage": 49.05 }, "lines": { - "covered": 952, - "total": 1169, - "percentage": 81.43 + "covered": 961, + "total": 1182, + "percentage": 81.3 } }, "src/vs/platform/agentHost/node/claude/claudeCanUseTool.ts": { @@ -3886,6 +3886,50 @@ "percentage": 97.8 } }, + "src/vs/platform/agentHost/node/claude/customizations/claudeCustomizationPolicy.ts": { + "statements": { + "covered": 19, + "total": 42, + "percentage": 45.23 + }, + "branches": { + "covered": 1, + "total": 4, + "percentage": 25 + }, + "functions": { + "covered": 1, + "total": 3, + "percentage": 33.33 + }, + "lines": { + "covered": 19, + "total": 42, + "percentage": 45.23 + } + }, + "src/vs/platform/agentHost/node/claude/customizations/claudeMultiRootCustomizationDiscovery.ts": { + "statements": { + "covered": 49, + "total": 72, + "percentage": 68.05 + }, + "branches": { + "covered": 2, + "total": 4, + "percentage": 50 + }, + "functions": { + "covered": 2, + "total": 4, + "percentage": 50 + }, + "lines": { + "covered": 49, + "total": 72, + "percentage": 68.05 + } + }, "src/vs/platform/agentHost/node/claude/customizations/claudeSessionClientCustomizationsModel.ts": { "statements": { "covered": 168, @@ -3910,46 +3954,46 @@ }, "src/vs/platform/agentHost/node/claude/customizations/claudeSessionCustomizationDiscovery.ts": { "statements": { - "covered": 422, - "total": 539, - "percentage": 78.29 + "covered": 424, + "total": 551, + "percentage": 76.95 }, "branches": { - "covered": 41, - "total": 61, - "percentage": 67.21 + "covered": 43, + "total": 68, + "percentage": 63.23 }, "functions": { - "covered": 11, - "total": 13, - "percentage": 84.61 + "covered": 12, + "total": 14, + "percentage": 85.71 }, "lines": { - "covered": 422, - "total": 539, - "percentage": 78.29 + "covered": 424, + "total": 551, + "percentage": 76.95 } }, "src/vs/platform/agentHost/node/claude/customizations/scan/claudeAgentSkillScan.ts": { "statements": { - "covered": 98, - "total": 105, - "percentage": 93.33 + "covered": 107, + "total": 114, + "percentage": 93.85 }, "branches": { - "covered": 5, - "total": 7, - "percentage": 71.42 + "covered": 6, + "total": 9, + "percentage": 66.66 }, "functions": { - "covered": 4, - "total": 4, + "covered": 5, + "total": 5, "percentage": 100 }, "lines": { - "covered": 98, - "total": 105, - "percentage": 93.33 + "covered": 107, + "total": 114, + "percentage": 93.85 } }, "src/vs/platform/agentHost/node/claude/customizations/scan/claudeHookScan.ts": { @@ -3998,24 +4042,24 @@ }, "src/vs/platform/agentHost/node/claude/customizations/scan/claudeNativePluginScan.ts": { "statements": { - "covered": 122, - "total": 220, - "percentage": 55.45 + "covered": 135, + "total": 266, + "percentage": 50.75 }, "branches": { - "covered": 4, - "total": 11, - "percentage": 36.36 + "covered": 5, + "total": 13, + "percentage": 38.46 }, "functions": { - "covered": 3, - "total": 7, - "percentage": 42.85 + "covered": 4, + "total": 10, + "percentage": 40 }, "lines": { - "covered": 122, - "total": 220, - "percentage": 55.45 + "covered": 135, + "total": 266, + "percentage": 50.75 } }, "src/vs/platform/agentHost/node/claude/customizations/scan/claudeRuleScan.ts": { @@ -4064,24 +4108,24 @@ }, "src/vs/platform/agentHost/node/codex/codexAgent.ts": { "statements": { - "covered": 2540, - "total": 4445, - "percentage": 57.14 + "covered": 2580, + "total": 4602, + "percentage": 56.06 }, "branches": { - "covered": 193, - "total": 354, - "percentage": 54.51 + "covered": 196, + "total": 373, + "percentage": 52.54 }, "functions": { - "covered": 85, - "total": 162, - "percentage": 52.46 + "covered": 87, + "total": 169, + "percentage": 51.47 }, "lines": { - "covered": 2540, - "total": 4445, - "percentage": 57.14 + "covered": 2580, + "total": 4602, + "percentage": 56.06 } }, "src/vs/platform/agentHost/node/codex/codexAppServerClient.ts": { @@ -4219,8 +4263,8 @@ "src/vs/platform/agentHost/node/codex/codexLaunchConfig.ts": { "statements": { "covered": 58, - "total": 68, - "percentage": 85.29 + "total": 72, + "percentage": 80.55 }, "branches": { "covered": 2, @@ -4234,8 +4278,8 @@ }, "lines": { "covered": 58, - "total": 68, - "percentage": 85.29 + "total": 72, + "percentage": 80.55 } }, "src/vs/platform/agentHost/node/codex/codexMapAppServerEvents.ts": { @@ -4394,24 +4438,24 @@ }, "src/vs/platform/agentHost/node/codex/codexSessionMetadataStore.ts": { "statements": { - "covered": 86, - "total": 112, - "percentage": 76.78 + "covered": 99, + "total": 157, + "percentage": 63.05 }, "branches": { - "covered": 2, - "total": 3, - "percentage": 66.66 + "covered": 3, + "total": 6, + "percentage": 50 }, "functions": { - "covered": 2, - "total": 3, - "percentage": 66.66 + "covered": 3, + "total": 5, + "percentage": 60 }, "lines": { - "covered": 86, - "total": 112, - "percentage": 76.78 + "covered": 99, + "total": 157, + "percentage": 63.05 } }, "src/vs/platform/agentHost/node/codex/codexShellCommand.ts": { @@ -4460,24 +4504,24 @@ }, "src/vs/platform/agentHost/node/commandAutoApprover.ts": { "statements": { - "covered": 492, - "total": 606, - "percentage": 81.18 + "covered": 565, + "total": 705, + "percentage": 80.14 }, "branches": { - "covered": 36, - "total": 64, - "percentage": 56.25 + "covered": 41, + "total": 83, + "percentage": 49.39 }, "functions": { - "covered": 14, - "total": 16, - "percentage": 87.5 + "covered": 16, + "total": 19, + "percentage": 84.21 }, "lines": { - "covered": 492, - "total": 606, - "percentage": 81.18 + "covered": 565, + "total": 705, + "percentage": 80.14 } }, "src/vs/platform/agentHost/node/copilot/agentHostSandboxEngine.ts": { @@ -4592,14 +4636,14 @@ }, "src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts": { "statements": { - "covered": 3230, - "total": 5048, - "percentage": 63.98 + "covered": 3249, + "total": 5086, + "percentage": 63.88 }, "branches": { - "covered": 422, - "total": 696, - "percentage": 60.63 + "covered": 429, + "total": 705, + "percentage": 60.85 }, "functions": { "covered": 125, @@ -4607,9 +4651,9 @@ "percentage": 67.56 }, "lines": { - "covered": 3230, - "total": 5048, - "percentage": 63.98 + "covered": 3249, + "total": 5086, + "percentage": 63.88 } }, "src/vs/platform/agentHost/node/copilot/copilotAttachmentUtils.ts": { @@ -5081,9 +5125,9 @@ "percentage": 40.01 }, "branches": { - "covered": 62, - "total": 79, - "percentage": 78.48 + "covered": 63, + "total": 80, + "percentage": 78.75 }, "functions": { "covered": 15, @@ -5257,9 +5301,9 @@ "percentage": 100 }, "branches": { - "covered": 13, - "total": 14, - "percentage": 92.85 + "covered": 15, + "total": 16, + "percentage": 93.75 }, "functions": { "covered": 5, @@ -5389,9 +5433,9 @@ "percentage": 79.18 }, "branches": { - "covered": 22, - "total": 25, - "percentage": 88 + "covered": 23, + "total": 26, + "percentage": 88.46 }, "functions": { "covered": 12, @@ -5406,14 +5450,14 @@ }, "src/vs/platform/agentHost/node/sessionDatabase.ts": { "statements": { - "covered": 657, + "covered": 654, "total": 869, - "percentage": 75.6 + "percentage": 75.25 }, "branches": { - "covered": 102, - "total": 122, - "percentage": 83.6 + "covered": 104, + "total": 125, + "percentage": 83.2 }, "functions": { "covered": 32, @@ -5421,9 +5465,9 @@ "percentage": 60.37 }, "lines": { - "covered": 657, + "covered": 654, "total": 869, - "percentage": 75.6 + "percentage": 75.25 } }, "src/vs/platform/agentHost/node/sessionDiffAggregator.ts": { @@ -5450,23 +5494,23 @@ }, "src/vs/platform/agentHost/node/sessionPermissions.ts": { "statements": { - "covered": 499, - "total": 658, + "covered": 521, + "total": 687, "percentage": 75.83 }, "branches": { - "covered": 68, - "total": 108, - "percentage": 62.96 + "covered": 72, + "total": 112, + "percentage": 64.28 }, "functions": { - "covered": 22, - "total": 27, - "percentage": 81.48 + "covered": 23, + "total": 28, + "percentage": 82.14 }, "lines": { - "covered": 499, - "total": 658, + "covered": 521, + "total": 687, "percentage": 75.83 } }, @@ -5890,14 +5934,14 @@ }, "src/vs/platform/agentHost/node/shared/worktreeIsolation.ts": { "statements": { - "covered": 729, + "covered": 731, "total": 926, - "percentage": 78.72 + "percentage": 78.94 }, "branches": { - "covered": 73, - "total": 113, - "percentage": 64.6 + "covered": 76, + "total": 115, + "percentage": 66.08 }, "functions": { "covered": 31, @@ -5905,9 +5949,9 @@ "percentage": 73.8 }, "lines": { - "covered": 729, + "covered": 731, "total": 926, - "percentage": 78.72 + "percentage": 78.94 } }, "src/vs/platform/agentHost/node/webSocketTransport.ts": { @@ -5917,9 +5961,9 @@ "percentage": 85.71 }, "branches": { - "covered": 14, - "total": 22, - "percentage": 63.63 + "covered": 15, + "total": 23, + "percentage": 65.21 }, "functions": { "covered": 7, diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/agentHostE2ESuites.ts b/src/vs/platform/agentHost/test/node/e2e/suites/agentHostE2ESuites.ts index ef38c052392..35964e895b8 100644 --- a/src/vs/platform/agentHost/test/node/e2e/suites/agentHostE2ESuites.ts +++ b/src/vs/platform/agentHost/test/node/e2e/suites/agentHostE2ESuites.ts @@ -8,6 +8,7 @@ import type { IAgentHostTarget } from '../harness/agentHostTarget.js'; import type { TestProtocolClient } from '../../serverIntegrationTestHelpers.js'; import { defineCoreTests } from './coreSuite.js'; import { defineAnnotationsTests } from './annotationsSuite.js'; +import { defineChangesetTests } from './changesetSuite.js'; import { defineClientFilesystemTests } from './clientFilesystemSuite.js'; import { defineProtocolContractTests } from './protocolContractsSuite.js'; import { defineFileOperationsTests } from './fileOperationsSuite.js'; @@ -111,6 +112,7 @@ function defineSuite(config: IAgentHostE2EProviderConfig, options: IDefineOption defineClientFilesystemTests(context); defineAnnotationsTests(context); defineProtocolContractTests(context); + defineChangesetTests(context); } // Suites that contain only parity-tier scenarios. diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/changesetSuite.ts b/src/vs/platform/agentHost/test/node/e2e/suites/changesetSuite.ts new file mode 100644 index 00000000000..63539797715 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/e2e/suites/changesetSuite.ts @@ -0,0 +1,278 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * The changeset channel: how the host reports what a session changed on disk. + * + * A changeset is computed from git rather than from what a tool reported, so + * it sees edits the agent made by any means — the scenarios here drive real + * file changes through host-executed bang commands and never cross the model + * boundary. + * + * The host publishes several changesets per session, each on its own + * subscribable channel: `branch` (against the branch point), `uncommitted` + * (working-tree state), and `session` (cumulative for the session). They are + * separate channels because `changeset/*` actions are scoped to the changeset + * URI, so a session-only subscription never receives them. + * + * This contract previously existed only in the frozen `../protocol/` suite, + * which drives a mock agent with the magic prompt `terminal-edit:` and + * so cannot describe the contract for any other AHP implementation. + */ + +import assert from 'assert'; +import { execSync } from 'child_process'; +import { mkdtempSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from '../../../../../../base/common/path.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import type { SubscribeResult } from '../../../../common/state/protocol/commands.js'; +import { ActionType } from '../../../../common/state/sessionActions.js'; +import { + buildBranchChangesetUri, + buildSessionChangesetUri, + buildUncommittedChangesetUri, +} from '../../../../common/changesetUri.js'; +import { createRealSession, dispatchTurn, initTestGitRepo } from '../harness/agentHostE2ETestHarness.js'; +import { getActionEnvelope, isActionNotification } from '../../serverIntegrationTestHelpers.js'; +import { conformanceTest, type IAgentHostE2ETestContext } from './e2eTestContext.js'; + +/** The subset of `ChangesetFile` these tests assert on. */ +interface IObservedChangesetFile { + readonly id: string; + readonly reviewed?: boolean; + readonly edit: { + readonly before?: { readonly uri: string }; + readonly after?: { readonly uri: string }; + readonly diff?: { readonly added: number; readonly removed: number }; + }; +} + +interface IContentChangedAction { + readonly files: readonly IObservedChangesetFile[]; + readonly operations?: readonly { readonly id: string; readonly scopes: readonly string[] }[]; +} + +export function defineChangesetTests(context: IAgentHostE2ETestContext): void { + const { config, createdSessions, tempDirs } = context; + + /** + * Client sequence numbers must strictly increase for the lifetime of a + * client, and the suite shares one across tests, so they cannot be + * hard-coded per scenario. + */ + let clientSeq = 1000; + function nextClientSeq(): number { + return clientSeq++; + } + + /** A git repository with one committed file, so a branch point exists. */ + function createGitWorkspace(prefix: string): string { + const workspace = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(workspace); + initTestGitRepo(workspace); + writeFileSync(join(workspace, 'seed.txt'), 'seed\n'); + execSync('git add .', { cwd: workspace }); + execSync('git commit -q -m "seed"', { cwd: workspace }); + return workspace; + } + + async function createSessionIn(workspace: string, prefix: string): Promise { + return createRealSession(context.client, config, `${prefix}-${config.provider}`, createdSessions, URI.file(workspace)); + } + + /** + * Writes `file` through a host-executed bang command, so the change reaches + * disk the way an agent's shell edit would rather than from the test + * process. Paths are relative so no Windows backslash has to survive into a + * JavaScript string literal. + * + * The file name and contents are passed as `process.argv` entries rather + * than interpolated into the script, so a value containing a quote or a + * backslash cannot break out of the literal or change what runs. + */ + function writeFileCommand(file: string, contents: string): string { + return `!node -e "require('fs').writeFileSync(process.argv[1],process.argv[2])" ${file} ${contents}`; + } + + function fileUri(file: IObservedChangesetFile): string { + return file.edit.after?.uri ?? file.edit.before?.uri ?? ''; + } + + /** + * Waits for a `changeset/contentChanged` on `channel` that reports + * `basename`. Matched by basename because git resolves symlinks when + * reporting its top level (macOS `/var` versus `/private/var`), so the + * reported URI need not share a prefix with the workspace path. + */ + async function waitForFileInChangeset(channel: string, basename: string, timeout = 60_000): Promise { + const notification = await context.client.waitForNotification(n => { + if (!isActionNotification(n, 'changeset/contentChanged') || getActionEnvelope(n).channel !== channel) { + return false; + } + const action = getActionEnvelope(n).action as IContentChangedAction; + return action.files.some(file => fileUri(file).endsWith(`/${basename}`)); + }, timeout); + const action = getActionEnvelope(notification).action as IContentChangedAction; + return action.files.find(file => fileUri(file).endsWith(`/${basename}`))!; + } + + + conformanceTest(context, 'subscribing to a changeset reports its computation status', async function () { + const workspace = createGitWorkspace('ahp-changeset-status-'); + const sessionUri = await createSessionIn(workspace, 'changeset-status'); + const branchUri = buildBranchChangesetUri(sessionUri); + + const subscribed = await context.client.call('subscribe', { channel: branchUri }); + + // A changeset is computed asynchronously, so the snapshot a subscriber + // receives is a starting point and the terminal status arrives as an + // action. Asserting only the snapshot would pass without the host ever + // finishing the computation. + await context.client.waitForNotification(n => + isActionNotification(n, 'changeset/statusChanged') + && getActionEnvelope(n).channel === branchUri + && (getActionEnvelope(n).action as { status: string }).status === 'ready', + 60_000, + ); + + assert.deepStrictEqual({ + resource: subscribed.snapshot!.resource, + files: (subscribed.snapshot!.state as { files: unknown[] }).files, + }, { + resource: branchUri, + files: [], + }); + }); + + conformanceTest(context, 'a file written during a turn appears in the branch changeset', async function () { + const workspace = createGitWorkspace('ahp-changeset-add-'); + const sessionUri = await createSessionIn(workspace, 'changeset-add'); + const branchUri = buildBranchChangesetUri(sessionUri); + await context.client.call('subscribe', { channel: branchUri }); + + context.client.clearReceived(); + dispatchTurn(context.client, sessionUri, 'turn-changeset-add', writeFileCommand('added.txt', 'ADDED'), 1); + + const file = await waitForFileInChangeset(branchUri, 'added.txt'); + + // A newly added file has no before-side, and its diff counts the added + // line. Both come from git rather than from anything the tool reported, + // which is the property that makes the changeset trustworthy. + assert.deepStrictEqual({ + hasBeforeSide: file.edit.before !== undefined, + hasAfterSide: file.edit.after !== undefined, + diff: file.edit.diff, + reviewed: file.reviewed, + }, { + hasBeforeSide: false, + hasAfterSide: true, + diff: { added: 1, removed: 0 }, + reviewed: false, + }); + }); + + conformanceTest(context, 'editing a committed file reports both sides of the change', async function () { + const workspace = createGitWorkspace('ahp-changeset-edit-'); + const sessionUri = await createSessionIn(workspace, 'changeset-edit'); + const branchUri = buildBranchChangesetUri(sessionUri); + await context.client.call('subscribe', { channel: branchUri }); + + context.client.clearReceived(); + dispatchTurn(context.client, sessionUri, 'turn-changeset-edit', writeFileCommand('seed.txt', 'edited'), 1); + + const file = await waitForFileInChangeset(branchUri, 'seed.txt'); + + // Unlike an added file, an edit to a committed file has a before-side — + // the committed revision — so the client can render a real diff. + assert.deepStrictEqual({ + hasBeforeSide: file.edit.before !== undefined, + hasAfterSide: file.edit.after !== undefined, + }, { + hasBeforeSide: true, + hasAfterSide: true, + }); + }); + + conformanceTest(context, 'a client can mark a changeset file reviewed', async function () { + const workspace = createGitWorkspace('ahp-changeset-review-'); + const sessionUri = await createSessionIn(workspace, 'changeset-review'); + const branchUri = buildBranchChangesetUri(sessionUri); + await context.client.call('subscribe', { channel: branchUri }); + + context.client.clearReceived(); + dispatchTurn(context.client, sessionUri, 'turn-changeset-review', writeFileCommand('reviewme.txt', 'REVIEW'), 1); + const file = await waitForFileInChangeset(branchUri, 'reviewme.txt'); + + // `changeset/filesReviewChanged` is the one client-dispatchable action + // on this channel: review state is the client's to own, and the server + // echoes it back so other connected clients converge. + context.client.dispatch({ + channel: branchUri, + clientSeq: nextClientSeq(), + action: { type: ActionType.ChangesetFilesReviewChanged, files: [file.id], reviewed: true }, + }); + + const echoed = await context.client.waitForNotification(n => + isActionNotification(n, 'changeset/filesReviewChanged') + && getActionEnvelope(n).channel === branchUri, + 60_000, + ); + + assert.deepStrictEqual(getActionEnvelope(echoed).action, { + type: ActionType.ChangesetFilesReviewChanged, + files: [file.id], + reviewed: true, + }); + }); + + conformanceTest(context, 'uncommitted changes advertise the operations that act on them', async function () { + const workspace = createGitWorkspace('ahp-changeset-ops-'); + const sessionUri = await createSessionIn(workspace, 'changeset-ops'); + const uncommittedUri = buildUncommittedChangesetUri(sessionUri); + await context.client.call('subscribe', { channel: uncommittedUri }); + + context.client.clearReceived(); + dispatchTurn(context.client, sessionUri, 'turn-changeset-ops', writeFileCommand('operate.txt', 'OPERATE'), 1); + + // Operations are what a client turns into affordances, and they are + // only offered once there is something to act on — a session with no + // uncommitted changes advertises none. Each carries the scope it + // applies to, so a client knows whether to offer it for the whole + // changeset or per file. + const notification = await context.client.waitForNotification(n => { + if (!isActionNotification(n, 'changeset/contentChanged') || getActionEnvelope(n).channel !== uncommittedUri) { + return false; + } + return ((getActionEnvelope(n).action as IContentChangedAction).operations ?? []).length > 0; + }, 60_000); + + const operations = (getActionEnvelope(notification).action as IContentChangedAction).operations ?? []; + assert.deepStrictEqual(operations.map(operation => ({ id: operation.id, scopes: operation.scopes })), [ + { id: 'commit', scopes: ['changeset'] }, + { id: 'discard-changes', scopes: ['resource'] }, + ]); + }); + + conformanceTest(context, 'the session advertises its changeset catalog on separate channels', async function () { + const workspace = createGitWorkspace('ahp-changeset-catalog-'); + const sessionUri = await createSessionIn(workspace, 'changeset-catalog'); + + // Each changeset is its own channel. A client that subscribes only to + // the session never receives `changeset/*` actions, so the catalog is + // how it learns what else to subscribe to. + const subscribed = await Promise.all([ + context.client.call('subscribe', { channel: buildBranchChangesetUri(sessionUri) }), + context.client.call('subscribe', { channel: buildUncommittedChangesetUri(sessionUri) }), + context.client.call('subscribe', { channel: buildSessionChangesetUri(sessionUri) }), + ]); + + assert.deepStrictEqual(subscribed.map(result => result.snapshot!.resource), [ + buildBranchChangesetUri(sessionUri), + buildUncommittedChangesetUri(sessionUri), + buildSessionChangesetUri(sessionUri), + ]); + }); +} diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/protocolContractsSuite.ts b/src/vs/platform/agentHost/test/node/e2e/suites/protocolContractsSuite.ts index 28b95460cd4..4062e93d12e 100644 --- a/src/vs/platform/agentHost/test/node/e2e/suites/protocolContractsSuite.ts +++ b/src/vs/platform/agentHost/test/node/e2e/suites/protocolContractsSuite.ts @@ -116,21 +116,14 @@ export function defineProtocolContractTests(context: IAgentHostE2ETestContext): const { sessionUri } = await createSession('reconnect'); const chatUri = buildDefaultChatUri(sessionUri); + // The cutoff comes from the subscribe response rather than from watching + // this client receive its own dispatch: a subscription is not guaranteed + // to be installed before a dispatch sent immediately after it is handled, + // so waiting for that echo races. `fromSeq` is the same boundary and the + // response itself guarantees it. const { carried: seenThrough, revived } = await afterConnectionDrop(`reconnect-${config.provider}`, async first => { - await first.call('subscribe', { channel: chatUri }); - const seq = nextClientSeq(); - first.dispatch({ - channel: chatUri, - clientSeq: seq, - action: { type: ActionType.ChatDraftChanged, draft: { text: 'seen before the drop', origin: { kind: MessageKind.User } } }, - }); - const echoed = await first.waitForNotification(n => - isActionNotification(n, 'chat/draftChanged') - && getActionEnvelope(n).channel === chatUri - && getActionEnvelope(n).origin?.clientSeq === seq, - 30_000, - ); - return getActionEnvelope(echoed).serverSeq; + const subscribed = await first.call('subscribe', { channel: chatUri }); + return subscribed.snapshot!.fromSeq; }); try { diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/stateOperationsSuite.ts b/src/vs/platform/agentHost/test/node/e2e/suites/stateOperationsSuite.ts index 120ab30469f..635acee49f8 100644 --- a/src/vs/platform/agentHost/test/node/e2e/suites/stateOperationsSuite.ts +++ b/src/vs/platform/agentHost/test/node/e2e/suites/stateOperationsSuite.ts @@ -56,6 +56,13 @@ export function defineStateOperationsTests(context: IAgentHostE2ETestContext): v return result.snapshot!.state as TerminalState; } + /** The terminal's visible text, flattening command parts and raw output alike. */ + function terminalText(state: TerminalState): string { + return state.content + .map(part => part.type === 'command' ? part.output : part.value) + .join(''); + } + async function dispatchAndWait(channel: string, clientSeq: number, action: StateAction): Promise { context.client.clearReceived(); context.client.dispatch({ channel, clientSeq, action }); @@ -393,9 +400,7 @@ export function defineStateOperationsTests(context: IAgentHostE2ETestContext): v streamedOutput += action.data; return /(?:^|\D)42(?:\D|$)/.test(streamedOutput); }, 30_000); - const output = (await terminalState(terminalUri)).content - .map(part => part.type === 'command' ? part.output : part.value) - .join(''); + const output = terminalText(await terminalState(terminalUri)); assert.match(output, /(?:^|\D)42(?:\D|$)/); }); }); @@ -413,13 +418,26 @@ export function defineStateOperationsTests(context: IAgentHostE2ETestContext): v && (getActionEnvelope(n).action as { data: string }).data.includes('CLEAR_MARKER'), 30_000, ); + const before = terminalText(await terminalState(terminalUri)); await dispatchAndWait(terminalUri, 2, { type: ActionType.TerminalCleared }); // The scrollback lives in host state, not just in the client's view, - // so clearing must empty it for every subscriber including one that + // so clearing must drop it for every subscriber including one that // subscribes later. - assert.deepStrictEqual((await terminalState(terminalUri)).content, []); + // + // Asserting the buffer is *empty* would be wrong: the shell is live + // and redraws its prompt as soon as the screen is cleared, so bytes + // legitimately arrive after the clear reduces. What has to be gone + // is the output the client had already accumulated. + const after = terminalText(await terminalState(terminalUri)); + assert.deepStrictEqual({ + markerBeforeClear: before.includes('CLEAR_MARKER'), + markerAfterClear: after.includes('CLEAR_MARKER'), + }, { + markerBeforeClear: true, + markerAfterClear: false, + }); }); }); From 7c8f552dc66e5e9ff6be74f2022661f08bed8763 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 30 Jul 2026 14:44:14 -0700 Subject: [PATCH 31/86] agentHost: stream rich tool call progress (#327765) * agentHost: stream Copilot tool call arguments Render tool invocations while their arguments are still being generated, preserving final tool metadata and client-tool execution semantics.\n\n(Written by Copilot)\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chat: sequence streamed client tool updates Ensure partial-input handlers finish in order before client tool execution, and release pending streams when protocol completion wins the race. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: finalize streamed tool metadata at ready Allow Ready actions to replace provisional contributor and intention metadata so MCP tools can stream immediately while retaining correct execution, rendering, and telemetry semantics. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: simplify streamed tool call lifecycle Limit partial streaming to server-owned tools, preserve client execution ownership, and separate telemetry attribution from invocation timing. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: harden partial tool input display Fall back to raw streaming input for empty partial objects and avoid exposing cached parser objects. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: enrich streamed edit messages Compute progressive file and line-count messages in Agent Host while keeping incremental tool arguments off the AHP wire. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: stream rich Claude edit progress Share rich edit progress formatting across Copilot and Claude while preserving client-tool identity through live and replay lifecycles. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: test streamed tool progress end to end Add deterministic Copilot and Claude coverage for rich message-only file progress and client-tool ownership. Flush Claude's final progress update at the content-block boundary so line counts do not remain stale below the geometric checkpoint. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: normalize streamed file E2E line endings Compare replay-created file content with normalized LF line endings so the streaming progress scenario passes on Windows while retaining EOL-aware line-count coverage. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: sync merged AHP protocol Regenerate the vendored protocol from agent-host-protocol main at 8e0a9bbf after the Ready metadata refinement merged. Keep JSON-RPC parse-error typing in the VS Code transport shim rather than the generated protocol surface. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: throttle streamed tool progress on time only The geometric growth gate required each update to add 25% more input, so streamed edits updated less and less often and appeared to stall on large arguments. Throttle on a shared 50ms interval instead, and suppress updates whose rendered message is unchanged so the steadier cadence does not re-send identical rows. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: raise streamed tool display interval to 100ms Derive the streaming test waits from the shared interval constant so they do not silently fall back to asserting the final force-flush when the interval changes. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/common/partialToolInput.ts | 26 ++ .../common/state/protocol/.ahp-version | 2 +- .../state/protocol/channels-chat/actions.ts | 10 + .../state/protocol/channels-chat/reducer.ts | 30 +- .../state/protocol/channels-chat/state.ts | 31 +- .../common/state/protocol/common/messages.ts | 9 +- .../agentHost/common/state/sessionProtocol.ts | 8 +- .../common/streamingToolCallDisplay.ts | 180 ++++++++++ .../node/agentHostTelemetryReporter.ts | 2 +- .../node/agentHostToolCallTracker.ts | 38 ++- .../agentHost/node/agentSideEffects.ts | 23 +- .../node/claude/claudeMapSessionEvents.ts | 76 +++-- .../node/claude/claudeReplayMapper.ts | 25 +- .../node/claude/claudeSubagentSignals.ts | 19 +- .../node/claude/claudeToolCallRegistry.ts | 63 +++- .../node/claude/claudeToolDisplay.ts | 37 +++ .../node/copilot/copilotAgentSession.ts | 308 ++++++++++++++---- .../node/copilot/copilotSessionWrapper.ts | 5 + .../node/copilot/copilotToolDisplay.ts | 156 ++++++++- .../node/copilot/mapSessionEvents.ts | 17 +- .../agentHost/node/sessionPermissions.ts | 4 + .../test/common/partialToolInput.test.ts | 59 ++++ .../node/agentHostToolCallTelemetry.test.ts | 102 +++++- .../test/node/claudeMapSessionEvents.test.ts | 204 ++++++++++++ .../test/node/claudeReplayMapper.test.ts | 45 +++ .../test/node/claudeSubagentSignals.test.ts | 66 +++- .../test/node/claudeToolDisplay.test.ts | 33 ++ .../test/node/copilotAgentSession.test.ts | 187 ++++++++++- .../test/node/copilotToolDisplay.test.ts | 98 +++++- ...ogress-without-exposing-partial-input.yaml | 59 ++++ ...ogress-without-exposing-partial-input.yaml | 55 ++++ .../e2e/harness/agentHostE2ETestHarness.ts | 2 + .../claudeAgentHostE2E.integrationTest.ts | 1 + .../copilotAgentHostE2E.integrationTest.ts | 26 +- .../node/e2e/suites/fileOperationsSuite.ts | 53 +++ .../node/e2e/suites/turnLifecycleSuite.ts | 7 + .../test/node/mapSessionEvents.test.ts | 29 ++ .../agentHost/test/node/reducers.test.ts | 125 ++++++- .../agentHost/agentHostSessionHandler.ts | 18 +- .../agentHost/stateToProgressAdapter.ts | 32 +- .../agentHostChatContribution.test.ts | 113 +++++++ .../stateToProgressAdapter.test.ts | 57 +++- 42 files changed, 2248 insertions(+), 192 deletions(-) create mode 100644 src/vs/platform/agentHost/common/partialToolInput.ts create mode 100644 src/vs/platform/agentHost/common/streamingToolCallDisplay.ts create mode 100644 src/vs/platform/agentHost/test/common/partialToolInput.test.ts create mode 100644 src/vs/platform/agentHost/test/node/e2e/captures/claude-streams-rich-file-creation-progress-without-exposing-partial-input.yaml create mode 100644 src/vs/platform/agentHost/test/node/e2e/captures/copilotcli-streams-rich-file-creation-progress-without-exposing-partial-input.yaml diff --git a/src/vs/platform/agentHost/common/partialToolInput.ts b/src/vs/platform/agentHost/common/partialToolInput.ts new file mode 100644 index 00000000000..3be00e9f96c --- /dev/null +++ b/src/vs/platform/agentHost/common/partialToolInput.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { parse } from '../../../base/common/json.js'; + +const MAX_PARTIAL_TOOL_INPUT_PARSE_LENGTH = 4 * 1024; +let lastDisplayInput: string | undefined; +let lastDisplayValue: Record | undefined; + +export function parsePartialToolInput(raw: string, maxLength?: number): Record | undefined { + const parsed: unknown = parse(maxLength === undefined ? raw : raw.slice(0, maxLength)); + return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) && Object.keys(parsed).length > 0 + ? { ...parsed as Record } + : undefined; +} + +export function parsePartialToolInputForDisplay(raw: string): Record | undefined { + const input = raw.slice(0, MAX_PARTIAL_TOOL_INPUT_PARSE_LENGTH); + if (input !== lastDisplayInput) { + lastDisplayInput = input; + lastDisplayValue = parsePartialToolInput(input); + } + return lastDisplayValue ? { ...lastDisplayValue } : undefined; +} diff --git a/src/vs/platform/agentHost/common/state/protocol/.ahp-version b/src/vs/platform/agentHost/common/state/protocol/.ahp-version index 885d350651d..970c6c67057 100644 --- a/src/vs/platform/agentHost/common/state/protocol/.ahp-version +++ b/src/vs/platform/agentHost/common/state/protocol/.ahp-version @@ -1 +1 @@ -c72272f8 +8e0a9bbf diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-chat/actions.ts b/src/vs/platform/agentHost/common/state/protocol/channels-chat/actions.ts index 3ddcc7d7ef2..ffd7a4eaec9 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-chat/actions.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-chat/actions.ts @@ -184,6 +184,16 @@ export interface ChatToolCallDeltaAction extends ToolCallActionBase { */ export interface ChatToolCallReadyAction extends ToolCallActionBase { type: ActionType.ChatToolCallReady; + /** + * Final contributor metadata. MUST NOT change execution ownership established + * at `chat/toolCallStart`; a client contributor must keep the same `clientId`. + */ + contributor?: ToolCallContributor; + /** + * Final human-readable description of what the tool invocation intends to do. + * When present, replaces the provisional intention from `chat/toolCallStart`. + */ + intention?: string; /** Message describing what the tool will do or what confirmation is needed */ invocationMessage: StringOrMarkdown; /** Raw tool input */ diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-chat/reducer.ts b/src/vs/platform/agentHost/common/state/protocol/channels-chat/reducer.ts index 231ecbfc5c5..b427cd42dfb 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-chat/reducer.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-chat/reducer.ts @@ -7,7 +7,7 @@ // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts import { ActionType } from '../common/actions.js'; -import { TurnState, ToolCallStatus, ToolCallConfirmationReason, ToolCallCancellationReason, ToolCallContributorKind, ResponsePartKind, PendingMessageKind, type ChatState, type ToolCallState, type ResponsePart, type ToolCallResponsePart, type InputRequestResponsePart, type Turn, type PendingMessage, type ConfirmationOption } from './state.js'; +import { TurnState, ToolCallStatus, ToolCallConfirmationReason, ToolCallCancellationReason, ToolCallContributorKind, ResponsePartKind, PendingMessageKind, type ChatState, type ToolCallState, type ResponsePart, type ToolCallResponsePart, type InputRequestResponsePart, type Turn, type PendingMessage, type ConfirmationOption, type ToolCallContributor } from './state.js'; import { SessionStatus } from '../channels-session/state.js'; import type { ChatAction } from '../action-origin.generated.js'; import { softAssertNever } from '../common/reducer-helpers.js'; @@ -33,6 +33,28 @@ function tcBaseWithMeta(tc: ToolCallState, meta: Record | undef }; } +function refineToolCallContributor( + current: ToolCallContributor | undefined, + next: ToolCallContributor | undefined, + log?: (msg: string) => void, +): ToolCallContributor | undefined { + if (!next) { + return current; + } + if (current?.kind === ToolCallContributorKind.Client) { + if (next.kind === ToolCallContributorKind.Client && next.clientId === current.clientId) { + return next; + } + log?.(`Ignoring contributor change for client tool call from '${current.clientId}'`); + return current; + } + if (next.kind === ToolCallContributorKind.Client) { + log?.(`Ignoring late client contributor '${next.clientId}' because client execution ownership must be established at tool call start`); + return current; + } + return next; +} + /** Resolves a selected option from the confirmation options array by ID. */ function resolveSelectedOption(options: ConfirmationOption[] | undefined, id: string | undefined): ConfirmationOption | undefined { if (!id || !options) { @@ -439,7 +461,11 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st ) { return tc; } - const base = tcBaseWithMeta(tc, action._meta); + const base = { + ...tcBaseWithMeta(tc, action._meta), + contributor: refineToolCallContributor(tc.contributor, action.contributor, log), + intention: action.intention ?? tc.intention, + }; if (action.confirmed) { return { status: ToolCallStatus.Running, diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-chat/state.ts b/src/vs/platform/agentHost/common/state/protocol/channels-chat/state.ts index 1419fb15f42..6c8cd3b4cb0 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-chat/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-chat/state.ts @@ -793,21 +793,23 @@ export interface MessageAnnotationsAttachment extends MessageAttachmentBase { } /** - * An attachment that references a chat transcript through a completed turn. - * - * The referenced chat MAY belong to any session, not only the message's own. - * The model representation is a pointer — an `agent-host-session://` link, a - * short transcript excerpt, and a hint to call the `get_session_context` server - * tool — and both that link and that tool already resolve across sessions, so a - * cross-session reference carries the same weight as a same-session one. - * The host resolves the transcript from its first retained turn through - * `endTurn`, inclusive, when accepting the message. Later turns do not change - * the context represented by an already-sent attachment. When `endTurn` is - * omitted (e.g. a drag-and-drop client that cannot know turn ids), the host - * pins it to the referenced chat's latest completed turn as it accepts the - * message; when `endTurn` is provided it MUST reference a completed, retained + * An attachment that references a chat transcript through a fixed completed * turn. * + * The referenced chat MAY belong to a different session than the message's + * chat. The attachment's model representation identifies the chat in a way + * that hosts can resolve regardless of the session that owns it. + * + * When `endTurn` is omitted, the host MUST resolve and pin the referenced + * chat's latest completed turn when accepting the message. This lets clients + * attach a chat without knowing its turn identifiers. When provided, `endTurn` + * MUST reference a completed, retained turn. The host resolves the transcript + * from its first retained turn through the pinned turn, inclusive. Later turns + * do not change the context represented by an already-sent attachment. + * + * When the referenced chat has no completed retained turns, the resolved + * transcript is empty and hosts MUST NOT reject the attachment on that basis. + * * Hosts MUST NOT recursively expand chat attachments found inside the * referenced transcript. Clients SHOULD keep rendering `label` if the * referenced chat is later pruned, and treat opening `resource` as best-effort. @@ -821,8 +823,7 @@ export interface MessageChatAttachment extends MessageAttachmentBase { resource: URI; /** * Last completed turn included in the referenced transcript. When omitted, - * the host resolves the referenced chat's latest completed turn as it - * accepts the message. + * the host pins the latest completed turn when accepting the message. */ endTurn?: string; } 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 897f072ebe1..82764b3f711 100644 --- a/src/vs/platform/agentHost/common/state/protocol/common/messages.ts +++ b/src/vs/platform/agentHost/common/state/protocol/common/messages.ts @@ -48,13 +48,6 @@ export interface JsonRpcErrorResponse { }; } -/** A JSON-RPC parse error cannot identify the request that failed to parse. */ -export interface JsonRpcParseErrorResponse { - readonly jsonrpc: '2.0'; - readonly id: null; - readonly error: JsonRpcErrorResponse['error']; -} - /** * A typed JSON-RPC error response whose error object is a fully typed * {@link AhpError}. Useful when the caller knows the response is an AHP @@ -67,7 +60,7 @@ export interface AhpErrorResponse { } /** A JSON-RPC response (success or error). */ -export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse | JsonRpcParseErrorResponse; +export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse; /** A JSON-RPC notification: has `method` but no `id`. */ export interface JsonRpcNotification { diff --git a/src/vs/platform/agentHost/common/state/sessionProtocol.ts b/src/vs/platform/agentHost/common/state/sessionProtocol.ts index 0241c9a3b50..1fb849eb68f 100644 --- a/src/vs/platform/agentHost/common/state/sessionProtocol.ts +++ b/src/vs/platform/agentHost/common/state/sessionProtocol.ts @@ -16,12 +16,18 @@ export type { JsonRpcErrorResponse, JsonRpcNotification, - JsonRpcParseErrorResponse, JsonRpcRequest, JsonRpcResponse, JsonRpcSuccessResponse, } from './protocol/messages.js'; +/** A JSON-RPC parse error cannot identify the request that failed to parse. */ +export interface JsonRpcParseErrorResponse { + readonly jsonrpc: '2.0'; + readonly id: null; + readonly error: JsonRpcErrorResponse['error']; +} + // Typed message unions export type { AhpClientNotification, diff --git a/src/vs/platform/agentHost/common/streamingToolCallDisplay.ts b/src/vs/platform/agentHost/common/streamingToolCallDisplay.ts new file mode 100644 index 00000000000..68bf3dce447 --- /dev/null +++ b/src/vs/platform/agentHost/common/streamingToolCallDisplay.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { escapeMarkdownLinkLabel } from '../../../base/common/htmlContent.js'; +import { basename } from '../../../base/common/resources.js'; +import { splitLines } from '../../../base/common/strings.js'; +import { URI } from '../../../base/common/uri.js'; +import { localize } from '../../../nls.js'; +import type { StringOrMarkdown } from './state/protocol/state.js'; + +export type ToolPathResolver = (path: string) => string; + +const identityPathResolver: ToolPathResolver = path => path; + +/** + * Minimum interval between streamed tool-call display updates + */ +export const STREAMING_TOOL_DISPLAY_INTERVAL_MS = 100; + +/** Flattens a display message so equal updates can be suppressed. */ +export function streamingToolDisplayText(message: StringOrMarkdown): string { + return typeof message === 'string' ? message : message.markdown; +} + +export function formatGenericToolInput(input: Record | undefined, rawFallback?: string): string | undefined { + if (!input) { + return rawFallback; + } + try { + return JSON.stringify(input, null, 2); + } catch { + return rawFallback; + } +} + +function markdown(value: string): StringOrMarkdown { + return { markdown: value }; +} + +function formatPath(path: unknown, resolvePath: ToolPathResolver): string | undefined { + if (typeof path !== 'string' || !path) { + return undefined; + } + const uri = URI.file(resolvePath(path)); + return `[${escapeMarkdownLinkLabel(basename(uri))}](${uri})`; +} + +export function streamingToolTextLineCount(value: unknown): number | undefined { + return typeof value === 'string' ? splitLines(value).length : undefined; +} + +export function getStreamingEditMessage( + path: unknown, + lineCount: number | undefined, + resolvePath: ToolPathResolver = identityPathResolver, +): StringOrMarkdown { + const file = formatPath(path, resolvePath); + if (lineCount !== undefined) { + if (file) { + return lineCount === 1 + ? markdown(localize('toolStream.editOneLineInFile', "Editing 1 line in {0}", file)) + : markdown(localize('toolStream.editLinesInFile', "Editing {0} lines in {1}", lineCount, file)); + } + return lineCount === 1 + ? localize('toolStream.editOneLine', "Editing 1 line") + : localize('toolStream.editLines', "Editing {0} lines", lineCount); + } + return file + ? markdown(localize('toolStream.editFile', "Editing {0}", file)) + : localize('toolStream.edit', "Editing file"); +} + +export function getStreamingReplaceMessage( + path: unknown, + oldLineCount: number | undefined, + newLineCount: number | undefined, + resolvePath: ToolPathResolver = identityPathResolver, +): StringOrMarkdown { + const file = formatPath(path, resolvePath); + if (oldLineCount !== undefined && newLineCount !== undefined) { + if (file) { + if (oldLineCount === 1 && newLineCount === 1) { + return markdown(localize('toolStream.replaceOneLineWithOneLineInFile', "Replacing 1 line with 1 line in {0}", file)); + } + if (oldLineCount === 1) { + return markdown(localize('toolStream.replaceOneLineWithLinesInFile', "Replacing 1 line with {0} lines in {1}", newLineCount, file)); + } + if (newLineCount === 1) { + return markdown(localize('toolStream.replaceLinesWithOneLineInFile', "Replacing {0} lines with 1 line in {1}", oldLineCount, file)); + } + return markdown(localize('toolStream.replaceLinesWithLinesInFile', "Replacing {0} lines with {1} lines in {2}", oldLineCount, newLineCount, file)); + } + if (oldLineCount === 1 && newLineCount === 1) { + return localize('toolStream.replaceOneLineWithOneLine', "Replacing 1 line with 1 line"); + } + if (oldLineCount === 1) { + return localize('toolStream.replaceOneLineWithLines', "Replacing 1 line with {0} lines", newLineCount); + } + if (newLineCount === 1) { + return localize('toolStream.replaceLinesWithOneLine', "Replacing {0} lines with 1 line", oldLineCount); + } + return localize('toolStream.replaceLinesWithLines', "Replacing {0} lines with {1} lines", oldLineCount, newLineCount); + } + if (oldLineCount !== undefined) { + if (file) { + return oldLineCount === 1 + ? markdown(localize('toolStream.replaceOneLineInFile', "Replacing 1 line in {0}", file)) + : markdown(localize('toolStream.replaceLinesInFile', "Replacing {0} lines in {1}", oldLineCount, file)); + } + return oldLineCount === 1 + ? localize('toolStream.replaceOneLine', "Replacing 1 line") + : localize('toolStream.replaceLines', "Replacing {0} lines", oldLineCount); + } + return getStreamingEditMessage(path, undefined, resolvePath); +} + +export function getStreamingCreateMessage( + path: unknown, + lineCount: number | undefined, + resolvePath: ToolPathResolver = identityPathResolver, +): StringOrMarkdown { + const file = formatPath(path, resolvePath); + if (lineCount !== undefined) { + if (file) { + return lineCount === 1 + ? markdown(localize('toolStream.createOneLineInFile', "Creating {0} (1 line)", file)) + : markdown(localize('toolStream.createLinesInFile', "Creating {0} ({1} lines)", file, lineCount)); + } + return lineCount === 1 + ? localize('toolStream.createOneLine', "Creating file (1 line)") + : localize('toolStream.createLines', "Creating file ({0} lines)", lineCount); + } + return file + ? markdown(localize('toolStream.createFile', "Creating {0}", file)) + : localize('toolStream.create', "Creating file"); +} + +export function getStreamingInsertMessage( + path: unknown, + lineCount: number | undefined, + resolvePath: ToolPathResolver = identityPathResolver, +): StringOrMarkdown { + const file = formatPath(path, resolvePath); + if (lineCount !== undefined) { + if (file) { + return lineCount === 1 + ? markdown(localize('toolStream.insertOneLineInFile', "Inserting 1 line in {0}", file)) + : markdown(localize('toolStream.insertLinesInFile', "Inserting {0} lines in {1}", lineCount, file)); + } + return lineCount === 1 + ? localize('toolStream.insertOneLine', "Inserting 1 line") + : localize('toolStream.insertLines', "Inserting {0} lines", lineCount); + } + return file + ? markdown(localize('toolStream.insertInFile', "Inserting text in {0}", file)) + : localize('toolStream.insert', "Inserting text"); +} + +export function getStreamingPatchMessage( + paths: readonly string[], + lineCount: number | undefined, + resolvePath: ToolPathResolver = identityPathResolver, +): StringOrMarkdown { + const fileList = paths.map(path => formatPath(path, resolvePath)).filter(path => path !== undefined).join(', ') || undefined; + if (lineCount !== undefined) { + if (fileList) { + return lineCount === 1 + ? markdown(localize('toolStream.patchOneLineInFiles', "Generating patch (1 line) in {0}", fileList)) + : markdown(localize('toolStream.patchLinesInFiles', "Generating patch ({0} lines) in {1}", lineCount, fileList)); + } + return lineCount === 1 + ? localize('toolStream.patchOneLine', "Generating patch (1 line)") + : localize('toolStream.patchLines', "Generating patch ({0} lines)", lineCount); + } + return fileList + ? markdown(localize('toolStream.patchFiles', "Generating patch in {0}", fileList)) + : localize('toolStream.patch', "Generating patch"); +} diff --git a/src/vs/platform/agentHost/node/agentHostTelemetryReporter.ts b/src/vs/platform/agentHost/node/agentHostTelemetryReporter.ts index 33688a362ad..a878a9d8a08 100644 --- a/src/vs/platform/agentHost/node/agentHostTelemetryReporter.ts +++ b/src/vs/platform/agentHost/node/agentHostTelemetryReporter.ts @@ -132,7 +132,7 @@ export interface IAgentHostToolInvokedReport { toolId: string; toolSourceKind: string; result: ToolInvokedResult; - invocationTimeMs: number; + invocationTimeMs?: number; } type AgentHostToolCallResponseType = 'success' | 'cancelled' | 'failed'; diff --git a/src/vs/platform/agentHost/node/agentHostToolCallTracker.ts b/src/vs/platform/agentHost/node/agentHostToolCallTracker.ts index 2d674bdad17..76b30e5f104 100644 --- a/src/vs/platform/agentHost/node/agentHostToolCallTracker.ts +++ b/src/vs/platform/agentHost/node/agentHostToolCallTracker.ts @@ -55,13 +55,22 @@ export function toolSourceKindFromContributor(contributor: ToolCallContributor | } } +function canRefineContributor(current: ToolCallContributor | undefined, next: ToolCallContributor): boolean { + if (current?.kind === ToolCallContributorKind.Client) { + return next.kind === ToolCallContributorKind.Client && next.clientId === current.clientId; + } + return next.kind !== ToolCallContributorKind.Client; +} + /** Per-tool-call timing state, keyed by `session:toolCallId`. */ interface IToolCallTiming { - readonly stopWatch: StopWatch; + readonly lifecycleStopWatch: StopWatch; + invocationStopWatch?: StopWatch; readonly provider: string; readonly session: string; readonly toolId: string; - readonly toolSourceKind: string; + contributor: ToolCallContributor | undefined; + toolSourceKind: string; } interface IStalledToolCall { @@ -95,14 +104,33 @@ export class AgentHostToolCallTracker extends Disposable { toolCallStarted(provider: string, session: string, toolCallId: string, toolName: string, contributor: ToolCallContributor | undefined): void { this._toolCalls.set(this._key(session, toolCallId), { - stopWatch: StopWatch.create(true), + lifecycleStopWatch: StopWatch.create(true), provider, session, toolId: toolName, + contributor, toolSourceKind: toolSourceKindFromContributor(contributor), }); } + toolCallMetadataUpdated(session: string, toolCallId: string, contributor: ToolCallContributor | undefined): void { + const timing = this._toolCalls.get(this._key(session, toolCallId)); + if (!timing) { + return; + } + if (contributor && canRefineContributor(timing.contributor, contributor)) { + timing.contributor = contributor; + timing.toolSourceKind = toolSourceKindFromContributor(contributor); + } + } + + toolCallExecutionStarted(session: string, toolCallId: string): void { + const timing = this._toolCalls.get(this._key(session, toolCallId)); + if (timing && !timing.invocationStopWatch) { + timing.invocationStopWatch = StopWatch.create(true); + } + } + toolCallCompleted(session: string, toolCallId: string, result: ToolCallResult): void { const key = this._key(session, toolCallId); const timing = this._toolCalls.get(key); @@ -114,7 +142,7 @@ export class AgentHostToolCallTracker extends Disposable { } this._toolCalls.delete(key); const resultBucket = deriveToolInvokedResult(result); - const totalTimeMs = timing.stopWatch.elapsed(); + const totalTimeMs = timing.lifecycleStopWatch.elapsed(); this._reporter.toolInvoked({ provider: timing.provider, @@ -122,7 +150,7 @@ export class AgentHostToolCallTracker extends Disposable { toolId: timing.toolId, toolSourceKind: timing.toolSourceKind, result: resultBucket, - invocationTimeMs: totalTimeMs, + invocationTimeMs: timing.invocationStopWatch?.elapsed(), }); const stalled = this._stalledToolCalls.get(key); diff --git a/src/vs/platform/agentHost/node/agentSideEffects.ts b/src/vs/platform/agentHost/node/agentSideEffects.ts index 055213b020e..22970e8114c 100644 --- a/src/vs/platform/agentHost/node/agentSideEffects.ts +++ b/src/vs/platform/agentHost/node/agentSideEffects.ts @@ -709,10 +709,14 @@ export class AgentSideEffects extends Disposable { if (action.type === ActionType.ChatToolCallStart && agent) { this._toolCallAgents.set(`${sessionKey}:${action.toolCallId}`, agent.id); // Stamp the tool call start for `languageModelToolInvoked` telemetry. - // Only the start action carries the tool name and contributor, so the - // source kind must be captured here rather than on completion. The - // provider comes from the agent that emitted the signal. + // Ready may refine the contributor once the complete tool metadata is + // available, so the tracker updates the source kind below when needed. this._toolCallTracker.toolCallStarted(agent.id, sessionKey, action.toolCallId, action.toolName, action.contributor); + } else if (action.type === ActionType.ChatToolCallReady) { + this._toolCallTracker.toolCallMetadataUpdated(sessionKey, action.toolCallId, action.contributor); + if (action.confirmed) { + this._toolCallTracker.toolCallExecutionStarted(sessionKey, action.toolCallId); + } } const sessionUri = isAhpChatChannel(sessionKey) ? parseRequiredSessionUriFromChatUri(sessionKey) : sessionKey; @@ -1169,10 +1173,12 @@ export class AgentSideEffects extends Disposable { // Mark confirmations where a persistent allow rule can suppress the next equivalent prompt. effective = { ...effective, state: { ...effective.state, _meta: { ...toolCall?._meta, ...effective.state._meta, ...toToolCallMeta({ autoApproveRuleResolvable: true }) } } }; } - this._stateManager.dispatchServerAction( - sessionKey, - this._permissionManager.createToolReadyAction(effective, sessionKey, turnId) - ); + const readyAction = this._permissionManager.createToolReadyAction(effective, sessionKey, turnId); + this._toolCallTracker.toolCallMetadataUpdated(sessionKey, readyAction.toolCallId, readyAction.contributor); + if (readyAction.confirmed) { + this._toolCallTracker.toolCallExecutionStarted(sessionKey, readyAction.toolCallId); + } + this._stateManager.dispatchServerAction(sessionKey, readyAction); } handleAction(channel: ProtocolURI, action: StateAction, clientId?: string, clientType = AgentHostClientType.Unknown): void { @@ -1236,6 +1242,9 @@ export class AgentSideEffects extends Disposable { throw new Error(`ChatToolCallConfirmed must be handled on an AHP chat channel: ${channel}`); } const toolCallKey = `${channel}:${action.toolCallId}`; + if (action.approved) { + this._toolCallTracker.toolCallExecutionStarted(channel, action.toolCallId); + } const managedApprovalRequired = this._managedApprovalToolCalls.delete(toolCallKey); const agentId = this._toolCallAgents.get(toolCallKey); if (agentId) { diff --git a/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts b/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts index f8fa0250179..25100d63262 100644 --- a/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts +++ b/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts @@ -13,7 +13,7 @@ import { extractForwardedErrorInfo } from '../shared/forwardedChatError.js'; import { buildTopLevelSubagentReadyAction, emitInnerAssistantSignals, mapSubagentSystemMessage, SUBAGENT_SPAWNING_TOOL_NAMES, tagWithParent } from './claudeSubagentSignals.js'; import type { SubagentRegistry } from './claudeSubagentRegistry.js'; import { stripClientToolNamePrefix, hasClientToolNamePrefix } from './clientTools/claudeClientToolMcpServer.js'; -import { buildClaudeToolMeta, getClaudePastTenseMessage, getClaudeToolDisplayName } from './claudeToolDisplay.js'; +import { buildClaudeToolMeta, getClaudePastTenseMessage, getClaudeToolDisplayName, isClaudeFileEditTool } from './claudeToolDisplay.js'; import { claudeToolDenialCode } from './claudeToolDenial.js'; import { ClaudeToolCallRegistry } from './claudeToolCallRegistry.js'; import { ToolCallConfirmationReason, ToolCallContributorKind, type StringOrMarkdown } from '../../common/state/protocol/state.js'; @@ -47,7 +47,7 @@ import { ToolCallConfirmationReason, ToolCallContributorKind, type StringOrMarkd * lifecycle invariants live behind named methods. */ export class ClaudeMapperState { - private readonly _activeToolBlocks = new Map(); + private readonly _activeToolBlocks = new Map(); /** * Phase 8.5 — cross-message tool-call attribution + input * accumulation + computed start-info, encapsulated as its own @@ -55,9 +55,13 @@ export class ClaudeMapperState { * Public so mapper functions can call its lifecycle methods * directly without forwarding through this class. */ - readonly toolCalls = new ClaudeToolCallRegistry(); + readonly toolCalls: ClaudeToolCallRegistry; private _currentMessageId: string | undefined; + constructor(now: () => number = Date.now) { + this.toolCalls = new ClaudeToolCallRegistry(now); + } + /** * Phase 8 — file-edit content pre-staged by * `ClaudeAgentSession._observeUserMessage` and consumed by @@ -88,12 +92,12 @@ export class ClaudeMapperState { * scopes; the per-message map gets drained on `content_block_stop`, * the cross-message maps survive until the matching `tool_result`. */ - startToolBlock(index: number, toolUseId: string, toolName: string, turnId: string): void { - this._activeToolBlocks.set(index, { toolUseId, toolName }); - this.toolCalls.begin(toolUseId, toolName, turnId); + startToolBlock(index: number, toolUseId: string, toolName: string, turnId: string, isClientTool = false): void { + this._activeToolBlocks.set(index, { toolUseId, toolName, isClientTool }); + this.toolCalls.begin(toolUseId, toolName, turnId, isClientTool); } - getActiveToolBlock(index: number): { toolUseId: string; toolName: string } | undefined { + getActiveToolBlock(index: number): { toolUseId: string; toolName: string; isClientTool: boolean } | undefined { return this._activeToolBlocks.get(index); } @@ -133,9 +137,9 @@ export class ClaudeMapperState { * `undefined` if the `tool_use_id` is unknown (defense-in-depth * against transport drift / replay). */ - lookupToolCall(toolUseId: string): { turnId: string; toolName: string } | undefined { + lookupToolCall(toolUseId: string): { turnId: string; toolName: string; isClientTool: boolean } | undefined { const entry = this.toolCalls.lookup(toolUseId); - return entry ? { turnId: entry.turnId, toolName: entry.toolName } : undefined; + return entry ? { turnId: entry.turnId, toolName: entry.toolName, isClientTool: entry.isClientTool } : undefined; } /** Drain cross-message tracking once a `tool_result` is delivered. */ @@ -183,6 +187,20 @@ export class ClaudeMapperState { } } +function fileEditToolDelta(chat: URI, turnId: string, toolCallId: string, invocationMessage: StringOrMarkdown): AgentSignal { + return { + kind: 'action', + resource: chat, + action: { + type: ActionType.ChatToolCallDelta, + turnId, + toolCallId, + content: '', + invocationMessage, + }, + }; +} + /** * Map one SDK message to zero or more agent signals. * @@ -243,7 +261,7 @@ export function mapSDKMessageToAgentSignals( return mapResult(message, chat, turnId, turnDuration, state, logService, registry); case 'assistant': return tagWithParent( - mapAssistantCanonical(message, chat, turnId, state, message.parent_tool_use_id, registry), + mapAssistantCanonical(message, chat, turnId, state, message.parent_tool_use_id, registry, clientToolOwner), chat, message.parent_tool_use_id, registry, @@ -291,6 +309,7 @@ function mapAssistantCanonical( state: ClaudeMapperState, parentToolUseId: string | null, registry: SubagentRegistry, + clientToolOwner?: (toolName: string) => string | undefined, ): AgentSignal[] { if (parentToolUseId === null) { const top: AgentSignal[] = []; @@ -302,7 +321,7 @@ function mapAssistantCanonical( } return top; } - return emitInnerAssistantSignals(message, chat, turnId, state, parentToolUseId, registry); + return emitInnerAssistantSignals(message, chat, turnId, state, parentToolUseId, registry, clientToolOwner); } /** @@ -350,8 +369,12 @@ function mapUserMessage( .map(c => c.text) .join('\n'); const pastTenseMessage: StringOrMarkdown = info - ? getClaudePastTenseMessage(info.toolName, info.displayName, info.parsedInput, !isError, resultText) - : `${getClaudeToolDisplayName(tracked.toolName)} finished`; + ? info.isClientTool + ? info.displayName + : getClaudePastTenseMessage(info.toolName, info.displayName, info.parsedInput, !isError, resultText) + : tracked.isClientTool + ? tracked.toolName + : `${getClaudeToolDisplayName(tracked.toolName)} finished`; // A denied/cancelled tool surfaces as an `is_error` result whose content // is the deny `message` we returned from `canUseTool`; classify it so the // telemetry reports `userCancelled` rather than a generic error. @@ -570,7 +593,7 @@ function mapStreamEvent( // they don't carry the prefix. const toolName = stripClientToolNamePrefix(block.name); const isClientTool = hasClientToolNamePrefix(block.name); - state.startToolBlock(event.index, block.id, toolName, turnId); + state.startToolBlock(event.index, block.id, toolName, turnId, isClientTool); // Phase 12 — subagent correlation bookkeeping. Either this // tool_use is at the top level and (if Task/Agent) spawns a // new subagent, or it is inner and we record its edge to the @@ -593,7 +616,7 @@ function mapStreamEvent( // state transitions (D6). Subagent meta from Phase 12 is now // produced by `buildClaudeToolMeta` because // `getClaudeToolKind('Task') === 'subagent'`. - const meta = buildClaudeToolMeta(toolName); + const meta = isClientTool ? undefined : buildClaudeToolMeta(toolName); const toolClientId = isClientTool ? clientToolOwner?.(toolName) : undefined; return [{ kind: 'action', @@ -603,7 +626,7 @@ function mapStreamEvent( turnId, toolCallId: block.id, toolName, - displayName: getClaudeToolDisplayName(toolName), + displayName: isClientTool ? toolName : getClaudeToolDisplayName(toolName), ...(toolClientId ? { contributor: { kind: ToolCallContributorKind.Client, clientId: toolClientId } } : {}), ...(meta ? { _meta: meta } : {}), }, @@ -644,6 +667,13 @@ function mapStreamEvent( return []; } state.appendToolBlockInputDelta(event.index, event.delta.partial_json); + if (!tracked.isClientTool && isClaudeFileEditTool(tracked.toolName)) { + const update = state.toolCalls.streamingInputUpdate(tracked.toolUseId); + if (!update) { + return []; + } + return [fileEditToolDelta(chat, turnId, tracked.toolUseId, update.invocationMessage)]; + } return [{ kind: 'action', resource: chat, @@ -660,6 +690,9 @@ function mapStreamEvent( case 'content_block_stop': { const tracked = state.getActiveToolBlock(event.index); + const finalStreamingUpdate = tracked && !tracked.isClientTool && isClaudeFileEditTool(tracked.toolName) + ? state.toolCalls.streamingInputUpdate(tracked.toolUseId, true) + : undefined; state.finalizeToolBlock(event.index); state.endToolBlock(event.index); if (!tracked) { @@ -670,8 +703,12 @@ function mapStreamEvent( if (!info) { return []; } - const meta = buildClaudeToolMeta(tracked.toolName); - return [{ + const meta = tracked.isClientTool ? undefined : buildClaudeToolMeta(tracked.toolName); + const signals: AgentSignal[] = []; + if (finalStreamingUpdate) { + signals.push(fileEditToolDelta(chat, turnId, tracked.toolUseId, finalStreamingUpdate.invocationMessage)); + } + signals.push({ kind: 'action', resource: chat, action: { @@ -683,7 +720,8 @@ function mapStreamEvent( confirmed: ToolCallConfirmationReason.NotNeeded, ...(meta ? { _meta: meta } : {}), }, - }]; + }); + return signals; } case 'message_delta': diff --git a/src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts b/src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts index c82f9d8d543..5522564e786 100644 --- a/src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts +++ b/src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts @@ -24,8 +24,9 @@ import { } from '../../common/state/protocol/state.js'; import { buildSubagentSessionUri } from '../../common/state/sessionState.js'; import { readToolCallMeta } from '../../common/meta/agentToolCallMeta.js'; +import { formatGenericToolInput } from '../../common/streamingToolCallDisplay.js'; import { buildClaudeToolMeta, getClaudeInvocationMessage, getClaudePastTenseMessage, getClaudeToolDisplayName, getClaudeToolInputString } from './claudeToolDisplay.js'; -import { stripClientToolNamePrefix } from './clientTools/claudeClientToolMcpServer.js'; +import { hasClientToolNamePrefix, stripClientToolNamePrefix } from './clientTools/claudeClientToolMcpServer.js'; /** * Phase 13 — replay mapper. Reduces a flat `SessionMessage[]` (the SDK's @@ -266,7 +267,7 @@ class ReplayBuilder { * pattern but simpler (replay has the full input synchronously on * the `tool_use` block). */ - private readonly _toolUses = new Map | undefined }>(); + private readonly _toolUses = new Map | undefined; readonly isClientTool: boolean }>(); /** Turns opened from a leading assistant envelope because the prompt was missing. Reported once by {@link finish}. */ private _recoveredPromptlessTurns = 0; @@ -376,7 +377,7 @@ class ReplayBuilder { // the workbench-registered tool by its unprefixed name (matches the // live stream mapper). Without this, replayed client-tool calls // fall back to the generic "Run MCP tool" rendering. - this._openToolUse(block.id, stripClientToolNamePrefix(block.name), block.input); + this._openToolUse(block.id, stripClientToolNamePrefix(block.name), block.input, hasClientToolNamePrefix(block.name)); } // Other block types (server_tool_use, etc.) are dropped silently per M7. } @@ -385,21 +386,23 @@ class ReplayBuilder { } } - private _openToolUse(toolUseId: string, toolName: string, input: unknown): void { + private _openToolUse(toolUseId: string, toolName: string, input: unknown, isClientTool: boolean): void { if (this._active === undefined) { return; } - const displayName = getClaudeToolDisplayName(toolName); + const displayName = isClientTool ? toolName : getClaudeToolDisplayName(toolName); const parsedInput = input !== null && typeof input === 'object' ? input as Record : undefined; - const meta = buildClaudeToolMeta(toolName); + const meta = isClientTool ? undefined : buildClaudeToolMeta(toolName); // Build a placeholder Cancelled state by default; replaced with Completed when the tool_result lands. const placeholder: ToolCallCancelledState = { status: ToolCallStatus.Cancelled, toolCallId: toolUseId, toolName, displayName, - invocationMessage: getClaudeInvocationMessage(toolName, displayName, parsedInput), - toolInput: parsedInput !== undefined ? getClaudeToolInputString(toolName, parsedInput) : (typeof input === 'string' ? input : input !== undefined ? safeStringify(input) : undefined), + invocationMessage: isClientTool ? displayName : getClaudeInvocationMessage(toolName, displayName, parsedInput), + toolInput: parsedInput !== undefined + ? isClientTool ? formatGenericToolInput(parsedInput) : getClaudeToolInputString(toolName, parsedInput) + : (typeof input === 'string' ? input : input !== undefined ? safeStringify(input) : undefined), reason: ToolCallCancellationReason.Skipped, ...(meta ? { _meta: meta } : {}), }; @@ -410,7 +413,7 @@ class ReplayBuilder { this._active.responseParts.push(part); this._active.toolCallParts.set(toolUseId, part); this._active.pendingToolUseIds.add(toolUseId); - this._toolUses.set(toolUseId, { turnId: this._active.id, parsedInput }); + this._toolUses.set(toolUseId, { turnId: this._active.id, parsedInput, isClientTool }); } private _attachToolResult(block: UserToolResultBlock): string | undefined { @@ -449,7 +452,9 @@ class ReplayBuilder { toolInput: previousState.status === ToolCallStatus.Streaming ? undefined : previousState.toolInput, confirmed: ToolCallConfirmationReason.NotNeeded, success: !isError, - pastTenseMessage: getClaudePastTenseMessage(previousState.toolName, previousState.displayName, entry.parsedInput, !isError, resultText), + pastTenseMessage: entry.isClientTool + ? previousState.displayName + : getClaudePastTenseMessage(previousState.toolName, previousState.displayName, entry.parsedInput, !isError, resultText), content: content.length > 0 ? content : undefined, ...(previousState._meta ? { _meta: previousState._meta } : {}), }; diff --git a/src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts b/src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts index a10365af689..a90f19aa930 100644 --- a/src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts +++ b/src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts @@ -9,11 +9,11 @@ import type { Mutable } from '../../../../base/common/types.js'; import { toToolCallMeta, type IToolCallMeta } from '../../common/meta/agentToolCallMeta.js'; import type { AgentSignal, IAgentSubagentStartedSignal } from '../../common/agentService.js'; import { ActionType } from '../../common/state/sessionActions.js'; -import { ResponsePartKind, ToolCallConfirmationReason } from '../../common/state/sessionState.js'; +import { ResponsePartKind, ToolCallConfirmationReason, ToolCallContributorKind } from '../../common/state/sessionState.js'; import type { ClaudeMapperState } from './claudeMapSessionEvents.js'; import { SUBAGENT_TOOL_NAMES, type SubagentRegistry } from './claudeSubagentRegistry.js'; import { buildClaudeToolCallMeta, buildClaudeToolMeta, getClaudeInvocationMessage, getClaudeToolDisplayName, getClaudeToolInputString } from './claudeToolDisplay.js'; -import { stripClientToolNamePrefix } from './clientTools/claudeClientToolMcpServer.js'; +import { hasClientToolNamePrefix, stripClientToolNamePrefix } from './clientTools/claudeClientToolMcpServer.js'; /** * Phase 12 — SDK tool names that spawn subagent sessions. Re-exported @@ -215,6 +215,7 @@ export function emitInnerAssistantSignals( state: ClaudeMapperState, parentToolUseId: string, registry: SubagentRegistry, + clientToolOwner?: (toolName: string) => string | undefined, ): AgentSignal[] { const messageId = message.message.id; const signals: AgentSignal[] = []; @@ -257,7 +258,9 @@ export function emitInnerAssistantSignals( // calls render with their real name (matches the top-level stream // mapper). SDK-owned tools and Task/Agent passes through unchanged. const toolName = stripClientToolNamePrefix(block.name); - state.startToolBlock(index, block.id, toolName, turnId); + const isClientTool = hasClientToolNamePrefix(block.name); + const clientId = isClientTool ? clientToolOwner?.(toolName) : undefined; + state.startToolBlock(index, block.id, toolName, turnId, isClientTool); // Inner tool input arrives pre-parsed on the synthesized // `assistant` message (not via `input_json_delta` chunks), so // seed the registry directly. Without this the live @@ -266,9 +269,10 @@ export function emitInnerAssistantSignals( // always computes rich text) drifts from live — violating D6. state.toolCalls.seedParsedInput(block.id, block.input); registry.noteInnerTool(block.id, parentToolUseId); - const displayName = getClaudeToolDisplayName(toolName); - const meta = buildClaudeToolMeta(toolName); - const toolInputStr = getClaudeToolInputString(toolName, block.input); + const displayName = isClientTool ? toolName : getClaudeToolDisplayName(toolName); + const meta = isClientTool ? undefined : buildClaudeToolMeta(toolName); + const info = state.toolCalls.lookup(block.id)?.info; + const toolInputStr = info?.toolInput ?? getClaudeToolInputString(toolName, block.input); signals.push({ kind: 'action', resource: chat, @@ -278,6 +282,7 @@ export function emitInnerAssistantSignals( toolCallId: block.id, toolName, displayName, + ...(clientId ? { contributor: { kind: ToolCallContributorKind.Client, clientId } } : {}), ...(meta ? { _meta: meta } : {}), }, }); @@ -288,7 +293,7 @@ export function emitInnerAssistantSignals( type: ActionType.ChatToolCallReady, turnId, toolCallId: block.id, - invocationMessage: getClaudeInvocationMessage(toolName, displayName, block.input), + invocationMessage: isClientTool ? displayName : getClaudeInvocationMessage(toolName, displayName, block.input), ...(toolInputStr !== undefined ? { toolInput: toolInputStr } : {}), confirmed: ToolCallConfirmationReason.NotNeeded, }, diff --git a/src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts b/src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts index 83abf44a0f3..586faa83a03 100644 --- a/src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts +++ b/src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import type { ILogService } from '../../../log/common/log.js'; +import { parsePartialToolInput } from '../../common/partialToolInput.js'; +import { formatGenericToolInput, STREAMING_TOOL_DISPLAY_INTERVAL_MS, streamingToolDisplayText } from '../../common/streamingToolCallDisplay.js'; import type { StringOrMarkdown } from '../../common/state/protocol/state.js'; -import { getClaudeInvocationMessage, getClaudeToolDisplayName, getClaudeToolInputString } from './claudeToolDisplay.js'; +import { getClaudeInvocationMessage, getClaudeStreamingInvocationMessage, getClaudeToolDisplayName, getClaudeToolInputString } from './claudeToolDisplay.js'; /** * Phase 8.5 — per-tool-call info computed at `content_block_stop` and @@ -21,15 +23,24 @@ export interface IClaudeToolStartInfo { readonly parsedInput: Record | undefined; readonly invocationMessage: StringOrMarkdown; readonly toolInput: string | undefined; + readonly isClientTool: boolean; } interface IRegistryEntry { readonly toolName: string; readonly turnId: string; + readonly isClientTool: boolean; inputBuffer: string; + displayedInputLength: number; + displayedAt: number | undefined; + displayedMessage: string | undefined; info: IClaudeToolStartInfo | undefined; } +export interface IClaudeStreamingToolInputUpdate { + readonly invocationMessage: StringOrMarkdown; +} + /** * Phase 8.5 — per-session, cross-message tool-call tracking for the * live mapper. Owns: @@ -63,16 +74,22 @@ interface IRegistryEntry { export class ClaudeToolCallRegistry { private readonly _entries = new Map(); + constructor(private readonly _now: () => number = Date.now) { } + /** * Begin tracking a tool call. Called from `content_block_start` * for a `tool_use` block. Allocates the delta buffer; the * computed info bag is filled in by {@link finalize}. */ - begin(toolUseId: string, toolName: string, turnId: string): void { + begin(toolUseId: string, toolName: string, turnId: string, isClientTool = false): void { this._entries.set(toolUseId, { toolName, turnId, + isClientTool, inputBuffer: '', + displayedInputLength: 0, + displayedAt: undefined, + displayedMessage: undefined, info: undefined, }); } @@ -90,6 +107,37 @@ export class ClaudeToolCallRegistry { entry.inputBuffer += partialJson; } + /** + * Renders the next streaming display message for a file-edit tool, or + * `undefined` when nothing new should be shown. Throttled on elapsed time + * only: the SDK streams argument text token by token, so a size-based rule + * would make updates rarer as the edit grows. `force` bypasses the interval + * for the final flush at `content_block_stop`. Identical messages are + * suppressed so a steady tick does not re-send an unchanged row. + */ + streamingInputUpdate(toolUseId: string, force = false): IClaudeStreamingToolInputUpdate | undefined { + const entry = this._entries.get(toolUseId); + if (!entry || entry.displayedInputLength === entry.inputBuffer.length) { + return undefined; + } + const now = this._now(); + if (!force && entry.displayedAt !== undefined && now - entry.displayedAt < STREAMING_TOOL_DISPLAY_INTERVAL_MS) { + return undefined; + } + const invocationMessage = getClaudeStreamingInvocationMessage(entry.toolName, parsePartialToolInput(entry.inputBuffer)); + if (!invocationMessage) { + return undefined; + } + entry.displayedInputLength = entry.inputBuffer.length; + entry.displayedAt = now; + const message = streamingToolDisplayText(invocationMessage); + if (message === entry.displayedMessage) { + return undefined; + } + entry.displayedMessage = message; + return { invocationMessage }; + } + /** * Parse the accumulated buffer and stash the computed * {@link IClaudeToolStartInfo}. Called from `content_block_stop`. @@ -143,13 +191,14 @@ export class ClaudeToolCallRegistry { } private _writeInfo(entry: IRegistryEntry, parsedInput: Record | undefined, rawFallback?: string): void { - const displayName = getClaudeToolDisplayName(entry.toolName); + const displayName = entry.isClientTool ? entry.toolName : getClaudeToolDisplayName(entry.toolName); entry.info = { toolName: entry.toolName, displayName, parsedInput, - invocationMessage: getClaudeInvocationMessage(entry.toolName, displayName, parsedInput), - toolInput: getClaudeToolInputString(entry.toolName, parsedInput) ?? rawFallback, + invocationMessage: entry.isClientTool ? displayName : getClaudeInvocationMessage(entry.toolName, displayName, parsedInput), + toolInput: entry.isClientTool ? formatGenericToolInput(parsedInput, rawFallback) : getClaudeToolInputString(entry.toolName, parsedInput) ?? rawFallback, + isClientTool: entry.isClientTool, }; } @@ -159,12 +208,12 @@ export class ClaudeToolCallRegistry { * drift / replay). The `info` field may be `undefined` if the * tool block never reached `content_block_stop`. */ - lookup(toolUseId: string): { readonly turnId: string; readonly toolName: string; readonly info: IClaudeToolStartInfo | undefined } | undefined { + lookup(toolUseId: string): { readonly turnId: string; readonly toolName: string; readonly isClientTool: boolean; readonly info: IClaudeToolStartInfo | undefined } | undefined { const entry = this._entries.get(toolUseId); if (!entry) { return undefined; } - return { turnId: entry.turnId, toolName: entry.toolName, info: entry.info }; + return { turnId: entry.turnId, toolName: entry.toolName, isClientTool: entry.isClientTool, info: entry.info }; } /** diff --git a/src/vs/platform/agentHost/node/claude/claudeToolDisplay.ts b/src/vs/platform/agentHost/node/claude/claudeToolDisplay.ts index 2a913fe5611..5fb53566b50 100644 --- a/src/vs/platform/agentHost/node/claude/claudeToolDisplay.ts +++ b/src/vs/platform/agentHost/node/claude/claudeToolDisplay.ts @@ -8,6 +8,7 @@ import { appendEscapedMarkdownInlineCode, escapeMarkdownLinkLabel } from '../../ import { basename } from '../../../../base/common/resources.js'; import { truncate } from '../../../../base/common/strings.js'; import { URI } from '../../../../base/common/uri.js'; +import { getStreamingCreateMessage, getStreamingEditMessage, getStreamingReplaceMessage, streamingToolTextLineCount } from '../../common/streamingToolCallDisplay.js'; import { toToolCallMeta, type IToolCallMeta, type ToolKind } from '../../common/meta/agentToolCallMeta.js'; import type { StringOrMarkdown } from '../../common/state/protocol/state.js'; import { getServerToolDisplay } from '../shared/serverToolGroups.js'; @@ -470,6 +471,42 @@ export function getClaudeInvocationMessage( } } +export function getClaudeStreamingInvocationMessage(toolName: string, input: Record | undefined): StringOrMarkdown | undefined { + switch (toolName) { + case 'Write': + return getStreamingCreateMessage(input?.['file_path'], streamingToolTextLineCount(input?.['content'])); + case 'Edit': + return getStreamingReplaceMessage( + input?.['file_path'], + streamingToolTextLineCount(input?.['old_string']), + streamingToolTextLineCount(input?.['new_string']), + ); + case 'MultiEdit': { + const edits = Array.isArray(input?.['edits']) ? input['edits'] : []; + let oldLineCount: number | undefined; + let newLineCount: number | undefined; + for (const edit of edits) { + if (!edit || typeof edit !== 'object' || Array.isArray(edit)) { + continue; + } + const oldLines = streamingToolTextLineCount((edit as Record)['old_string']); + const newLines = streamingToolTextLineCount((edit as Record)['new_string']); + if (oldLines !== undefined) { + oldLineCount = (oldLineCount ?? 0) + oldLines; + } + if (newLines !== undefined) { + newLineCount = (newLineCount ?? 0) + newLines; + } + } + return getStreamingReplaceMessage(input?.['file_path'], oldLineCount, newLineCount); + } + case 'NotebookEdit': + return getStreamingEditMessage(input?.['notebook_path'], streamingToolTextLineCount(input?.['new_source'])); + default: + return undefined; + } +} + /** * Phase 8.5 — success-aware rich past-tense message. Mirror of * [`copilotToolDisplay.getPastTenseMessage`](../copilot/copilotToolDisplay.ts#L572). diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index 42de10a84c5..bf6028f0ff2 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import type { CopilotSession, CurrentToolMetadata, ExitPlanModeRequest, McpServersLoadedServer, MessageOptions, PermissionAllowAllMode, PermissionAutoApproval, PermissionRequestResult, PermissionResult, SessionConfig, Tool, ToolResultObject, McpServerStatus as SdkMcpServerStatus } from '@github/copilot-sdk'; -import { raceCancellation, Sequencer, Throttler } from '../../../../base/common/async.js'; +import { raceCancellation, RunOnceScheduler, Sequencer, Throttler } from '../../../../base/common/async.js'; import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter } from '../../../../base/common/event.js'; import { CancellationError, getErrorMessage } from '../../../../base/common/errors.js'; import { escapeMarkdownSyntaxTokens } from '../../../../base/common/htmlContent.js'; -import { Disposable, IReference, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, IReference, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { isAuthorizationProtectedResourceMetadata } from '../../../../base/common/oauth.js'; import { safeStringify } from '../../../../base/common/objects.js'; @@ -39,9 +39,10 @@ import { readToolCallMeta, toToolCallMeta, type IToolCallMeta, type IToolCallUiM import { OtelData, type OtelAttributeValue } from '../../common/otlp/otlpLogEmitter.js'; import { SessionConfigKey } from '../../common/sessionConfigKeys.js'; import { resolveCopilotConfigSlashCommandOnSend } from '../../common/copilotConfigSlashCommands.js'; +import { STREAMING_TOOL_DISPLAY_INTERVAL_MS, streamingToolDisplayText } from '../../common/streamingToolCallDisplay.js'; import { isAgentFeedbackAnnotationsAttachment, renderAgentFeedbackAnnotationsAttachment } from '../../common/meta/agentFeedbackAttachments.js'; import { ISessionDatabase, ISessionDataService, SESSION_ATTACHMENTS_DIRNAME } from '../../common/sessionDataService.js'; -import { MessageAttachmentKind, ToolCallContributorKind, type FileEdit, type MessageAttachment } from '../../common/state/protocol/state.js'; +import { MessageAttachmentKind, ToolCallContributorKind, type FileEdit, type MessageAttachment, type ToolCallContributor } from '../../common/state/protocol/state.js'; import { ActionType, isChatAction, type ChatAction, type SessionAction } from '../../common/state/sessionActions.js'; import { MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallStatus, ToolResultContentType, buildSubagentSessionUri, getToolSubagentContent, isDefaultChatUri, isSubagentSession, withSessionPromptCacheState, type Message, type PendingMessage, type ChatInputAnswer, type ChatInputOption, type ChatInputQuestion, type ChatInputRequest, type ToolCallResult, type ToolResultContent, type ToolResultTerminalContent, type Turn, type UsageInfo, type UsageInfoMeta, type IContextAttributionData } from '../../common/state/sessionState.js'; import { IAgentConfigurationService } from '../agentConfigurationService.js'; @@ -59,7 +60,7 @@ import type { IUnsandboxedCommandConfirmationRequest, ShellManager } from './cop import { NonPtyShellTerminalStreams } from './copilotNonPtyShellTerminals.js'; import { buildSandboxConfigForSdk, type ISdkSandboxConfig } from './sandboxConfigForSdk.js'; import type { IAgentServerToolHost } from '../../common/agentServerTools.js'; -import { getEditFilePaths, getInvocationMessage, getPastTenseMessage, getPermissionDisplay, getShellIntention, getShellLanguage, getSubagentMetadata, getTaskCompleteMarkdown, getToolDisplayName, getToolInputString, getToolKind, isAgentCoordinationTool, isEditTool, isHiddenTool, isShellTool, isTaskCompleteTool, synthesizeSkillToolCall, tryStringify, type ITypedPermissionRequest } from './copilotToolDisplay.js'; +import { getEditFilePaths, getInvocationMessage, getPastTenseMessage, getPermissionDisplay, getShellIntention, getShellLanguage, getStreamingInvocationMessage, getSubagentMetadata, getTaskCompleteMarkdown, getToolDisplayName, getToolInputString, getToolKind, isAgentCoordinationTool, isEditTool, isHiddenTool, isShellTool, isTaskCompleteTool, parseCopilotStreamingToolInput, synthesizeSkillToolCall, tryStringify, type ITypedPermissionRequest } from './copilotToolDisplay.js'; import { FileEditTracker } from '../shared/fileEditTracker.js'; import { ICopilotApiService, type IRestrictedTelemetryContext } from '../shared/copilotApiService.js'; import type { IAgentHostRestrictedTelemetryContext } from '../agentHostRestrictedTelemetry.js'; @@ -101,6 +102,27 @@ interface IMcpAuthToolCall { readonly parentToolCallId: string | undefined; } +interface ICopilotActiveToolCall { + readonly toolName: string; + readonly displayName: string; + readonly parameters: Record | undefined; + readonly content: ToolResultContent[]; + readonly parentToolCallId: string | undefined; + readonly mcpServerName: string | undefined; + readonly contributor: ToolCallContributor | undefined; + readonly intention: string | undefined; + meta: IToolCallMeta | undefined; +} + +interface ICopilotStreamingToolCall { + input: string; + toolName: string | undefined; + parentToolCallId: string | undefined; + started: boolean; + displayedInputLength: number; + displayedMessage: string | undefined; +} + const COPILOT_HOME_DIRECTORY = '.copilot'; const SESSION_STATE_DIRECTORY = join(COPILOT_HOME_DIRECTORY, 'session-state'); const EMPTY_TOOL_RESULT_TEXT = ''; @@ -582,7 +604,9 @@ export class CopilotAgentSession extends Disposable { get workingDirectory(): URI | undefined { return this._workingDirectory; } /** Tracks active tool invocations so we can produce past-tense messages on completion. */ - private readonly _activeToolCalls = new Map | undefined; content: ToolResultContent[]; parentToolCallId: string | undefined; mcpServerName: string | undefined; meta: IToolCallMeta | undefined }>(); + private readonly _activeToolCalls = new Map(); + private readonly _streamingToolCalls = new Map(); + private readonly _streamingToolDisplaySchedulers = this._register(new DisposableMap()); /** * Maps a subagent's stable `agentId` to its parent tool call id. Completion * ends the current subagent turn, but steering can start another turn with @@ -1062,12 +1086,97 @@ export class CopilotAgentSession extends Disposable { return false; } + private _getToolCallContributor(toolName: string, mcpServerName: string | undefined): ToolCallContributor | undefined { + const clientToolName = this._clientToolName(toolName); + if (this._clientToolNames.has(clientToolName)) { + const clientId = this._activeClientToolSet.ownerOf(clientToolName, this._currentTurn?.senderClientId); + return clientId ? { kind: ToolCallContributorKind.Client, clientId } : undefined; + } + if (mcpServerName) { + const customizationId = this._mcpCustomizations.customizationIdForServer(mcpServerName); + return customizationId ? { kind: ToolCallContributorKind.MCP, customizationId } : undefined; + } + return undefined; + } + + private _createToolCallMeta(toolName: string, parameters: Record | undefined): Mutable { + const toolKind = getToolKind(toolName); + const subagentMeta = toolKind === 'subagent' ? getSubagentMetadata(parameters) : undefined; + return { + toolKind, + language: toolKind === 'terminal' ? getShellLanguage(toolName) : undefined, + subagentDescription: subagentMeta?.description, + subagentAgentName: subagentMeta?.agentName, + }; + } + + private _getStreamingToolCallDisplay(toolName: string, input: string) { + const partialInput = parseCopilotStreamingToolInput(input); + const parameters = partialInput !== null && typeof partialInput === 'object' && !Array.isArray(partialInput) + ? partialInput as Record + : undefined; + return { + parameters, + meta: this._createToolCallMeta(toolName, parameters), + invocationMessage: getStreamingInvocationMessage(toolName, getToolDisplayName(toolName), partialInput, path => this._resolveEditFilePath(path)), + }; + } + + private _emitStreamingToolCallDisplay(toolCallId: string, streaming: ICopilotStreamingToolCall): void { + if (!streaming.toolName) { + return; + } + const display = this._getStreamingToolCallDisplay(streaming.toolName, streaming.input); + streaming.displayedInputLength = streaming.input.length; + const message = streamingToolDisplayText(display.invocationMessage); + if (message === streaming.displayedMessage) { + return; + } + streaming.displayedMessage = message; + this._emitAction({ + type: ActionType.ChatToolCallDelta, + turnId: this._turnId, + toolCallId, + content: '', + invocationMessage: display.invocationMessage, + _meta: toToolCallMeta(display.meta), + }, streaming.parentToolCallId); + } + + private _scheduleStreamingToolCallDisplay(toolCallId: string): void { + let scheduler = this._streamingToolDisplaySchedulers.get(toolCallId); + if (!scheduler) { + scheduler = new RunOnceScheduler(() => { + const streaming = this._streamingToolCalls.get(toolCallId); + if (!streaming?.started || !streaming.toolName) { + return; + } + if (streaming.displayedInputLength === streaming.input.length) { + return; + } + this._emitStreamingToolCallDisplay(toolCallId, streaming); + }, STREAMING_TOOL_DISPLAY_INTERVAL_MS); + this._streamingToolDisplaySchedulers.set(toolCallId, scheduler); + } + if (!scheduler.isScheduled()) { + scheduler.schedule(); + } + } + + private _beginToolCallRound(parentToolCallId: string | undefined): void { + const scope = parentToolCallId ?? ''; + this._currentTurn?.markdownPartIds.delete(scope); + this._currentTurn?.reasoningPartIds.delete(scope); + } + /** * Starts a fresh `pending` turn, discarding any per-turn streaming state * 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, senderClientId?: string, clientType = AgentHostClientType.Unknown): void { + this._streamingToolCalls.clear(); + this._streamingToolDisplaySchedulers.clearAndDisposeAll(); this._currentTurn = new CopilotTurn(turnId, this._nextTurnOrdinal++, senderClientId, clientType); } @@ -1156,6 +1265,8 @@ export class CopilotAgentSession extends Disposable { */ private _clearActiveTurn(): void { this._currentTurn = undefined; + this._streamingToolCalls.clear(); + this._streamingToolDisplaySchedulers.clearAndDisposeAll(); try { this._onTurnEnded(); } catch (err) { @@ -1449,7 +1560,9 @@ export class CopilotAgentSession extends Disposable { type: ActionType.ChatToolCallReady, turnId: this._turnId, toolCallId, - invocationMessage: getInvocationMessage(tracked.toolName, tracked.displayName, tracked.parameters), + ...(tracked.contributor ? { contributor: tracked.contributor } : {}), + ...(tracked.intention !== undefined ? { intention: tracked.intention } : {}), + invocationMessage: getInvocationMessage(tracked.toolName, tracked.displayName, tracked.parameters, path => this._resolveEditFilePath(path)), toolInput: getToolInputString(tracked.toolName, tracked.parameters, tracked.parameters ? tryStringify(tracked.parameters) : undefined), confirmed: ToolCallConfirmationReason.NotNeeded, _meta: toToolCallMeta({ ...(tracked.meta ?? {}), toolSearchCandidates: candidates }), @@ -2490,7 +2603,7 @@ export class CopilotAgentSession extends Disposable { toolCallId, toolName: request.toolName, displayName, - invocationMessage: getInvocationMessage(request.toolName, displayName, parameters), + invocationMessage: getInvocationMessage(request.toolName, displayName, parameters, path => this._resolveEditFilePath(path)), toolInput: getToolInputString(request.toolName, parameters, tryStringify(parameters)), riskAssessment: autoApproval?.reason ? { @@ -2638,7 +2751,8 @@ export class CopilotAgentSession extends Disposable { // route the resulting ChatToolCallReady to the correct // subagent session — without it the action would land on the // parent session, which has no matching ChatToolCallStart. - const parentToolCallId = this._activeToolCalls.get(toolCallId)?.parentToolCallId; + const trackedToolCall = this._activeToolCalls.get(toolCallId); + const parentToolCallId = trackedToolCall?.parentToolCallId; this._onDidSessionProgress.fire({ kind: 'pending_confirmation', chat: this._chatChannelUri, @@ -2647,6 +2761,8 @@ export class CopilotAgentSession extends Disposable { toolCallId, toolName, displayName: getToolDisplayName(toolName), + contributor: trackedToolCall?.contributor, + intention: trackedToolCall?.intention, invocationMessage, toolInput, confirmationTitle, @@ -3534,24 +3650,24 @@ export class CopilotAgentSession extends Disposable { // Other fields (toolRequests, reasoningText, encryptedContent) are // only used for history reconstruction and live tool calls fire their // own tool_start events, so we can safely drop them here. - if (!e.data.content) { - return; - } if (this._shouldDropUnmappedSubagentEvent(e, 'assistant.message')) { return; } const parentToolCallId = this._parentToolCallIdForSubagentEvent(e); const markdownScope = parentToolCallId ?? ''; - if (this._currentTurn?.markdownPartIds.has(markdownScope)) { - return; + if (e.data.content && !this._currentTurn?.markdownPartIds.has(markdownScope)) { + const partId = generateUuid(); + this._currentTurn?.markdownPartIds.set(markdownScope, partId); + this._emitAction({ + type: ActionType.ChatResponsePart, + turnId: this._turnId, + part: { kind: ResponsePartKind.Markdown, id: partId, content: e.data.content }, + }, parentToolCallId); + } + if (e.data.toolRequests?.length) { + // Wait for the full message boundary; clearing on an earlier tool delta would duplicate assembled markdown. + this._beginToolCallRound(parentToolCallId); } - const partId = generateUuid(); - this._currentTurn?.markdownPartIds.set(markdownScope, partId); - this._emitAction({ - type: ActionType.ChatResponsePart, - turnId: this._turnId, - part: { kind: ResponsePartKind.Markdown, id: partId, content: e.data.content }, - }, parentToolCallId); })); // TODO@connor4312: Remove this correlation once the SDK permission callback includes auto-approval data. @@ -3596,8 +3712,61 @@ export class CopilotAgentSession extends Disposable { } })); + this._register(wrapper.onToolCallDelta(e => { + this._logService.trace(`[Copilot:${sessionId}] Tool call delta: ${e.data.toolName ?? ''} (${e.data.toolCallId})`); + this._resumeSubagentForEvent(e); + if (this._shouldDropUnmappedSubagentEvent(e, 'assistant.tool_call_delta')) { + return; + } + + const existing = this._streamingToolCalls.get(e.data.toolCallId); + const streaming = existing ?? { + input: '', + toolName: undefined, + parentToolCallId: undefined, + started: false, + displayedInputLength: 0, + displayedMessage: undefined, + }; + streaming.input += e.data.inputDelta; + if (e.data.toolName) { + if (streaming.toolName && streaming.toolName !== e.data.toolName) { + this._logService.warn(`[Copilot:${sessionId}] Tool call ${e.data.toolCallId} changed name while streaming from ${streaming.toolName} to ${e.data.toolName}`); + } else { + streaming.toolName = e.data.toolName; + } + } + this._streamingToolCalls.set(e.data.toolCallId, streaming); + + const toolName = streaming.toolName; + if (!toolName || isHiddenTool(toolName) || isTaskCompleteTool(toolName) || this._clientToolNames.has(this._clientToolName(toolName))) { + return; + } + if (!streaming.started) { + streaming.parentToolCallId = this._parentToolCallIdForSubagentEvent(e); + } + + if (!streaming.started) { + streaming.started = true; + this._emitAction({ + type: ActionType.ChatToolCallStart, + turnId: this._turnId, + toolCallId: e.data.toolCallId, + toolName, + displayName: getToolDisplayName(toolName), + contributor: this._getToolCallContributor(toolName, undefined), + _meta: toToolCallMeta(this._createToolCallMeta(toolName, undefined)), + }, streaming.parentToolCallId); + this._emitStreamingToolCallDisplay(e.data.toolCallId, streaming); + return; + } + this._scheduleStreamingToolCallDisplay(e.data.toolCallId); + })); + this._register(wrapper.onToolStart(e => { if (isHiddenTool(e.data.toolName)) { + this._streamingToolDisplaySchedulers.deleteAndDispose(e.data.toolCallId); + this._streamingToolCalls.delete(e.data.toolCallId); this._logService.trace(`[Copilot:${sessionId}] Tool started (hidden): ${e.data.toolName}`); return; } @@ -3614,13 +3783,36 @@ export class CopilotAgentSession extends Disposable { toolArgs = tryStringify(parameters); } const displayName = getToolDisplayName(e.data.toolName); + const streamed = this._streamingToolCalls.get(e.data.toolCallId); + this._streamingToolDisplaySchedulers.deleteAndDispose(e.data.toolCallId); + if (streamed?.started && streamed.displayedInputLength < streamed.input.length) { + this._emitStreamingToolCallDisplay(e.data.toolCallId, streamed); + } + this._streamingToolCalls.delete(e.data.toolCallId); + if (streamed?.toolName && streamed.toolName !== e.data.toolName) { + this._logService.warn(`[Copilot:${sessionId}] Tool call ${e.data.toolCallId} started as ${e.data.toolName} after streaming as ${streamed.toolName}`); + } this._resumeSubagentForEvent(e); - if (this._shouldDropUnmappedSubagentEvent(e, 'tool.execution_start')) { + if (!streamed?.started && this._shouldDropUnmappedSubagentEvent(e, 'tool.execution_start')) { this._unroutableSubagentToolCallIds.add(e.data.toolCallId); return; } - const parentToolCallId = this._parentToolCallIdForSubagentEvent(e); - this._activeToolCalls.set(e.data.toolCallId, { toolName: e.data.toolName, displayName, parameters, content: [], parentToolCallId, mcpServerName: e.data.mcpServerName, meta: undefined }); + const parentToolCallId = streamed?.parentToolCallId ?? this._parentToolCallIdForSubagentEvent(e); + const clientToolName = this._clientToolName(e.data.toolName); + const isClientTool = this._clientToolNames.has(clientToolName); + const contributor = this._getToolCallContributor(e.data.toolName, e.data.mcpServerName); + const intention = getShellIntention(e.data.toolName, parameters); + this._activeToolCalls.set(e.data.toolCallId, { + toolName: e.data.toolName, + displayName, + parameters, + content: [], + parentToolCallId, + mcpServerName: e.data.mcpServerName, + contributor, + intention, + meta: undefined, + }); const existingApproval = this._toolApprovalRecords.get(e.data.toolCallId); const approvalRecord = { permissionRequested: existingApproval?.permissionRequested ?? false, @@ -3639,42 +3831,15 @@ export class CopilotAgentSession extends Disposable { this._nonPtyShellTerminals.track(e.data.toolCallId, displayName); } if (isTaskCompleteTool(e.data.toolName)) { - const scope = parentToolCallId ?? ''; - this._currentTurn?.markdownPartIds.delete(scope); - this._currentTurn?.reasoningPartIds.delete(scope); + this._beginToolCallRound(parentToolCallId); return; } - const toolKind = getToolKind(e.data.toolName); - const subagentMeta = toolKind === 'subagent' ? getSubagentMetadata(parameters) : undefined; - let contributor: { readonly kind: ToolCallContributorKind.Client; readonly clientId: string } | { readonly kind: ToolCallContributorKind.MCP; readonly customizationId: string } | undefined; - const clientToolName = this._clientToolName(e.data.toolName); - const isClientTool = this._clientToolNames.has(clientToolName); - const ownerClientId = isClientTool ? this._activeClientToolSet.ownerOf(clientToolName, this._currentTurn?.senderClientId) : undefined; - if (ownerClientId) { - contributor = { kind: ToolCallContributorKind.Client, clientId: ownerClientId }; - } else if (e.data.mcpServerName) { - const customizationId = this._mcpCustomizations.customizationIdForServer(e.data.mcpServerName); - if (customizationId !== undefined) { - contributor = { kind: ToolCallContributorKind.MCP, customizationId }; - } + if (!streamed?.started) { + this._beginToolCallRound(parentToolCallId); } - // A new tool call invalidates the current markdown and reasoning - // parts so the next text/reasoning delta after the tool call - // starts a fresh part. Without invalidating reasoning here, a - // later round of reasoning (after tool_start/tool_complete) - // would silently append to the pre-tool-call reasoning block. - this._currentTurn?.markdownPartIds.delete(parentToolCallId ?? ''); - this._currentTurn?.reasoningPartIds.delete(parentToolCallId ?? ''); - - const meta: Mutable = { toolKind, language: toolKind === 'terminal' ? getShellLanguage(e.data.toolName) : undefined }; - if (subagentMeta?.description) { - meta.subagentDescription = subagentMeta.description; - } - if (subagentMeta?.agentName) { - meta.subagentAgentName = subagentMeta.agentName; - } + const meta = this._createToolCallMeta(e.data.toolName, parameters); if (e.data.mcpServerName) { meta.mcpServerName = e.data.mcpServerName; } @@ -3694,16 +3859,18 @@ export class CopilotAgentSession extends Disposable { tracked.meta = meta; } - this._emitAction({ - type: ActionType.ChatToolCallStart, - turnId: this._turnId, - toolCallId: e.data.toolCallId, - toolName: e.data.toolName, - displayName, - intention: getShellIntention(e.data.toolName, parameters), - contributor, - _meta: toToolCallMeta(meta), - }, parentToolCallId); + if (!streamed?.started) { + this._emitAction({ + type: ActionType.ChatToolCallStart, + turnId: this._turnId, + toolCallId: e.data.toolCallId, + toolName: e.data.toolName, + displayName, + intention, + contributor, + _meta: toToolCallMeta(meta), + }, parentToolCallId); + } // No client is connected to run this client tool. Fail it // immediately instead of leaving it pending until the @@ -3719,9 +3886,12 @@ export class CopilotAgentSession extends Disposable { type: ActionType.ChatToolCallReady, turnId: this._turnId, toolCallId: e.data.toolCallId, - invocationMessage: getInvocationMessage(e.data.toolName, displayName, parameters), + ...(contributor ? { contributor } : {}), + ...(intention !== undefined ? { intention } : {}), + invocationMessage: getInvocationMessage(e.data.toolName, displayName, parameters, path => this._resolveEditFilePath(path)), toolInput: getToolInputString(e.data.toolName, parameters, toolArgs), confirmed: ToolCallConfirmationReason.NotNeeded, + _meta: toToolCallMeta(meta), }, parentToolCallId); this._emitAction({ type: ActionType.ChatToolCallComplete, @@ -3753,10 +3923,12 @@ export class CopilotAgentSession extends Disposable { type: ActionType.ChatToolCallReady, turnId: this._turnId, toolCallId: e.data.toolCallId, - invocationMessage: getInvocationMessage(e.data.toolName, displayName, parameters), + ...(contributor ? { contributor } : {}), + ...(intention !== undefined ? { intention } : {}), + invocationMessage: getInvocationMessage(e.data.toolName, displayName, parameters, path => this._resolveEditFilePath(path)), toolInput: getToolInputString(e.data.toolName, parameters, toolArgs), confirmed: ToolCallConfirmationReason.NotNeeded, - ...(clientToolAutoApproved ? { _meta: toToolCallMeta({ autoApproveBySetting: true }) } : {}), + _meta: toToolCallMeta(clientToolAutoApproved ? { ...meta, autoApproveBySetting: true } : meta), }, parentToolCallId); })); @@ -3852,7 +4024,7 @@ export class CopilotAgentSession extends Disposable { toolCallId: e.data.toolCallId, result: { success: e.data.success, - pastTenseMessage: getPastTenseMessage(tracked.toolName, displayName, tracked.parameters, e.data.success, e.data.success ? toolOutput : undefined), + pastTenseMessage: getPastTenseMessage(tracked.toolName, displayName, tracked.parameters, e.data.success, e.data.success ? toolOutput : undefined, path => this._resolveEditFilePath(path)), content: content.length > 0 ? content : undefined, error: e.data.error, }, diff --git a/src/vs/platform/agentHost/node/copilot/copilotSessionWrapper.ts b/src/vs/platform/agentHost/node/copilot/copilotSessionWrapper.ts index 44701c3dc5d..edece0b24e9 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotSessionWrapper.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotSessionWrapper.ts @@ -43,6 +43,11 @@ export class CopilotSessionWrapper extends Disposable { return this._onMessage ??= this._sdkEvent('assistant.message'); } + private _onToolCallDelta: Event> | undefined; + get onToolCallDelta(): Event> { + return this._onToolCallDelta ??= this._sdkEvent('assistant.tool_call_delta'); + } + private _onToolStart: Event> | undefined; get onToolStart(): Event> { return this._onToolStart ??= this._sdkEvent('tool.execution_start'); diff --git a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts index 4f76a9a1091..ccd83b108ec 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts @@ -11,8 +11,10 @@ import { hash } from '../../../../base/common/hash.js'; import { localize } from '../../../../nls.js'; import type { IAgentToolPendingConfirmationSignal } from '../../common/agentService.js'; import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js'; +import { parsePartialToolInput } from '../../common/partialToolInput.js'; import { StringOrMarkdown } from '../../common/state/protocol/state.js'; import { basename } from '../../../../base/common/resources.js'; +import { getStreamingCreateMessage, getStreamingInsertMessage, getStreamingPatchMessage, getStreamingReplaceMessage, streamingToolTextLineCount, type ToolPathResolver } from '../../common/streamingToolCallDisplay.js'; import { getServerToolDisplay } from '../shared/serverToolGroups.js'; // ============================================================================= @@ -104,6 +106,24 @@ interface ICopilotFileToolArgs { path: string; } +interface ICopilotEditToolArgs extends ICopilotFileToolArgs { + old_str?: string; + new_str?: string; +} + +interface ICopilotCreateToolArgs extends ICopilotFileToolArgs { + file_text?: string; +} + +interface ICopilotInsertToolArgs extends ICopilotFileToolArgs { + insert_line?: number; + new_str?: string; +} + +interface ICopilotStrReplaceEditorToolArgs extends ICopilotEditToolArgs, ICopilotCreateToolArgs, ICopilotInsertToolArgs { + command?: string; +} + /** * Parameters for the `view` tool. The Copilot CLI accepts an optional * `view_range: [startLine, endLine]` (1-based, inclusive). `endLine` may be @@ -521,6 +541,12 @@ function md(value: string): StringOrMarkdown { return { markdown: value }; } +const identityPathResolver: ToolPathResolver = path => path; + +export function parseCopilotStreamingToolInput(raw: string): unknown { + return parsePartialToolInput(raw) ?? raw; +} + export function getToolDisplayName(toolName: string): string { const serverDisplay = getServerToolDisplay(toolName, undefined)?.displayName; if (serverDisplay !== undefined) { @@ -584,7 +610,7 @@ export function getToolDisplayName(toolName: string): string { } } -export function getInvocationMessage(toolName: string, displayName: string, parameters: Record | undefined): StringOrMarkdown { +export function getInvocationMessage(toolName: string, displayName: string, parameters: Record | undefined, resolvePath: ToolPathResolver = identityPathResolver): StringOrMarkdown { const serverDisplay = getServerToolDisplay(toolName, parameters)?.invocationMessage; if (serverDisplay !== undefined) { return serverDisplay; @@ -615,8 +641,8 @@ export function getInvocationMessage(toolName: string, displayName: string, para switch (toolName) { case CopilotToolName.View: { const args = parameters as ICopilotViewToolArgs | undefined; - if (args?.path) { - const link = formatPathAsMarkdownLink(args.path); + if (typeof args?.path === 'string' && args.path) { + const link = formatPathAsMarkdownLink(resolvePath(args.path)); const range = formatViewRange(args.view_range); if (range) { if (range.endLine === -1) { @@ -631,20 +657,43 @@ export function getInvocationMessage(toolName: string, displayName: string, para } return localize('toolInvoke.view', "Reading file"); } - case CopilotToolName.Edit: { + case CopilotToolName.Edit: + case CopilotToolName.StrReplace: { const args = parameters as ICopilotFileToolArgs | undefined; - if (args?.path) { - return md(localize('toolInvoke.editFile', "Editing {0}", formatPathAsMarkdownLink(args.path))); + if (typeof args?.path === 'string' && args.path) { + return md(localize('toolInvoke.editFile', "Editing {0}", formatPathAsMarkdownLink(resolvePath(args.path)))); } return localize('toolInvoke.edit', "Editing file"); } + case CopilotToolName.Insert: { + const args = parameters as ICopilotFileToolArgs | undefined; + if (typeof args?.path === 'string' && args.path) { + return md(localize('toolInvoke.insertFile', "Inserting text in {0}", formatPathAsMarkdownLink(resolvePath(args.path)))); + } + return localize('toolInvoke.insert', "Inserting text"); + } case CopilotToolName.Create: { const args = parameters as ICopilotFileToolArgs | undefined; - if (args?.path) { - return md(localize('toolInvoke.createFile', "Creating {0}", formatPathAsMarkdownLink(args.path))); + if (typeof args?.path === 'string' && args.path) { + return md(localize('toolInvoke.createFile', "Creating {0}", formatPathAsMarkdownLink(resolvePath(args.path)))); } return localize('toolInvoke.create', "Creating file"); } + case CopilotToolName.StrReplaceEditor: { + const command = (parameters as ICopilotStrReplaceEditorToolArgs | undefined)?.command; + switch (command) { + case 'view': + return getInvocationMessage(CopilotToolName.View, displayName, parameters, resolvePath); + case 'create': + return getInvocationMessage(CopilotToolName.Create, displayName, parameters, resolvePath); + case 'insert': + return getInvocationMessage(CopilotToolName.Insert, displayName, parameters, resolvePath); + case 'edit': + case 'str_replace': + default: + return getInvocationMessage(CopilotToolName.Edit, displayName, parameters, resolvePath); + } + } case CopilotToolName.Grep: { const args = parameters as ICopilotGrepToolArgs | undefined; if (args?.pattern) { @@ -668,7 +717,7 @@ export function getInvocationMessage(toolName: string, displayName: string, para } case CopilotToolName.ApplyPatch: case CopilotToolName.GitApplyPatch: { - const files = getEditFilePaths(parameters); + const files = getEditFilePaths(parameters).map(resolvePath); if (files.length === 1) { return md(localize('toolInvoke.patchFile', "Editing {0}", formatPathAsMarkdownLink(files[0]))); } @@ -704,7 +753,55 @@ export function getInvocationMessage(toolName: string, displayName: string, para } } -export function getPastTenseMessage(toolName: string, displayName: string, parameters: Record | undefined, success: boolean, resultText?: string): StringOrMarkdown { +/** + * Returns the progressively refined message shown while Copilot generates tool input. + */ +export function getStreamingInvocationMessage(toolName: string, displayName: string, parameters: unknown, resolvePath: ToolPathResolver = identityPathResolver): StringOrMarkdown { + const objectParameters = parameters !== null && typeof parameters === 'object' && !Array.isArray(parameters) + ? parameters as Record + : undefined; + switch (toolName) { + case CopilotToolName.Edit: + case CopilotToolName.StrReplace: { + const args = objectParameters as ICopilotEditToolArgs | undefined; + return getStreamingReplaceMessage(args?.path, streamingToolTextLineCount(args?.old_str), streamingToolTextLineCount(args?.new_str), resolvePath); + } + case CopilotToolName.Create: { + const args = objectParameters as ICopilotCreateToolArgs | undefined; + return getStreamingCreateMessage(args?.path, streamingToolTextLineCount(args?.file_text), resolvePath); + } + case CopilotToolName.Insert: { + const args = objectParameters as ICopilotInsertToolArgs | undefined; + return getStreamingInsertMessage(args?.path, streamingToolTextLineCount(args?.new_str), resolvePath); + } + case CopilotToolName.StrReplaceEditor: { + const args = objectParameters as ICopilotStrReplaceEditorToolArgs | undefined; + const command = args?.command; + switch (command) { + case 'view': + return getInvocationMessage(CopilotToolName.View, displayName, objectParameters, resolvePath); + case 'create': + return getStreamingCreateMessage(args?.path, streamingToolTextLineCount(args?.file_text), resolvePath); + case 'insert': + return getStreamingInsertMessage(args?.path, streamingToolTextLineCount(args?.new_str), resolvePath); + case 'edit': + case 'str_replace': + default: + return getStreamingReplaceMessage(args?.path, streamingToolTextLineCount(args?.old_str), streamingToolTextLineCount(args?.new_str), resolvePath); + } + } + case CopilotToolName.ApplyPatch: + case CopilotToolName.GitApplyPatch: { + const args = objectParameters as ICopilotApplyPatchToolArgs | undefined; + const patch = typeof parameters === 'string' ? parameters : args?.input ?? args?.patch; + return getStreamingPatchMessage(getEditFilePaths(parameters), streamingToolTextLineCount(patch), resolvePath); + } + default: + return getInvocationMessage(toolName, displayName, objectParameters, resolvePath); + } +} + +export function getPastTenseMessage(toolName: string, displayName: string, parameters: Record | undefined, success: boolean, resultText?: string, resolvePath: ToolPathResolver = identityPathResolver): StringOrMarkdown { if (!success) { return localize('toolComplete.failed', "\"{0}\" failed", displayName); } @@ -739,8 +836,8 @@ export function getPastTenseMessage(toolName: string, displayName: string, param switch (toolName) { case CopilotToolName.View: { const args = parameters as ICopilotViewToolArgs | undefined; - if (args?.path) { - const link = formatPathAsMarkdownLink(args.path); + if (typeof args?.path === 'string' && args.path) { + const link = formatPathAsMarkdownLink(resolvePath(args.path)); const range = formatViewRange(args.view_range); if (range) { if (range.endLine === -1) { @@ -755,20 +852,43 @@ export function getPastTenseMessage(toolName: string, displayName: string, param } return localize('toolComplete.view', "Read file"); } - case CopilotToolName.Edit: { + case CopilotToolName.Edit: + case CopilotToolName.StrReplace: { const args = parameters as ICopilotFileToolArgs | undefined; - if (args?.path) { - return md(localize('toolComplete.editFile', "Edited {0}", formatPathAsMarkdownLink(args.path))); + if (typeof args?.path === 'string' && args.path) { + return md(localize('toolComplete.editFile', "Edited {0}", formatPathAsMarkdownLink(resolvePath(args.path)))); } return localize('toolComplete.edit', "Edited file"); } + case CopilotToolName.Insert: { + const args = parameters as ICopilotFileToolArgs | undefined; + if (typeof args?.path === 'string' && args.path) { + return md(localize('toolComplete.insertFile', "Inserted text in {0}", formatPathAsMarkdownLink(resolvePath(args.path)))); + } + return localize('toolComplete.insert', "Inserted text"); + } case CopilotToolName.Create: { const args = parameters as ICopilotFileToolArgs | undefined; - if (args?.path) { - return md(localize('toolComplete.createFile', "Created {0}", formatPathAsMarkdownLink(args.path))); + if (typeof args?.path === 'string' && args.path) { + return md(localize('toolComplete.createFile', "Created {0}", formatPathAsMarkdownLink(resolvePath(args.path)))); } return localize('toolComplete.create', "Created file"); } + case CopilotToolName.StrReplaceEditor: { + const command = (parameters as ICopilotStrReplaceEditorToolArgs | undefined)?.command; + switch (command) { + case 'view': + return getPastTenseMessage(CopilotToolName.View, displayName, parameters, success, resultText, resolvePath); + case 'create': + return getPastTenseMessage(CopilotToolName.Create, displayName, parameters, success, resultText, resolvePath); + case 'insert': + return getPastTenseMessage(CopilotToolName.Insert, displayName, parameters, success, resultText, resolvePath); + case 'edit': + case 'str_replace': + default: + return getPastTenseMessage(CopilotToolName.Edit, displayName, parameters, success, resultText, resolvePath); + } + } case CopilotToolName.Grep: { const args = parameters as ICopilotGrepToolArgs | undefined; if (args?.pattern) { @@ -792,7 +912,7 @@ export function getPastTenseMessage(toolName: string, displayName: string, param } case CopilotToolName.ApplyPatch: case CopilotToolName.GitApplyPatch: { - const files = getEditFilePaths(parameters); + const files = getEditFilePaths(parameters).map(resolvePath); if (files.length === 1) { return md(localize('toolComplete.patchFile', "Edited {0}", formatPathAsMarkdownLink(files[0]))); } diff --git a/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts b/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts index ce3c1c3a84e..8656e5077df 100644 --- a/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts +++ b/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts @@ -5,7 +5,8 @@ import type { AssistantMessageToolRequest, Attachment, SessionEvent, ToolExecutionCompleteContent, ToolExecutionCompleteData } from '@github/copilot-sdk'; import { decodeBase64 } from '../../../../base/common/buffer.js'; -import { basename } from '../../../../base/common/path.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { basename, isAbsolute, join } from '../../../../base/common/path.js'; import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; @@ -31,6 +32,12 @@ function tryStringify(value: unknown): string | undefined { } } +function resolveToolDisplayPath(path: string, workingDirectory: URI | undefined): string { + return isAbsolute(path) || !workingDirectory || workingDirectory.scheme !== Schemas.file + ? path + : join(workingDirectory.fsPath, path); +} + /** * Returns true if the event is a SDK-injected `user.message` that should not * be shown to the user (e.g. skill-content injection). @@ -210,7 +217,7 @@ function makeToolStartInfo(toolName: string, rawArguments: unknown, parentToolCa return { toolName, displayName, - invocationMessage: getInvocationMessage(toolName, displayName, parameters), + invocationMessage: getInvocationMessage(toolName, displayName, parameters, path => resolveToolDisplayPath(path, workingDirectory)), toolInput: getToolInputString(toolName, parameters, toolArgs), toolKind, language: toolKind === 'terminal' ? getShellLanguage(toolName) : undefined, @@ -590,7 +597,7 @@ export async function mapSessionEvents( // No active turn to attach this completion to. continue; } - const completedPart = makeCompletedToolCallPart(d, info, sessionUriStr, providerId, rawSessionId, storedEdits, subagentInfoByToolCallId.get(d.toolCallId)); + const completedPart = makeCompletedToolCallPart(d, info, sessionUriStr, providerId, rawSessionId, storedEdits, subagentInfoByToolCallId.get(d.toolCallId), workingDirectory); builder.responseParts.push(completedPart); // When a parent tool call that spawned a subagent completes, // flush the subagent's accumulated turn. @@ -681,6 +688,7 @@ export async function mapSessionEvents( rawSessionId, storedEdits, subagentInfoByToolCallId.get(request.toolCallId), + workingDirectory, )); } } @@ -783,6 +791,7 @@ function makeCompletedToolCallPart( rawSessionId: string, storedEdits: Map | undefined, subagent: ISubagentInfo | undefined, + workingDirectory: URI | undefined, ): ResponsePart { const toolOutput = d.error?.message ?? d.result?.content; const content: ToolResultContent[] = []; @@ -848,7 +857,7 @@ function makeCompletedToolCallPart( invocationMessage: info.invocationMessage, toolInput: info.toolInput, success: d.success, - pastTenseMessage: getPastTenseMessage(info.toolName, info.displayName, info.parameters, d.success, d.success ? toolOutput : undefined), + pastTenseMessage: getPastTenseMessage(info.toolName, info.displayName, info.parameters, d.success, d.success ? toolOutput : undefined, path => resolveToolDisplayPath(path, workingDirectory)), content: content.length > 0 ? content : undefined, error: d.error, confirmed: ToolCallConfirmationReason.NotNeeded, diff --git a/src/vs/platform/agentHost/node/sessionPermissions.ts b/src/vs/platform/agentHost/node/sessionPermissions.ts index 84444d9004d..087b16958c4 100644 --- a/src/vs/platform/agentHost/node/sessionPermissions.ts +++ b/src/vs/platform/agentHost/node/sessionPermissions.ts @@ -384,6 +384,8 @@ export class SessionPermissionManager extends Disposable { type: ActionType.ChatToolCallReady, turnId, toolCallId: state.toolCallId, + ...(state.contributor ? { contributor: state.contributor } : {}), + ...(state.intention !== undefined ? { intention: state.intention } : {}), invocationMessage: state.invocationMessage, toolInput: state.toolInput, confirmationTitle: state.confirmationTitle, @@ -405,6 +407,8 @@ export class SessionPermissionManager extends Disposable { type: ActionType.ChatToolCallReady, turnId, toolCallId: state.toolCallId, + ...(state.contributor ? { contributor: state.contributor } : {}), + ...(state.intention !== undefined ? { intention: state.intention } : {}), invocationMessage: state.invocationMessage, toolInput: state.toolInput, confirmed: ToolCallConfirmationReason.NotNeeded, diff --git a/src/vs/platform/agentHost/test/common/partialToolInput.test.ts b/src/vs/platform/agentHost/test/common/partialToolInput.test.ts new file mode 100644 index 00000000000..f2a93cc6cb4 --- /dev/null +++ b/src/vs/platform/agentHost/test/common/partialToolInput.test.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { parsePartialToolInput, parsePartialToolInputForDisplay } from '../../common/partialToolInput.js'; + +suite('PartialToolInput', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('returns useful object fields from incomplete JSON', () => { + assert.deepStrictEqual(parsePartialToolInputForDisplay('{"command":"npm test","description":"Run'), { + command: 'npm test', + description: 'Run', + }); + }); + + test('returns undefined when no object fields are parseable', () => { + assert.deepStrictEqual([ + parsePartialToolInputForDisplay('{"comm'), + parsePartialToolInputForDisplay('custom input'), + parsePartialToolInputForDisplay('["item"]'), + ], [ + undefined, + undefined, + undefined, + ]); + }); + + test('returns a snapshot instead of the cached object', () => { + const raw = '{"command":"npm test"}'; + const first = parsePartialToolInputForDisplay(raw); + assert.ok(first); + first['command'] = 'modified'; + + assert.deepStrictEqual(parsePartialToolInputForDisplay(raw), { + command: 'npm test', + }); + }); + + test('bounds generic display parsing', () => { + const raw = `{"command":"npm test","content":"${'x'.repeat(70 * 1024)}"}`; + const parsed = parsePartialToolInputForDisplay(raw); + assert.deepStrictEqual({ + command: parsed?.['command'], + contentIsTruncated: typeof parsed?.['content'] === 'string' && parsed['content'].length < raw.length, + }, { + command: 'npm test', + contentIsTruncated: true, + }); + }); + + test('supports uncapped provider parsing', () => { + const content = 'x'.repeat(70 * 1024); + assert.strictEqual(parsePartialToolInput(`{"content":"${content}"}`)?.['content'], content); + }); +}); diff --git a/src/vs/platform/agentHost/test/node/agentHostToolCallTelemetry.test.ts b/src/vs/platform/agentHost/test/node/agentHostToolCallTelemetry.test.ts index deec21e2b0e..5c0370a51f2 100644 --- a/src/vs/platform/agentHost/test/node/agentHostToolCallTelemetry.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostToolCallTelemetry.test.ts @@ -17,7 +17,7 @@ import { ITelemetryService, TelemetryLevel } from '../../../telemetry/common/tel import { AgentSession, IAgent } from '../../common/agentService.js'; import { SessionInputRequestKind } from '../../common/state/protocol/state.js'; import { ActionType, type ChatAction } from '../../common/state/sessionActions.js'; -import { buildDefaultChatUri, MessageKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, type ToolCallContributor, type ToolCallResult } from '../../common/state/sessionState.js'; +import { buildDefaultChatUri, MessageKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, type ToolCallContributor, type ToolCallResult } from '../../common/state/sessionState.js'; import { IAgentHostCheckpointService, NULL_CHECKPOINT_SERVICE } from '../../common/agentHostCheckpointService.js'; import { IAgentHostTerminalManager } from '../../node/agentHostTerminalManager.js'; import { AgentHostLocalTurns } from '../../node/agentHostLocalTurns.js'; @@ -138,7 +138,12 @@ suite('AgentSideEffects — tool call telemetry', () => { const data = e.data as Record; return { eventName: e.eventName, - data: { ...data, invocationTimeMs: typeof data.invocationTimeMs === 'number' && data.invocationTimeMs >= 0 }, + data: { + ...data, + invocationTimeMs: data.invocationTimeMs === undefined + ? undefined + : typeof data.invocationTimeMs === 'number' && data.invocationTimeMs >= 0, + }, }; }); } @@ -211,6 +216,13 @@ suite('AgentSideEffects — tool call telemetry', () => { startTurn('turn-1'); toolStart('turn-1', 'tc-1', 'bash'); + fire({ + type: ActionType.ChatToolCallReady, + turnId: 'turn-1', + toolCallId: 'tc-1', + invocationMessage: 'run', + confirmed: ToolCallConfirmationReason.NotNeeded, + }); toolComplete('turn-1', 'tc-1', { success: true, pastTenseMessage: 'ran' }); assert.deepStrictEqual(toolEvents(), [{ @@ -243,7 +255,7 @@ suite('AgentSideEffects — tool call telemetry', () => { toolExtensionId: undefined, toolSourceKind: 'mcp', provider: 'mock', - invocationTimeMs: true, + invocationTimeMs: undefined, }, }]); }); @@ -253,6 +265,13 @@ suite('AgentSideEffects — tool call telemetry', () => { startTurn('turn-1'); toolStart('turn-1', 'tc-client', 'run_tests', { kind: ToolCallContributorKind.Client, clientId: 'client-1' }); + fire({ + type: ActionType.ChatToolCallReady, + turnId: 'turn-1', + toolCallId: 'tc-client', + invocationMessage: 'run tests', + confirmed: ToolCallConfirmationReason.NotNeeded, + }); toolComplete('turn-1', 'tc-client', { success: true, pastTenseMessage: 'ran tests' }); assert.deepStrictEqual(toolEvents(), [{ @@ -269,6 +288,83 @@ suite('AgentSideEffects — tool call telemetry', () => { }]); }); + test('only accepts contributor refinements that preserve execution ownership', async () => { + setupSession(); + startTurn('turn-1'); + + toolStart('turn-1', 'tc-mcp-ready', 'lookup'); + agent.fireProgress({ + kind: 'pending_confirmation', + chat: URI.parse(defaultChatUri), + state: { + status: ToolCallStatus.PendingConfirmation, + toolCallId: 'tc-mcp-ready', + toolName: 'lookup', + displayName: 'Lookup', + contributor: { kind: ToolCallContributorKind.MCP, customizationId: 'mcp-1' }, + invocationMessage: 'Looking up metadata', + toolInput: '{}', + }, + }); + toolStart('turn-1', 'tc-late-client', 'run_tests'); + agent.fireProgress({ + kind: 'pending_confirmation', + chat: URI.parse(defaultChatUri), + state: { + status: ToolCallStatus.PendingConfirmation, + toolCallId: 'tc-late-client', + toolName: 'run_tests', + displayName: 'Run Tests', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'client-1' }, + invocationMessage: 'Running tests', + toolInput: '{}', + }, + }); + await timeout(0); + toolComplete('turn-1', 'tc-mcp-ready', { success: true, pastTenseMessage: 'looked up metadata' }); + toolComplete('turn-1', 'tc-late-client', { success: true, pastTenseMessage: 'ran tests' }); + + assert.deepStrictEqual(toolEvents().map(event => event.data.toolSourceKind), ['mcp', 'agentHost']); + }); + + test('excludes pending confirmation time from invocation timing', async () => { + await runWithFakedTimers({}, async () => { + setupSession(); + startTurn('turn-1'); + toolStart('turn-1', 'tc-confirm-timing', 'write'); + fire({ + type: ActionType.ChatToolCallReady, + turnId: 'turn-1', + toolCallId: 'tc-confirm-timing', + invocationMessage: 'Write file', + confirmationTitle: 'Write file', + }); + await timeout(10_000); + + const confirmed: ChatAction = { + type: ActionType.ChatToolCallConfirmed, + turnId: 'turn-1', + toolCallId: 'tc-confirm-timing', + approved: true, + confirmed: ToolCallConfirmationReason.UserAction, + }; + stateManager.dispatchClientAction(defaultChatUri, confirmed, { clientId: 'test', clientSeq: 2 }); + sideEffects.handleAction(defaultChatUri, confirmed); + await timeout(25); + toolComplete('turn-1', 'tc-confirm-timing', { success: true, pastTenseMessage: 'wrote file' }); + }); + + const event = telemetry.events.find(event => event.eventName === 'languageModelToolInvoked'); + const invocationTimeMs = (event?.data as { invocationTimeMs?: number } | undefined)?.invocationTimeMs; + assert.deepStrictEqual({ + isMeasured: typeof invocationTimeMs === 'number', + excludesConfirmationDelay: typeof invocationTimeMs === 'number' && invocationTimeMs < 1000, + }, { + isMeasured: true, + excludesConfirmationDelay: true, + }); + }); + test('emits error for a failure without a cancellation code', () => { setupSession(); startTurn('turn-1'); diff --git a/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts b/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts index 85991b81bb2..26cb15b9cc1 100644 --- a/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts @@ -10,6 +10,7 @@ import { NullLogService } from '../../../log/common/log.js'; import type { AgentSignal } from '../../common/agentService.js'; import { ActionType } from '../../common/state/sessionActions.js'; import { ResponsePartKind, ToolResultContentType } from '../../common/state/sessionState.js'; +import { STREAMING_TOOL_DISPLAY_INTERVAL_MS } from '../../common/streamingToolCallDisplay.js'; import { ToolCallConfirmationReason, ToolCallContributorKind } from '../../common/state/protocol/state.js'; import { ClaudeMapperState, mapSDKMessageToAgentSignals } from '../../node/claude/claudeMapSessionEvents.js'; import { CLAUDE_USER_DECLINED_MESSAGE } from '../../node/claude/claudeToolDenial.js'; @@ -329,6 +330,209 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { }]); }); + test('file-edit input deltas emit compact rich invocation messages', () => { + const log = new NullLogService(); + let now = 1_000; + const state = new ClaudeMapperState(() => now); + const resolver = r(); + mapSDKMessageToAgentSignals(makeStreamEvent(SESSION_ID, makeContentBlockStartToolUse(0, 'tu_write', 'Write')), SESSION, TURN_ID, state, log, resolver); + + const first = mapSDKMessageToAgentSignals( + makeStreamEvent(SESSION_ID, makeInputJsonDelta(0, '{"file_path":"/src/new.ts","content":"one\\ntwo')), + SESSION, + TURN_ID, + state, + log, + resolver, + ); + now += STREAMING_TOOL_DISPLAY_INTERVAL_MS; + const second = mapSDKMessageToAgentSignals( + makeStreamEvent(SESSION_ID, makeInputJsonDelta(0, '\\nthree\\nfour\\nfive"')), + SESSION, + TURN_ID, + state, + log, + resolver, + ); + + assert.deepStrictEqual([...first, ...second], [ + { + kind: 'action', + resource: SESSION, + action: { + type: ActionType.ChatToolCallDelta, + turnId: TURN_ID, + toolCallId: 'tu_write', + content: '', + invocationMessage: { markdown: 'Creating [new.ts](file:///src/new.ts) (2 lines)' }, + }, + }, + { + kind: 'action', + resource: SESSION, + action: { + type: ActionType.ChatToolCallDelta, + turnId: TURN_ID, + toolCallId: 'tu_write', + content: '', + invocationMessage: { markdown: 'Creating [new.ts](file:///src/new.ts) (5 lines)' }, + }, + }, + ]); + }); + + test('content_block_stop flushes the final rich file-edit message held back by the throttle', () => { + const log = new NullLogService(); + const now = 1_000; + const state = new ClaudeMapperState(() => now); + const resolver = r(); + mapSDKMessageToAgentSignals(makeStreamEvent(SESSION_ID, makeContentBlockStartToolUse(0, 'tu_write', 'Write')), SESSION, TURN_ID, state, log, resolver); + + const first = mapSDKMessageToAgentSignals( + makeStreamEvent(SESSION_ID, makeInputJsonDelta(0, '{"file_path":"/src/new.ts","content":"one')), + SESSION, + TURN_ID, + state, + log, + resolver, + ); + const withinInterval = mapSDKMessageToAgentSignals( + makeStreamEvent(SESSION_ID, makeInputJsonDelta(0, '\\ntwo"}')), + SESSION, + TURN_ID, + state, + log, + resolver, + ); + const stopped = mapSDKMessageToAgentSignals( + makeStreamEvent(SESSION_ID, makeContentBlockStop(0)), + SESSION, + TURN_ID, + state, + log, + resolver, + ); + + assert.deepStrictEqual({ + first: first.map(signal => signal.kind === 'action' ? signal.action : undefined), + withinInterval, + stopped: stopped.map(signal => signal.kind === 'action' ? signal.action : undefined), + }, { + first: [{ + type: ActionType.ChatToolCallDelta, + turnId: TURN_ID, + toolCallId: 'tu_write', + content: '', + invocationMessage: { markdown: 'Creating [new.ts](file:///src/new.ts) (1 line)' }, + }], + withinInterval: [], + stopped: [{ + type: ActionType.ChatToolCallDelta, + turnId: TURN_ID, + toolCallId: 'tu_write', + content: '', + invocationMessage: { markdown: 'Creating [new.ts](file:///src/new.ts) (2 lines)' }, + }, { + type: ActionType.ChatToolCallReady, + turnId: TURN_ID, + toolCallId: 'tu_write', + invocationMessage: { markdown: 'Editing [new.ts](file:///src/new.ts)' }, + toolInput: '{\n "file_path": "/src/new.ts",\n "content": "one\\ntwo"\n}', + confirmed: ToolCallConfirmationReason.NotNeeded, + }], + }); + }); + + test('client tools with Claude built-in names preserve client semantics throughout the lifecycle', () => { + const state = new ClaudeMapperState(); + const resolver = r(); + const start = mapSDKMessageToAgentSignals( + makeStreamEvent(SESSION_ID, makeContentBlockStartToolUse(0, 'tu_client_write', 'mcp__client__Write')), + SESSION, + TURN_ID, + state, + new NullLogService(), + resolver, + () => 'client-1', + ); + + const delta = mapSDKMessageToAgentSignals( + makeStreamEvent(SESSION_ID, makeInputJsonDelta(0, '{"value":"client input"}')), + SESSION, + TURN_ID, + state, + new NullLogService(), + resolver, + ); + const ready = mapSDKMessageToAgentSignals( + makeStreamEvent(SESSION_ID, makeContentBlockStop(0)), + SESSION, + TURN_ID, + state, + new NullLogService(), + resolver, + ); + const complete = mapSDKMessageToAgentSignals( + makeUserToolResultMessage(SESSION_ID, 'tu_client_write', 'done'), + SESSION, + 'turn-2-irrelevant', + state, + new NullLogService(), + resolver, + ); + + assert.deepStrictEqual([...start, ...delta, ...ready, ...complete], [ + { + kind: 'action', + resource: SESSION, + action: { + type: ActionType.ChatToolCallStart, + turnId: TURN_ID, + toolCallId: 'tu_client_write', + toolName: 'Write', + displayName: 'Write', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'client-1' }, + }, + }, + { + kind: 'action', + resource: SESSION, + action: { + type: ActionType.ChatToolCallDelta, + turnId: TURN_ID, + toolCallId: 'tu_client_write', + content: '{"value":"client input"}', + }, + }, + { + kind: 'action', + resource: SESSION, + action: { + type: ActionType.ChatToolCallReady, + turnId: TURN_ID, + toolCallId: 'tu_client_write', + invocationMessage: 'Write', + toolInput: '{\n "value": "client input"\n}', + confirmed: ToolCallConfirmationReason.NotNeeded, + }, + }, + { + kind: 'action', + resource: SESSION, + action: { + type: ActionType.ChatToolCallComplete, + turnId: TURN_ID, + toolCallId: 'tu_client_write', + result: { + success: true, + pastTenseMessage: 'Write', + content: [{ type: ToolResultContentType.Text, text: 'done' }], + }, + }, + }, + ]); + }); + test('Test 9.5 — content_block_stop emits ChatToolCallReady so auto-allowed tools leave Streaming', () => { const log = new CapturingLogService(); const state = new ClaudeMapperState(); diff --git a/src/vs/platform/agentHost/test/node/claudeReplayMapper.test.ts b/src/vs/platform/agentHost/test/node/claudeReplayMapper.test.ts index f373bec6996..3cb068acebf 100644 --- a/src/vs/platform/agentHost/test/node/claudeReplayMapper.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeReplayMapper.test.ts @@ -166,6 +166,51 @@ suite('claudeReplayMapper', () => { } }); + test('replay preserves generic semantics for client tools that collide with built-in names', () => { + const messages: SessionMessage[] = [ + makeUser('u1', 'run client tools'), + makeAssistantToolUse('a1', 'tu_bash', 'mcp__client__Bash', { command: 'echo client' }), + makeUserToolResult('r1', 'tu_bash', 'done'), + makeAssistantToolUse('a2', 'tu_task', 'mcp__client__Task', { description: 'client task' }), + makeUserToolResult('r2', 'tu_task', 'done'), + ]; + + const turns = mapSessionMessagesToTurns(messages, session, logService); + const tools = turns[0].responseParts.filter(part => part.kind === ResponsePartKind.ToolCall).map(part => { + assert.strictEqual(part.kind, ResponsePartKind.ToolCall); + return { + toolName: part.toolCall.toolName, + displayName: part.toolCall.displayName, + meta: part.toolCall._meta, + invocationMessage: part.toolCall.status === ToolCallStatus.Completed ? part.toolCall.invocationMessage : undefined, + toolInput: part.toolCall.status === ToolCallStatus.Completed ? part.toolCall.toolInput : undefined, + pastTenseMessage: part.toolCall.status === ToolCallStatus.Completed ? part.toolCall.pastTenseMessage : undefined, + hasSubagentContent: part.toolCall.status === ToolCallStatus.Completed + && part.toolCall.content?.some(content => content.type === ToolResultContentType.Subagent), + }; + }); + assert.deepStrictEqual(tools, [ + { + toolName: 'Bash', + displayName: 'Bash', + meta: undefined, + invocationMessage: 'Bash', + toolInput: '{\n "command": "echo client"\n}', + pastTenseMessage: 'Bash', + hasSubagentContent: false, + }, + { + toolName: 'Task', + displayName: 'Task', + meta: undefined, + invocationMessage: 'Task', + toolInput: '{\n "description": "client task"\n}', + pastTenseMessage: 'Task', + hasSubagentContent: false, + }, + ]); + }); + test('Fixture 3: multi-turn produces ordered Turns', () => { const messages: SessionMessage[] = [ makeUser('u1', 'first'), diff --git a/src/vs/platform/agentHost/test/node/claudeSubagentSignals.test.ts b/src/vs/platform/agentHost/test/node/claudeSubagentSignals.test.ts index 833a83a30bf..617bd1e39cd 100644 --- a/src/vs/platform/agentHost/test/node/claudeSubagentSignals.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeSubagentSignals.test.ts @@ -9,7 +9,7 @@ import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { NullLogService } from '../../../log/common/log.js'; import { ActionType } from '../../common/state/sessionActions.js'; -import { ToolCallConfirmationReason } from '../../common/state/sessionState.js'; +import { ToolCallConfirmationReason, ToolCallContributorKind } from '../../common/state/sessionState.js'; import { ClaudeMapperState, mapSDKMessageToAgentSignals } from '../../node/claude/claudeMapSessionEvents.js'; import { SubagentRegistry } from '../../node/claude/claudeSubagentRegistry.js'; import { buildTopLevelSubagentReadyAction, mapSubagentSystemMessage } from '../../node/claude/claudeSubagentSignals.js'; @@ -253,6 +253,70 @@ suite('claudeSubagentSignals — Phase 12 emission', () => { }); }); + test('inner client tools preserve client ownership and generic input across the lifecycle', () => { + const state = new ClaudeMapperState(); + const log = new NullLogService(); + const registry = r(); + const parentToolCallId = 'toolu_parent_client'; + mapSDKMessageToAgentSignals( + makeStreamEvent(SESSION_ID, makeContentBlockStartToolUse(0, parentToolCallId, 'Task')), + SESSION, TURN_ID, state, log, registry, + ); + + const innerAssistant = makeAssistantMessage(SESSION_ID, [ + { type: 'tool_use', id: 'toolu_inner_client', name: 'mcp__client__Bash', input: { command: 'echo client' } }, + ]); + innerAssistant.parent_tool_use_id = parentToolCallId; + const fromAssistant = mapSDKMessageToAgentSignals(innerAssistant, SESSION, TURN_ID, state, log, registry, () => 'client-1'); + const innerToolResult = makeUserToolResultMessage(SESSION_ID, 'toolu_inner_client', 'done'); + innerToolResult.parent_tool_use_id = parentToolCallId; + const fromResult = mapSDKMessageToAgentSignals(innerToolResult, SESSION, TURN_ID, state, log, registry); + + const actions = [...fromAssistant, ...fromResult].filter(signal => signal.kind === 'action').map(signal => signal.kind === 'action' ? signal.action : undefined); + assert.deepStrictEqual(actions.map(action => { + switch (action?.type) { + case ActionType.ChatToolCallStart: + return { + type: action.type, + toolName: action.toolName, + displayName: action.displayName, + contributor: action.contributor, + meta: action._meta, + }; + case ActionType.ChatToolCallReady: + return { + type: action.type, + invocationMessage: action.invocationMessage, + toolInput: action.toolInput, + }; + case ActionType.ChatToolCallComplete: + return { + type: action.type, + pastTenseMessage: action.result.pastTenseMessage, + }; + default: + return undefined; + } + }).filter(item => item !== undefined), [ + { + type: ActionType.ChatToolCallStart, + toolName: 'Bash', + displayName: 'Bash', + contributor: { kind: ToolCallContributorKind.Client, clientId: 'client-1' }, + meta: undefined, + }, + { + type: ActionType.ChatToolCallReady, + invocationMessage: 'Bash', + toolInput: '{\n "command": "echo client"\n}', + }, + { + type: ActionType.ChatToolCallComplete, + pastTenseMessage: 'Bash', + }, + ]); + }); + test('foreground subagent completion: tool_result for a Task spawn emits ChatToolCallComplete AND IAgentSubagentCompletedSignal, then clears the spawn from the registry', () => { const state = new ClaudeMapperState(); const log = new NullLogService(); diff --git a/src/vs/platform/agentHost/test/node/claudeToolDisplay.test.ts b/src/vs/platform/agentHost/test/node/claudeToolDisplay.test.ts index ad5ae745810..717c0149f7e 100644 --- a/src/vs/platform/agentHost/test/node/claudeToolDisplay.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeToolDisplay.test.ts @@ -10,6 +10,7 @@ import { getClaudeInvocationMessage, getClaudePastTenseMessage, getClaudePermissionKind, + getClaudeStreamingInvocationMessage, getClaudeToolDisplayName, getClaudeToolInputString, getClaudeToolKind, @@ -187,6 +188,38 @@ suite('claudeToolDisplay — §4 mapping table', () => { ); }); + test('streams rich file and line-count messages for Claude edit tools', () => { + assert.deepStrictEqual({ + write: getClaudeStreamingInvocationMessage('Write', { + file_path: '/src/new.ts', + content: 'one\r\ntwo\r\nthree', + }), + edit: getClaudeStreamingInvocationMessage('Edit', { + file_path: '/src/foo.ts', + old_string: 'one', + new_string: 'one\ntwo', + }), + multiEdit: getClaudeStreamingInvocationMessage('MultiEdit', { + file_path: '/src/foo.ts', + edits: [ + { old_string: 'one', new_string: 'one\ntwo' }, + { old_string: 'three\nfour', new_string: 'updated' }, + ], + }), + notebookEdit: getClaudeStreamingInvocationMessage('NotebookEdit', { + notebook_path: '/src/notebook.ipynb', + new_source: 'one\ntwo', + }), + read: getClaudeStreamingInvocationMessage('Read', { file_path: '/src/foo.ts' }), + }, { + write: { markdown: 'Creating [new.ts](file:///src/new.ts) (3 lines)' }, + edit: { markdown: 'Replacing 1 line with 2 lines in [foo.ts](file:///src/foo.ts)' }, + multiEdit: { markdown: 'Replacing 3 lines with 3 lines in [foo.ts](file:///src/foo.ts)' }, + notebookEdit: { markdown: 'Editing 2 lines in [notebook.ipynb](file:///src/notebook.ipynb)' }, + read: undefined, + }); + }); + test('Phase 8.5 — rich rendering snapshot covers every tool row', () => { const SAMPLE_INPUT: Record = { Bash: { command: 'git status' }, diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index 91a78a88a60..2468c66acf6 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -29,9 +29,10 @@ import { AgentFeedbackAttachmentDisplayKind } from '../../common/meta/agentFeedb import { readToolCallMeta } from '../../common/meta/agentToolCallMeta.js'; import { IDiffComputeService } from '../../common/diffComputeService.js'; import { ISessionDataService, type ISessionDatabase } from '../../common/sessionDataService.js'; -import { ActionType, type ChatDeltaAction, type ChatErrorAction, type ChatInputRequestedAction, type ChatResponsePartAction, type ChatToolCallCompleteAction, type ChatToolCallReadyAction, type ChatToolCallStartAction, type ChatTurnCompleteAction, type ChatUsageAction, type SessionAction, type StateAction } from '../../common/state/sessionActions.js'; +import { ActionType, type ChatDeltaAction, type ChatErrorAction, type ChatInputRequestedAction, type ChatResponsePartAction, type ChatToolCallCompleteAction, type ChatToolCallDeltaAction, type ChatToolCallReadyAction, type ChatToolCallStartAction, type ChatTurnCompleteAction, type ChatUsageAction, type SessionAction, type StateAction } from '../../common/state/sessionActions.js'; import { MessageAttachmentKind, MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildChatUri, buildDefaultChatUri, createSessionState, mergeSessionWithDefaultChat, readSessionPromptCacheState, readUsageInfoMeta, SessionStatus, type ToolDefinition, type ToolResultContent, type ToolResultFileEditContent, type ToolResultTerminalContent, type UsageInfoMeta } from '../../common/state/sessionState.js'; import { TerminalClaimKind } from '../../common/state/protocol/state.js'; +import { STREAMING_TOOL_DISPLAY_INTERVAL_MS } from '../../common/streamingToolCallDisplay.js'; import { CustomizationType, McpAuthRequiredReason, McpServerStatus, type Customization } from '../../common/state/protocol/channels-session/state.js'; import { CopilotAgentSession } from '../../node/copilot/copilotAgentSession.js'; import { buildNonPtyShellTerminalUri } from '../../node/copilot/copilotNonPtyShellTerminals.js'; @@ -3860,6 +3861,190 @@ suite('CopilotAgentSession', () => { } }); + test('tool call deltas start once, accumulate buffered input, and finalize at tool start', async () => { + const { mockSession, signals } = await createAgentSession(disposables); + mockSession.fire('assistant.tool_call_delta', { + toolCallId: 'tc-stream', + inputDelta: '{"command":"npm ', + }); + assert.strictEqual(signals.length, 0); + + mockSession.fire('assistant.tool_call_delta', { + toolCallId: 'tc-stream', + toolName: 'bash', + inputDelta: 'test","description":"Run', + }); + await timeout(STREAMING_TOOL_DISPLAY_INTERVAL_MS + 10); + mockSession.fire('tool.execution_start', { + toolCallId: 'tc-stream', + toolName: 'bash', + arguments: { command: 'npm test', description: 'Run all tests' }, + } as SessionEventPayload<'tool.execution_start'>['data']); + + const actions = getActions(signals); + const starts = actions.filter(action => action.type === ActionType.ChatToolCallStart) as ChatToolCallStartAction[]; + const deltas = actions.filter(action => action.type === ActionType.ChatToolCallDelta) as ChatToolCallDeltaAction[]; + const ready = actions.find(action => action.type === ActionType.ChatToolCallReady) as ChatToolCallReadyAction | undefined; + assert.deepStrictEqual({ + starts: starts.map(action => ({ toolCallId: action.toolCallId, toolName: action.toolName })), + deltas: deltas.map(action => ({ + content: action.content, + hasInvocationMessage: action.invocationMessage !== undefined, + })), + ready: ready && { toolCallId: ready.toolCallId, toolInput: ready.toolInput, intention: ready.intention }, + }, { + starts: [{ toolCallId: 'tc-stream', toolName: 'bash' }], + deltas: [{ content: '', hasInvocationMessage: true }], + ready: { toolCallId: 'tc-stream', toolInput: 'npm test', intention: 'Run all tests' }, + }); + }); + + test('edit tool deltas progressively refine file and line-count details', async () => { + const { mockSession, signals } = await createAgentSession(disposables); + mockSession.fire('assistant.tool_call_delta', { + toolCallId: 'tc-edit-stream', + toolName: 'edit', + inputDelta: '{"path":"/repo/file.ts","old_str":"one\\ntwo"', + }); + mockSession.fire('assistant.tool_call_delta', { + toolCallId: 'tc-edit-stream', + toolName: 'edit', + inputDelta: ',"new_str":"one\\nupdated\\nthree"', + }); + await timeout(STREAMING_TOOL_DISPLAY_INTERVAL_MS + 10); + mockSession.fire('tool.execution_start', { + toolCallId: 'tc-edit-stream', + toolName: 'edit', + arguments: { + path: '/repo/file.ts', + old_str: 'one\ntwo', + new_str: 'one\nupdated\nthree', + }, + } as SessionEventPayload<'tool.execution_start'>['data']); + + const actions = getActions(signals); + const deltas = actions.filter(action => action.type === ActionType.ChatToolCallDelta) as ChatToolCallDeltaAction[]; + const ready = actions.find(action => action.type === ActionType.ChatToolCallReady) as ChatToolCallReadyAction | undefined; + assert.deepStrictEqual({ + deltas: deltas.flatMap(action => { + const message = action.invocationMessage; + const text = typeof message === 'string' ? message : message?.markdown; + return text ? [text] : []; + }), + ready: typeof ready?.invocationMessage === 'string' ? ready.invocationMessage : ready?.invocationMessage.markdown, + }, { + deltas: [ + 'Replacing 2 lines in [file.ts](file:///repo/file.ts)', + 'Replacing 2 lines with 3 lines in [file.ts](file:///repo/file.ts)', + ], + ready: 'Editing [file.ts](file:///repo/file.ts)', + }); + }); + + test('raw apply_patch deltas stream line counts and resolved files', async () => { + const { mockSession, signals } = await createAgentSession(disposables, { + workingDirectory: URI.file('/workspace'), + }); + mockSession.fire('assistant.tool_call_delta', { + toolCallId: 'tc-patch-stream', + toolName: 'apply_patch', + inputDelta: [ + '*** Begin Patch', + '*** Update File: src/file.ts', + '@@', + '-old', + '+new', + '*** End Patch', + ].join('\n'), + }); + await timeout(STREAMING_TOOL_DISPLAY_INTERVAL_MS + 10); + + const delta = getActions(signals).find(action => action.type === ActionType.ChatToolCallDelta) as ChatToolCallDeltaAction | undefined; + const message = delta?.invocationMessage; + assert.strictEqual( + typeof message === 'string' ? message : message?.markdown, + 'Generating patch (6 lines) in [file.ts](file:///workspace/src/file.ts)', + ); + }); + + test('MCP tool deltas stream before final contributor metadata arrives', async () => { + const { mockSession, signals } = await createAgentSession(disposables, { + configureMockSession: mock => { + mock.mcpListResult = { servers: [{ name: 'docs', status: 'connected' }] }; + }, + }); + mockSession.fire('session.mcp_server_status_changed', { + serverName: 'docs', + status: 'connected', + } as SessionEventPayload<'session.mcp_server_status_changed'>['data']); + mockSession.fire('assistant.tool_call_delta', { + toolCallId: 'tc-stream-mcp', + toolName: 'mcp_tool', + inputDelta: '{"topic":"metadata"}', + }); + await timeout(STREAMING_TOOL_DISPLAY_INTERVAL_MS + 10); + mockSession.fire('tool.execution_start', { + toolCallId: 'tc-stream-mcp', + toolName: 'mcp_tool', + mcpServerName: 'docs', + arguments: { topic: 'metadata' }, + } as SessionEventPayload<'tool.execution_start'>['data']); + + const actions = getActions(signals); + const starts = actions.filter(action => action.type === ActionType.ChatToolCallStart) as ChatToolCallStartAction[]; + const deltas = actions.filter(action => action.type === ActionType.ChatToolCallDelta) as ChatToolCallDeltaAction[]; + const ready = actions.find(action => action.type === ActionType.ChatToolCallReady) as ChatToolCallReadyAction | undefined; + assert.deepStrictEqual({ + startCount: starts.length, + startContributor: starts[0]?.contributor, + deltas: deltas.map(action => ({ + content: action.content, + hasInvocationMessage: action.invocationMessage !== undefined, + })), + readyContributor: ready?.contributor, + }, { + startCount: 1, + startContributor: undefined, + deltas: [{ content: '', hasInvocationMessage: true }], + readyContributor: { + kind: ToolCallContributorKind.MCP, + customizationId: 'mcp-top-level:copilot:test-session-1:docs', + }, + }); + }); + + test('full assistant message does not duplicate markdown emitted before a tool delta', async () => { + const { session, mockSession, signals } = await createAgentSession(disposables); + session.resetTurnState('turn-stream-dedup'); + mockSession.fire('assistant.message_delta', { + deltaContent: 'I will inspect the file.', + } as SessionEventPayload<'assistant.message_delta'>['data']); + mockSession.fire('assistant.tool_call_delta', { + toolCallId: 'tc-dedup', + toolName: 'view', + inputDelta: '{"path":"/workspace/file.ts"}', + }); + mockSession.fire('assistant.message', { + messageId: 'msg-dedup', + content: 'I will inspect the file.', + toolRequests: [{ + toolCallId: 'tc-dedup', + name: 'view', + arguments: { path: '/workspace/file.ts' }, + type: 'function', + }], + } as SessionEventPayload<'assistant.message'>['data']); + + const markdownParts = getActions(signals).flatMap(action => + action.type === ActionType.ChatResponsePart && action.part.kind === ResponsePartKind.Markdown + ? [{ kind: action.part.kind, content: action.part.content }] + : []); + assert.deepStrictEqual(markdownParts, [{ + kind: ResponsePartKind.Markdown, + content: 'I will inspect the file.', + }]); + }); + test('tool_start carries MCP App UI metadata from the SDK', async () => { const { mockSession, signals } = await createAgentSession(disposables); mockSession.fire('tool.execution_start', { diff --git a/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts b/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts index a2857050e6d..9ee86e2b160 100644 --- a/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotToolDisplay.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 { getEditFilePath, getEditFilePaths, getInvocationMessage, getPastTenseMessage, getPermissionDisplay, getShellIntention, getShellLanguage, getToolDisplayName, getToolInputString, getToolKind, getToolMarkdownContent, isEditTool, isHiddenTool, isMarkdownRenderedTool, synthesizeSkillToolCall, type ITypedPermissionRequest } from '../../node/copilot/copilotToolDisplay.js'; +import { getEditFilePath, getEditFilePaths, getInvocationMessage, getPastTenseMessage, getPermissionDisplay, getShellIntention, getShellLanguage, getStreamingInvocationMessage, getToolDisplayName, getToolInputString, getToolKind, getToolMarkdownContent, isEditTool, isHiddenTool, isMarkdownRenderedTool, synthesizeSkillToolCall, type ITypedPermissionRequest } from '../../node/copilot/copilotToolDisplay.js'; suite('copilotToolDisplay — friendly tool names', () => { @@ -385,6 +385,102 @@ suite('copilotToolDisplay — built-in tool invocation/past-tense messages', () }); }); +suite('copilotToolDisplay — streaming edit messages', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + function streaming(toolName: string, parameters: unknown, resolvePath?: (path: string) => string): string { + const result = getStreamingInvocationMessage(toolName, getToolDisplayName(toolName), parameters, resolvePath); + return typeof result === 'string' ? result : result.markdown; + } + + function invocation(toolName: string, parameters: Record): string { + const result = getInvocationMessage(toolName, getToolDisplayName(toolName), parameters); + return typeof result === 'string' ? result : result.markdown; + } + + function completed(toolName: string, parameters: Record): string { + const result = getPastTenseMessage(toolName, getToolDisplayName(toolName), parameters, true); + return typeof result === 'string' ? result : result.markdown; + } + + test('streams replacement line counts and the target file', () => { + assert.deepStrictEqual([ + streaming('edit', { path: '/repo/file.ts' }), + streaming('edit', { path: '/repo/file.ts', old_str: 'one\ntwo' }), + streaming('edit', { path: '/repo/file.ts', old_str: 'one\ntwo', new_str: 'one\nupdated\nthree' }), + ], [ + 'Editing [file.ts](file:///repo/file.ts)', + 'Replacing 2 lines in [file.ts](file:///repo/file.ts)', + 'Replacing 2 lines with 3 lines in [file.ts](file:///repo/file.ts)', + ]); + }); + + test('streams create and insert line counts', () => { + assert.deepStrictEqual([ + streaming('create', { path: '/repo/new.ts', file_text: 'one\r\ntwo\r\nthree' }), + streaming('insert', { path: '/repo/file.ts', new_str: 'one\rtwo' }), + ], [ + 'Creating [new.ts](file:///repo/new.ts) (3 lines)', + 'Inserting 2 lines in [file.ts](file:///repo/file.ts)', + ]); + }); + + test('uses the str_replace_editor command shape', () => { + assert.deepStrictEqual([ + streaming('str_replace_editor', { command: 'create', path: '/repo/new.ts', file_text: 'one\ntwo' }), + streaming('str_replace_editor', { command: 'str_replace', path: '/repo/file.ts', old_str: 'old', new_str: 'new\nvalue' }), + streaming('str_replace_editor', { command: 'view', path: '/repo/file.ts' }), + ], [ + 'Creating [new.ts](file:///repo/new.ts) (2 lines)', + 'Replacing 1 line with 2 lines in [file.ts](file:///repo/file.ts)', + 'Reading [file.ts](file:///repo/file.ts)', + ]); + }); + + test('preserves file context after streaming aliases become ready and complete', () => { + const cases: Array<[toolName: string, parameters: Record, ready: string, complete: string]> = [ + ['str_replace', { path: '/repo/file.ts' }, 'Editing [file.ts](file:///repo/file.ts)', 'Edited [file.ts](file:///repo/file.ts)'], + ['insert', { path: '/repo/file.ts' }, 'Inserting text in [file.ts](file:///repo/file.ts)', 'Inserted text in [file.ts](file:///repo/file.ts)'], + ['str_replace_editor', { command: 'create', path: '/repo/new.ts' }, 'Creating [new.ts](file:///repo/new.ts)', 'Created [new.ts](file:///repo/new.ts)'], + ['str_replace_editor', { command: 'str_replace', path: '/repo/file.ts' }, 'Editing [file.ts](file:///repo/file.ts)', 'Edited [file.ts](file:///repo/file.ts)'], + ]; + assert.deepStrictEqual(cases.map(([toolName, parameters]) => ({ + ready: invocation(toolName, parameters), + complete: completed(toolName, parameters), + })), cases.map(([, , ready, complete]) => ({ ready, complete }))); + }); + + test('streams raw patch line counts and resolves discovered file paths', () => { + const patch = [ + '*** Begin Patch', + '*** Update File: src/file.ts', + '@@', + '-old', + '+new', + '*** End Patch', + ].join('\n'); + assert.strictEqual( + streaming('apply_patch', patch, path => `/workspace/${path}`), + 'Generating patch (6 lines) in [file.ts](file:///workspace/src/file.ts)', + ); + }); + + test('ignores malformed partial paths', () => { + assert.strictEqual( + streaming('edit', { path: 42, old_str: 'one' }), + 'Replacing 1 line', + ); + }); + + test('falls back to the normal invocation formatter for non-edit tools', () => { + assert.strictEqual( + streaming('bash', { command: 'npm test' }), + 'Running `npm test`', + ); + }); +}); + // ---- write_/read_ shell tool display --------------------------------------- // // Coverage for the secondary shell helpers (write_bash, read_bash, and their diff --git a/src/vs/platform/agentHost/test/node/e2e/captures/claude-streams-rich-file-creation-progress-without-exposing-partial-input.yaml b/src/vs/platform/agentHost/test/node/e2e/captures/claude-streams-rich-file-creation-progress-without-exposing-partial-input.yaml new file mode 100644 index 00000000000..30ced22d7cc --- /dev/null +++ b/src/vs/platform/agentHost/test/node/e2e/captures/claude-streams-rich-file-creation-progress-without-exposing-partial-input.yaml @@ -0,0 +1,59 @@ +version: 1 +dialect: anthropic +exchanges: + - request: + model: claude-opus-4.8 + system: ${system} + messages: + - role: user + content: |- + Create streaming.txt containing exactly these three lines, with no other content: + STREAM_ALPHA + STREAM_BETA + STREAM_GAMMA + Use your file creation tool; do not run a shell command. Then reply exactly "done". + response: + content: + - type: text + text: I'll create the file. + - type: tool_use + id: toolcall_0 + name: Write + input: + file_path: ${workdir}/streaming.txt + content: | + STREAM_ALPHA + STREAM_BETA + STREAM_GAMMA + stopReason: tool_use + - request: + model: claude-opus-4.8 + system: ${system} + messages: + - role: user + content: |- + Create streaming.txt containing exactly these three lines, with no other content: + STREAM_ALPHA + STREAM_BETA + STREAM_GAMMA + Use your file creation tool; do not run a shell command. Then reply exactly "done". + - role: assistant + content: + - type: text + text: I'll create the file. + - type: tool_use + name: Write + input: + file_path: ${workdir}/streaming.txt + content: | + STREAM_ALPHA + STREAM_BETA + STREAM_GAMMA + - role: user + content: + - type: tool_result + tool_use_id: toolcall_0 + content: 'File created successfully at: ${workdir}/streaming.txt (file state is current in your context — no need to Read it back)' + response: + content: done + stopReason: end_turn diff --git a/src/vs/platform/agentHost/test/node/e2e/captures/copilotcli-streams-rich-file-creation-progress-without-exposing-partial-input.yaml b/src/vs/platform/agentHost/test/node/e2e/captures/copilotcli-streams-rich-file-creation-progress-without-exposing-partial-input.yaml new file mode 100644 index 00000000000..394bd041c8e --- /dev/null +++ b/src/vs/platform/agentHost/test/node/e2e/captures/copilotcli-streams-rich-file-creation-progress-without-exposing-partial-input.yaml @@ -0,0 +1,55 @@ +version: 1 +dialect: anthropic +exchanges: + - request: + model: claude-sonnet-5 + system: ${system} + messages: + - role: user + content: |- + Create streaming.txt containing exactly these three lines, with no other content: + STREAM_ALPHA + STREAM_BETA + STREAM_GAMMA + Use your file creation tool; do not run a shell command. Then reply exactly "done". + response: + content: + - type: tool_use + id: toolcall_0 + name: create + input: + path: ${workdir}/streaming.txt + file_text: | + STREAM_ALPHA + STREAM_BETA + STREAM_GAMMA + stopReason: tool_use + - request: + model: claude-sonnet-5 + system: ${system} + messages: + - role: user + content: |- + Create streaming.txt containing exactly these three lines, with no other content: + STREAM_ALPHA + STREAM_BETA + STREAM_GAMMA + Use your file creation tool; do not run a shell command. Then reply exactly "done". + - role: assistant + content: + - type: tool_use + name: create + input: + path: ${workdir}/streaming.txt + file_text: | + STREAM_ALPHA + STREAM_BETA + STREAM_GAMMA + - role: user + content: + - type: tool_result + tool_use_id: toolcall_0 + content: Created file ${workdir}/streaming.txt with 38 characters + response: + content: done + stopReason: end_turn diff --git a/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts b/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts index 5acbcc4a322..fc9fbc895a1 100644 --- a/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts +++ b/src/vs/platform/agentHost/test/node/e2e/harness/agentHostE2ETestHarness.ts @@ -268,6 +268,8 @@ export interface IAgentHostE2EProviderConfig { * plan mode. (`exit_plan_mode` for Copilot, `ExitPlanMode` for Claude.) */ readonly exitPlanModeToolName: string; + /** File-creation tool that exposes model-generated argument deltas, when supported. */ + readonly streamingFileCreateToolName?: string; /** * Whether the suite should be enabled. Returning false skips the suite * entirely (mirrors `suite.skip(...)`). diff --git a/src/vs/platform/agentHost/test/node/e2e/providers/claudeAgentHostE2E.integrationTest.ts b/src/vs/platform/agentHost/test/node/e2e/providers/claudeAgentHostE2E.integrationTest.ts index b767dea2572..1ac1564dc6a 100644 --- a/src/vs/platform/agentHost/test/node/e2e/providers/claudeAgentHostE2E.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/e2e/providers/claudeAgentHostE2E.integrationTest.ts @@ -61,6 +61,7 @@ const CLAUDE_CONFIG: IAgentHostE2EProviderConfig = { shellToolName: 'Bash', subagentToolNames: ['Task', 'Agent'], exitPlanModeToolName: 'ExitPlanMode', + streamingFileCreateToolName: 'Write', enabled: !!CLAUDE_SDK_ROOT, claudeSdkRoot: CLAUDE_SDK_ROOT, // Worktree isolation is now shared across agents via the host-owned diff --git a/src/vs/platform/agentHost/test/node/e2e/providers/copilotAgentHostE2E.integrationTest.ts b/src/vs/platform/agentHost/test/node/e2e/providers/copilotAgentHostE2E.integrationTest.ts index b8704b66dd4..7864133e0ea 100644 --- a/src/vs/platform/agentHost/test/node/e2e/providers/copilotAgentHostE2E.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/e2e/providers/copilotAgentHostE2E.integrationTest.ts @@ -29,8 +29,8 @@ import { mkdtemp, rm, writeFile } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from '../../../../../../base/common/path.js'; import { URI } from '../../../../../../base/common/uri.js'; -import { MessageAttachmentKind, MessageKind, PendingMessageKind, ToolCallConfirmationReason, buildDefaultChatUri, type MessageAttachment } from '../../../../common/state/sessionState.js'; -import { ActionType, type ChatUsageAction } from '../../../../common/state/sessionActions.js'; +import { MessageAttachmentKind, MessageKind, PendingMessageKind, ToolCallConfirmationReason, ToolCallContributorKind, buildDefaultChatUri, type MessageAttachment } from '../../../../common/state/sessionState.js'; +import { ActionType, type ChatToolCallDeltaAction, type ChatToolCallReadyAction, type ChatToolCallStartAction, type ChatUsageAction } from '../../../../common/state/sessionActions.js'; import { AgentHostE2EServerLease, createRealSession, dispatchTurn, driveTurnWithAttachmentsToCompletion, runAhpSnapshotTest, type IAgentHostE2EProviderConfig, @@ -45,6 +45,7 @@ const COPILOT_CONFIG: IAgentHostE2EProviderConfig = { shellToolName: 'bash', subagentToolNames: ['task'], exitPlanModeToolName: 'exit_plan_mode', + streamingFileCreateToolName: 'create', // The shared suite runs by default in deterministic replay mode (tokenless, // against committed fixtures). Recording new fixtures is opt-in via // `AGENT_HOST_REPLAY_RECORD=1`. The Copilot CLI is always present (dev dep). @@ -106,6 +107,27 @@ suite('Agent Host E2E — Copilot (Copilot-specific)', function () { test('client tool reaches ready after start and completes', async function () { this.timeout(180_000); await runAhpSnapshotTest(client, COPILOT_CONFIG, this.test!, createdSessions, tempDirs); + + const start = client.receivedNotifications(n => isActionNotification(n, 'chat/toolCallStart')) + .map(n => getActionEnvelope(n).action as ChatToolCallStartAction) + .find(action => action.toolName === 'get_magic_word'); + const ready = start && client.receivedNotifications(n => isActionNotification(n, 'chat/toolCallReady')) + .map(n => getActionEnvelope(n).action as ChatToolCallReadyAction) + .find(action => action.toolCallId === start.toolCallId); + const deltas = start && client.receivedNotifications(n => isActionNotification(n, 'chat/toolCallDelta')) + .map(n => getActionEnvelope(n).action as ChatToolCallDeltaAction) + .filter(action => action.toolCallId === start.toolCallId); + + // The AHP snapshot projects contributor metadata only on Start, so Ready ownership needs an explicit assertion. + assert.deepStrictEqual({ + startContributor: start?.contributor, + readyContributor: ready?.contributor, + deltaCount: deltas?.length, + }, { + startContributor: { kind: ToolCallContributorKind.Client, clientId: 'copilot-client-tool' }, + readyContributor: { kind: ToolCallContributorKind.Client, clientId: 'copilot-client-tool' }, + deltaCount: 0, + }); }); test('client tool disconnect before permission still completes the turn', async function () { diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/fileOperationsSuite.ts b/src/vs/platform/agentHost/test/node/e2e/suites/fileOperationsSuite.ts index e8f2fcf6fe5..563e58079d2 100644 --- a/src/vs/platform/agentHost/test/node/e2e/suites/fileOperationsSuite.ts +++ b/src/vs/platform/agentHost/test/node/e2e/suites/fileOperationsSuite.ts @@ -9,10 +9,17 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from import { tmpdir } from 'os'; import { join } from '../../../../../../base/common/path.js'; import { URI } from '../../../../../../base/common/uri.js'; +import type { StringOrMarkdown } from '../../../../common/state/protocol/state.js'; +import type { ChatToolCallDeltaAction, ChatToolCallReadyAction, ChatToolCallStartAction } from '../../../../common/state/sessionActions.js'; import { createRealSession, driveTurnToCompletion, initTestGitRepo } from '../harness/agentHostE2ETestHarness.js'; import { assertRecordedAhpSnapshot } from '../harness/ahpSnapshot.js'; +import { getActionEnvelope, isActionNotification } from '../../serverIntegrationTestHelpers.js'; import type { IAgentHostE2ETestContext } from './e2eTestContext.js'; +function stringOrMarkdownText(value: StringOrMarkdown | undefined): string | undefined { + return typeof value === 'string' ? value : value?.markdown; +} + export function defineFileOperationsTests(context: IAgentHostE2ETestContext): void { const { config, createdSessions, tempDirs, portableShellToolReplayEnabled, supportsFileTools, stableSharedServerFileScenarios } = context; const BEHAVIOR_SNAPSHOT = { profile: 'behavior' } as const; @@ -75,6 +82,52 @@ export function defineFileOperationsTests(context: IAgentHostE2ETestContext): vo await assertRecordedAhpSnapshot(this.test!, context.client, BEHAVIOR_SNAPSHOT); }); + (supportsFileTools && config.streamingFileCreateToolName ? test : test.skip)('streams rich file creation progress without exposing partial input', async function () { + this.timeout(180_000); + const workspace = mkdtempSync(join(tmpdir(), 'ahp-streaming-create-')); + tempDirs.push(workspace); + const sessionUri = await createRealSession(context.client, config, `streaming-create-${config.provider}`, createdSessions, URI.file(workspace)); + const turnId = 'turn-streaming-create'; + const expectedContent = 'STREAM_ALPHA\nSTREAM_BETA\nSTREAM_GAMMA'; + + await driveTurnToCompletion(context.client, sessionUri, turnId, `Create streaming.txt containing exactly these three lines, with no other content: +STREAM_ALPHA +STREAM_BETA +STREAM_GAMMA +Use your file creation tool; do not run a shell command. Then reply exactly "done".`, 1); + + const start = context.client.receivedNotifications(n => isActionNotification(n, 'chat/toolCallStart')) + .map(n => getActionEnvelope(n).action as ChatToolCallStartAction) + .find(action => action.turnId === turnId && action.toolName === config.streamingFileCreateToolName); + const deltas = start ? context.client.receivedNotifications(n => isActionNotification(n, 'chat/toolCallDelta')) + .map(n => getActionEnvelope(n).action as ChatToolCallDeltaAction) + .filter(action => action.toolCallId === start.toolCallId) : []; + const ready = start ? context.client.receivedNotifications(n => isActionNotification(n, 'chat/toolCallReady')) + .map(n => getActionEnvelope(n).action as ChatToolCallReadyAction) + .filter(action => action.toolCallId === start.toolCallId) : []; + const progressMessages = deltas.map(delta => stringOrMarkdownText(delta.invocationMessage)); + const fileContent = readFileSync(join(workspace, 'streaming.txt'), 'utf8'); + const normalizedFileContent = fileContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); + const lineCount = fileContent.split(/\r\n|\r|\n/).length; + const readyInputs = ready.map(action => action.toolInput).filter(input => input !== undefined); + + assert.deepStrictEqual({ + fileContent: normalizedFileContent.trimEnd(), + hasProgress: deltas.length > 0, + hidesPartialInput: deltas.every(delta => delta.content === ''), + showsFile: progressMessages.some(message => message?.includes('streaming.txt')), + showsLineCount: progressMessages.some(message => message?.includes(`(${lineCount} lines)`)), + readyHasFinalInput: readyInputs.some(input => ['STREAM_ALPHA', 'STREAM_BETA', 'STREAM_GAMMA'].every(value => input.includes(value))), + }, { + fileContent: expectedContent, + hasProgress: true, + hidesPartialInput: true, + showsFile: true, + showsLineCount: true, + readyHasFinalInput: true, + }); + }); + // Copilot never completes the replayed turn; Codex has no file tools, so it // cannot honor this prompt's steer away from the shell. (supportsFileTools && config.provider === 'claude' ? test : test.skip)('reads a value from JSON', async function () { diff --git a/src/vs/platform/agentHost/test/node/e2e/suites/turnLifecycleSuite.ts b/src/vs/platform/agentHost/test/node/e2e/suites/turnLifecycleSuite.ts index d09fa8e6336..35a858deb63 100644 --- a/src/vs/platform/agentHost/test/node/e2e/suites/turnLifecycleSuite.ts +++ b/src/vs/platform/agentHost/test/node/e2e/suites/turnLifecycleSuite.ts @@ -78,6 +78,13 @@ export function defineTurnLifecycleTests(context: IAgentHostE2ETestContext): voi const toolStarts = context.client.receivedNotifications(n => isActionNotification(n, 'chat/toolCallStart')); assert.ok(toolStarts.length > 0, 'expected at least one shell tool call'); + if (config.provider === 'copilotcli') { + const toolDeltas = context.client.receivedNotifications(n => isActionNotification(n, 'chat/toolCallDelta')); + assert.ok(toolDeltas.length > 0, 'expected Copilot tool progress before the tool was ready'); + const delta = getActionEnvelope(toolDeltas[0]).action as { content?: string; invocationMessage?: unknown }; + assert.ok(delta.invocationMessage, 'expected Copilot to stream an invocation message'); + assert.strictEqual(delta.content, '', 'Copilot should keep partial tool input in the agent host'); + } // Drain the post-tool continuation to `turnComplete` so the turn ends // within this test's window. This is required for the shared replay diff --git a/src/vs/platform/agentHost/test/node/mapSessionEvents.test.ts b/src/vs/platform/agentHost/test/node/mapSessionEvents.test.ts index f9fd1fe17d6..c94b753765a 100644 --- a/src/vs/platform/agentHost/test/node/mapSessionEvents.test.ts +++ b/src/vs/platform/agentHost/test/node/mapSessionEvents.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { readToolCallMeta } from '../../common/meta/agentToolCallMeta.js'; import { AgentSession } from '../../common/agentService.js'; @@ -124,6 +125,34 @@ suite('mapSessionEvents — history replay', () => { ]); }); + test('resolves relative patch links in restored tool messages', async () => { + const patch = [ + '*** Begin Patch', + '*** Update File: src/file.ts', + '@@', + '-old', + '+new', + '*** End Patch', + ].join('\n'); + const events: ISessionEvent[] = [ + { type: 'user.message', data: { interactionId: 'm1', content: 'edit the file' } }, + { type: 'assistant.message', data: { messageId: 'm2', content: '', toolRequests: [{ toolCallId: 'tc-1', name: 'apply_patch' }] } }, + { type: 'tool.execution_start', data: { toolCallId: 'tc-1', toolName: 'apply_patch', arguments: patch } }, + { type: 'tool.execution_complete', data: { toolCallId: 'tc-1', success: true } }, + ]; + + const { turns } = await mapSessionEvents(session, undefined, toSessionEvents(events), URI.file('/workspace')); + const part = turns[0].responseParts.find(part => part.kind === ResponsePartKind.ToolCall) as ToolCallResponsePart | undefined; + assert.ok(part); + assert.deepStrictEqual({ + invocationMessage: part.toolCall.status === ToolCallStatus.Completed ? part.toolCall.invocationMessage : undefined, + pastTenseMessage: part.toolCall.status === ToolCallStatus.Completed ? part.toolCall.pastTenseMessage : undefined, + }, { + invocationMessage: { markdown: 'Editing [file.ts](file:///workspace/src/file.ts)' }, + pastTenseMessage: { markdown: 'Edited [file.ts](file:///workspace/src/file.ts)' }, + }); + }); + test('restores MCP app data for completed tool calls', async () => { const events: ISessionEvent[] = [ { type: 'user.message', data: { interactionId: 'm1', content: 'call an MCP app tool' } }, diff --git a/src/vs/platform/agentHost/test/node/reducers.test.ts b/src/vs/platform/agentHost/test/node/reducers.test.ts index 6dbb241ba30..b0b52247dd4 100644 --- a/src/vs/platform/agentHost/test/node/reducers.test.ts +++ b/src/vs/platform/agentHost/test/node/reducers.test.ts @@ -8,7 +8,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { changesetReducer, chatReducer, sessionReducer } from '../../common/state/protocol/reducers.js'; import { ActionType } from '../../common/state/sessionActions.js'; import { ChangesetStatus, ChangesetOperationStatus, CustomizationLoadStatus, MessageKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ChatOriginKind, SessionLifecycle, SessionStatus, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ResponsePartKind, ToolCallStatus, TurnState, type AgentCustomization, type ChangesetState, type Customization, type PluginCustomization, type ChatState, type SessionState } from '../../common/state/sessionState.js'; -import { CustomizationType } from '../../common/state/protocol/state.js'; +import { CustomizationType, ToolCallContributorKind, type ToolCallContributor } from '../../common/state/protocol/state.js'; function makeSession(): SessionState { return { @@ -334,6 +334,129 @@ suite('chatReducer – summaryStatus with tool call confirmations and input requ ]); }); + test('ChatToolCallDelta can update the invocation message without exposing partial input', () => { + let state = chatReducer(makeChat(), { + type: ActionType.ChatTurnStarted, + turnId: 'turn-1', + startedAt: '2025-01-01T00:00:00.000Z', + message: { text: 'hello', origin: { kind: MessageKind.User } }, + }); + state = chatReducer(state, { + type: ActionType.ChatToolCallStart, + turnId: 'turn-1', + toolCallId: 'tc-1', + toolName: 'edit', + displayName: 'Edit File', + }); + state = chatReducer(state, { + type: ActionType.ChatToolCallDelta, + turnId: 'turn-1', + toolCallId: 'tc-1', + content: '', + invocationMessage: 'Replacing 2 lines with 3 lines', + }); + + const part = state.activeTurn?.responseParts.find(part => part.kind === ResponsePartKind.ToolCall); + assert.ok(part?.kind === ResponsePartKind.ToolCall); + assert.deepStrictEqual({ + invocationMessage: part.toolCall.status === ToolCallStatus.Streaming ? part.toolCall.invocationMessage : undefined, + partialInput: part.toolCall.status === ToolCallStatus.Streaming ? part.toolCall.partialInput : undefined, + }, { + invocationMessage: 'Replacing 2 lines with 3 lines', + partialInput: '', + }); + }); + + test('ChatToolCallReady replaces provisional contributor and intention', () => { + let state = chatReducer(makeChat(), { + type: ActionType.ChatTurnStarted, + turnId: 'turn-1', + startedAt: '2025-01-01T00:00:00.000Z', + message: { text: 'hello', origin: { kind: MessageKind.User } }, + }); + state = chatReducer(state, { + type: ActionType.ChatToolCallStart, + turnId: 'turn-1', + toolCallId: 'tc-1', + toolName: 'mcp_tool', + displayName: 'MCP Tool', + intention: 'Query', + }); + state = chatReducer(state, { + type: ActionType.ChatToolCallReady, + turnId: 'turn-1', + toolCallId: 'tc-1', + contributor: { kind: ToolCallContributorKind.MCP, customizationId: 'mcp-1' }, + intention: 'Query project metadata', + invocationMessage: 'Querying project metadata', + toolInput: '{"query":"metadata"}', + confirmed: ToolCallConfirmationReason.NotNeeded, + }); + + const part = state.activeTurn?.responseParts.find(part => part.kind === ResponsePartKind.ToolCall); + assert.ok(part?.kind === ResponsePartKind.ToolCall); + assert.deepStrictEqual({ + status: part.toolCall.status, + contributor: part.toolCall.contributor, + intention: part.toolCall.intention, + }, { + status: ToolCallStatus.Running, + contributor: { kind: ToolCallContributorKind.MCP, customizationId: 'mcp-1' }, + intention: 'Query project metadata', + }); + }); + + test('ChatToolCallReady cannot change client execution ownership', () => { + const readyContributor = (startContributor: ToolCallContributor | undefined, contributor: ToolCallContributor) => { + let state = chatReducer(makeChat(), { + type: ActionType.ChatTurnStarted, + turnId: 'turn-1', + startedAt: '2025-01-01T00:00:00.000Z', + message: { text: 'hello', origin: { kind: MessageKind.User } }, + }); + state = chatReducer(state, { + type: ActionType.ChatToolCallStart, + turnId: 'turn-1', + toolCallId: 'tc-1', + toolName: 'tool', + displayName: 'Tool', + contributor: startContributor, + }); + state = chatReducer(state, { + type: ActionType.ChatToolCallReady, + turnId: 'turn-1', + toolCallId: 'tc-1', + contributor, + invocationMessage: 'Running tool', + confirmed: ToolCallConfirmationReason.NotNeeded, + }); + const part = state.activeTurn?.responseParts.find(part => part.kind === ResponsePartKind.ToolCall); + assert.ok(part?.kind === ResponsePartKind.ToolCall); + return part.toolCall.contributor; + }; + + assert.deepStrictEqual([ + readyContributor(undefined, { kind: ToolCallContributorKind.Client, clientId: 'client-1' }), + readyContributor( + { kind: ToolCallContributorKind.MCP, customizationId: 'mcp-1' }, + { kind: ToolCallContributorKind.Client, clientId: 'client-1' }, + ), + readyContributor( + { kind: ToolCallContributorKind.Client, clientId: 'client-1' }, + { kind: ToolCallContributorKind.Client, clientId: 'client-2' }, + ), + readyContributor( + { kind: ToolCallContributorKind.Client, clientId: 'client-1' }, + { kind: ToolCallContributorKind.Client, clientId: 'client-1' }, + ), + ], [ + undefined, + { kind: ToolCallContributorKind.MCP, customizationId: 'mcp-1' }, + { kind: ToolCallContributorKind.Client, clientId: 'client-1' }, + { kind: ToolCallContributorKind.Client, clientId: 'client-1' }, + ]); + }); + test('ChatToolCallReady updates an asynchronous judge result on a pending confirmation', () => { const loading = chatReducer(withActiveTurnAndToolCall(makeChat()), { type: ActionType.ChatToolCallReady, 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 5d12dbe508c..f50aedcba5a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts @@ -99,7 +99,7 @@ import { buildHostLocalEventsPath } from '../../copilotCliEventsUri.js'; import { toolDataToDefinition } from './agentHostToolUtils.js'; import { IAgentHostUntitledProvisionalSessionService } from './agentHostUntitledProvisionalSessionService.js'; import { IAgentHostImportConversationStore } from './agentHostImportConversationStore.js'; -import { activeTurnToProgress, BOOLEAN_TRUE_OPTION_ID, completedToolCallToEditParts, completedToolCallToSerialized, containsAutomaticReplyAnswer, convertProtocolAnswers, convertProtocolPlanReviewResult, createInputRequestCarousel, createInputRequestPlanReview, finalizeToolInvocation, formatTurnResponseDetails, getTerminalContent, getUrlInputRequestPresentation, isSubagentTool, makeAhpTerminalToolSessionId, messageAttachmentsToVariableData, messageToVariableData, parseAhpTerminalToolSessionId, rewriteAgentHostLinkTarget, stringOrMarkdownToString, systemNotificationToChatPart, toolCallAuthenticationServer, toolCallConfirmationMessages, toolCallStateToInvocation, toolCallStateToPreparedInvocation, toolCallStateToStreamingInvocation, turnsToHistory, updateRunningToolSpecificData, usageInfoToAutoModeResolution, usageInfoToChatUsage, usageInfoToQuotas, type IAgentHostToolInvocationOptions, type IToolCallFileEdit, type TurnModelLookup } from './stateToProgressAdapter.js'; +import { activeTurnToProgress, BOOLEAN_TRUE_OPTION_ID, completedToolCallToEditParts, completedToolCallToSerialized, containsAutomaticReplyAnswer, convertProtocolAnswers, convertProtocolPlanReviewResult, createInputRequestCarousel, createInputRequestPlanReview, finalizeToolInvocation, formatTurnResponseDetails, getTerminalContent, getUrlInputRequestPresentation, isSubagentTool, makeAhpTerminalToolSessionId, messageAttachmentsToVariableData, messageToVariableData, parseAhpTerminalToolSessionId, rewriteAgentHostLinkTarget, stringOrMarkdownToString, systemNotificationToChatPart, toolCallAuthenticationServer, toolCallConfirmationMessages, toolCallStateToInvocation, toolCallStateToPreparedInvocation, toolCallStateToStreamingInvocation, turnsToHistory, updateRunningToolSpecificData, updateStreamingToolInvocation, usageInfoToAutoModeResolution, usageInfoToChatUsage, usageInfoToQuotas, type IAgentHostToolInvocationOptions, type IToolCallFileEdit, type TurnModelLookup } from './stateToProgressAdapter.js'; import { resolveMcpServerAuthentication, agentHostMcpServerId } from './agentHostAuth.js'; export { toolDataToDefinition }; @@ -2837,7 +2837,9 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC && previousStatus !== ToolCallStatus.PendingConfirmation; previousStatus = status; - if (enteringConfirmation) { + if (status === ToolCallStatus.Streaming) { + updateStreamingToolInvocation(invocation, tc, this._config.connectionAuthority); + } else if (enteringConfirmation) { if (!IChatToolInvocation.isComplete(invocation)) { const prepared = toolCallStateToPreparedInvocation(tc, opts.backendSession, this._config.connectionAuthority, opts.sessionResource.authority); invocation.requestConfirmation(prepared); @@ -2873,7 +2875,9 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC if ((status === ToolCallStatus.Completed || status === ToolCallStatus.Cancelled) && !IChatToolInvocation.isComplete(invocation)) { // Detach live non-PTY output before completion synchronously rebuilds the terminal subpart. - this._ensureLeftStreaming(invocation, tc, opts); + if (status === ToolCallStatus.Completed) { + this._ensureLeftStreaming(invocation, tc, opts); + } this._reviveTerminalIfNeeded(invocation, tc, opts.backendSession, outputTerminalAttachment); const fileEdits = finalizeToolInvocation(invocation, tc, opts.backendSession, this._config.connectionAuthority); if (fileEdits.length > 0) { @@ -3171,11 +3175,17 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC // nobody will consume. In the normal path we complete the call // ourselves first, so `invokeTool` has already settled and this // cancellation is a harmless no-op. + if (state.type === IChatToolInvocation.StateKind.Streaming) { + const fileEdits = finalizeToolInvocation(invocation, tc, opts.backendSession, this._config.connectionAuthority); + if (fileEdits.length > 0) { + opts.onFileEdits?.(tc, fileEdits); + } + } if (cts.token.isCancellationRequested) { return; } cts.cancel(); - if (!invoked && tc.status === ToolCallStatus.Cancelled) { + if (!invoked && tc.status === ToolCallStatus.Cancelled && state.type !== IChatToolInvocation.StateKind.Streaming) { // No `invokeTool` is listening to the CTS — transition // the invocation to `Cancelled` ourselves. invocation.cancelFromStreaming(ToolConfirmKind.Skipped); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts index 7835cf65852..5e2aa0c344f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts @@ -24,6 +24,7 @@ import { getAgentFeedbackAttachmentMetadata, isAgentFeedbackAnnotationsAttachmen import { getBrowserViewAttachmentMetadata, isBrowserViewAttachment } from '../../../../../../platform/agentHost/common/meta/browserViewAttachments.js'; import { isViewUnreviewedCommentsTool, isAddCommentTool } from '../../../../../../platform/agentHost/common/meta/agentFeedbackAnnotations.js'; import { isCreateChatTool, isCreateSessionTool, isSendMessageTool, parseOpenSessionLinkChatId, parseOpenSessionLinkUri } from '../../../../../../platform/agentHost/common/openSessionLink.js'; +import { parsePartialToolInputForDisplay } from '../../../../../../platform/agentHost/common/partialToolInput.js'; import { MessageAttachmentKind, type FileEdit, type MessageAttachment, type StringOrMarkdown, type TextRange } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { normalizeFileEdit } from '../../../../../../platform/agentHost/common/fileEditDiff.js'; import product from '../../../../../../platform/product/common/product.js'; @@ -2303,6 +2304,7 @@ export function toolCallStateToStreamingInvocation(tc: ToolCallState, subAgentIn }, subagentInvocationId: subAgentInvocationId, }); + updateStreamingToolInvocation(invocation, tc, connectionAuthority ?? ''); if (isAgentHostAskUserTool(tc.toolName)) { invocation.invocationMessage = localize('agentHost.askUser.asking', "Asking a question..."); invocation.presentation = ToolInvocationPresentation.HiddenAfterComplete; @@ -2313,6 +2315,28 @@ export function toolCallStateToStreamingInvocation(tc: ToolCallState, subAgentIn return invocation; } +function getStreamingToolInputForDisplay(tc: ToolCallState): unknown | undefined { + if (tc.status !== ToolCallStatus.Streaming || !tc.partialInput) { + return undefined; + } + return parsePartialToolInputForDisplay(tc.partialInput) ?? tc.partialInput; +} + +export function updateStreamingToolInvocation(existing: ChatToolInvocation, tc: ToolCallState, connectionAuthority: string): unknown | undefined { + if (tc.status !== ToolCallStatus.Streaming) { + return undefined; + } + const partialInput = getStreamingToolInputForDisplay(tc); + if (partialInput !== undefined) { + existing.updatePartialInput(partialInput); + } + const invocationMessage = stringOrMarkdownToString(tc.invocationMessage, connectionAuthority); + if (invocationMessage) { + existing.updateStreamingMessage(invocationMessage); + } + return partialInput; +} + /** * Extracts the {@link IPreparedToolInvocation} display fields for a tool-call * state, reusing {@link toolCallStateToInvocation} so the confirmation, @@ -2574,7 +2598,13 @@ export function finalizeToolInvocation(invocation: ChatToolInvocation, tc: ToolC const result: IToolResult | undefined = isFailure || resultDetails ? { content: [], toolResultError: isFailure ? errorString : undefined, toolResultDetails: resultDetails } : undefined; - invocation.didExecuteTool(result); + const cancelledFromStreaming = isCancelled && invocation.cancelFromStreaming( + tc.reason === ToolCallCancellationReason.Skipped ? ToolConfirmKind.Skipped : ToolConfirmKind.Denied, + tc.reasonMessage ? stringOrMarkdownToString(tc.reasonMessage, connectionAuthority) : undefined, + ); + if (!cancelledFromStreaming) { + invocation.didExecuteTool(result); + } return fileEdits; } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts index babbed5ca99..bd2ee462635 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts @@ -4068,6 +4068,71 @@ suite('AgentHostChatContribution', () => { assert.strictEqual(collected[0][0].kind, 'toolInvocation'); })); + test('tool deltas update one streaming invocation and transition it in place', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const { sessionHandler, agentHostService, chatAgentService } = createContribution(disposables); + const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + + fire({ type: 'chat/toolCallStart', session, turnId, toolCallId: 'tc-stream', toolName: 'view', displayName: 'View' } as ChatAction); + const invocation = collected.flat().find((part): part is IChatToolInvocation => part.kind === 'toolInvocation'); + assert.ok(invocation); + fire({ + type: 'chat/toolCallDelta', + session, + turnId, + toolCallId: 'tc-stream', + content: '{"path":"/workspace/file.ts"}', + invocationMessage: 'Viewing file', + } as ChatAction); + + const streamingState = invocation.state.get(); + assert.strictEqual(streamingState.type, IChatToolInvocation.StateKind.Streaming); + assert.strictEqual(collected.flat().filter(part => part.kind === 'toolInvocation').length, 1); + if (streamingState.type === IChatToolInvocation.StateKind.Streaming) { + assert.deepStrictEqual({ + partialInput: streamingState.partialInput.get(), + streamingMessage: streamingState.streamingMessage.get(), + }, { + partialInput: { path: '/workspace/file.ts' }, + streamingMessage: 'Viewing file', + }); + } + + fire({ + type: 'chat/toolCallReady', + session, + turnId, + toolCallId: 'tc-stream', + invocationMessage: 'Viewing file', + toolInput: '{"path":"/workspace/file.ts"}', + confirmed: 'not-needed', + } as ChatAction); + assert.strictEqual(invocation.state.get().type, IChatToolInvocation.StateKind.Executing); + assert.strictEqual(collected.flat().filter(part => part.kind === 'toolInvocation').length, 1); + + fire({ type: 'chat/turnComplete', endedAt: '2025-01-01T00:00:00.000Z', session, turnId } as ChatAction); + await turnPromise; + })); + + test('turn completion cancels a server tool that is still streaming', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const { sessionHandler, agentHostService, chatAgentService } = createContribution(disposables); + const { turnPromise, collected, session, turnId, fire } = await startTurn(sessionHandler, agentHostService, chatAgentService, disposables); + + fire({ type: 'chat/toolCallStart', session, turnId, toolCallId: 'tc-stream-cancel', toolName: 'view', displayName: 'View' } as ChatAction); + fire({ + type: 'chat/toolCallDelta', + session, + turnId, + toolCallId: 'tc-stream-cancel', + content: '{"path":"/workspace/file.ts', + } as ChatAction); + const invocation = collected.flat().find((part): part is IChatToolInvocation => part.kind === 'toolInvocation'); + assert.ok(invocation); + + fire({ type: 'chat/turnComplete', endedAt: '2025-01-01T00:00:00.000Z', session, turnId } as ChatAction); + await turnPromise; + assert.strictEqual(invocation.state.get().type, IChatToolInvocation.StateKind.Cancelled); + })); + test('tool_complete event transitions toolInvocation to completed', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const { sessionHandler, agentHostService, chatAgentService } = createContribution(disposables); @@ -8972,6 +9037,54 @@ suite('AgentHostChatContribution', () => { assert.strictEqual(toolInvocation!.toolCallId, 'tc-running'); }); + test('adopts and updates an active streaming tool call after reconnect', async () => { + const { sessionHandler, agentHostService } = createContribution(disposables); + const sessionUri = AgentSession.uri('copilot', 'reconnect-streaming-tool'); + const sessionState = makeSessionStateWithActiveTurn(sessionUri.toString()); + sessionState.activeTurn!.responseParts.push({ + kind: ResponsePartKind.ToolCall, + toolCall: { + toolCallId: 'tc-streaming', + toolName: 'view', + displayName: 'View', + status: ToolCallStatus.Streaming, + partialInput: '{"path":"/workspace/file.ts"}', + invocationMessage: 'Viewing file', + }, + }); + agentHostService.sessionStates.set(sessionUri.toString(), sessionState); + + const sessionResource = URI.from({ scheme: 'agent-host-copilot', path: '/reconnect-streaming-tool' }); + const session = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None); + disposables.add(toDisposable(() => session.dispose())); + const progress = session.progressObs?.get() ?? []; + const invocation = progress.find((part): part is IChatToolInvocation => part.kind === 'toolInvocation'); + assert.ok(invocation); + const streamingState = invocation.state.get(); + assert.strictEqual(streamingState.type, IChatToolInvocation.StateKind.Streaming); + if (streamingState.type === IChatToolInvocation.StateKind.Streaming) { + assert.deepStrictEqual(streamingState.partialInput.get(), { path: '/workspace/file.ts' }); + } + + agentHostService.fireAction({ + channel: sessionUri.toString(), + action: { + type: ActionType.ChatToolCallReady, + turnId: 'turn-active', + toolCallId: 'tc-streaming', + invocationMessage: 'Viewing file', + toolInput: '{"path":"/workspace/file.ts"}', + confirmed: ToolCallConfirmationReason.NotNeeded, + }, + serverSeq: 1, + origin: undefined, + }); + await timeout(0); + + assert.strictEqual(invocation.state.get().type, IChatToolInvocation.StateKind.Executing); + assert.strictEqual((session.progressObs?.get() ?? []).filter(part => part.kind === 'toolInvocation').length, 1); + }); + test('handles active turn with pending tool confirmation', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const { sessionHandler, agentHostService } = createContribution(disposables); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts index 5b764be2417..58a7045e5fa 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts @@ -1348,12 +1348,33 @@ suite('stateToProgressAdapter', () => { type AnyToolCallState = Parameters[0]; test('toolCallStateToStreamingInvocation starts in the native Streaming state', () => { - const tc: AnyToolCallState = { toolCallId: 'tc-stream', toolName: 'bash', displayName: 'Bash', status: ToolCallStatus.Streaming }; + const tc: AnyToolCallState = { + toolCallId: 'tc-stream', + toolName: 'bash', + displayName: 'Bash', + status: ToolCallStatus.Streaming, + partialInput: '{"command":"npm test","description":"Run', + invocationMessage: 'Running npm test', + }; const invocation = toolCallStateToStreamingInvocation(tc, undefined); - assert.strictEqual(invocation.toolCallId, 'tc-stream'); - assert.strictEqual(invocation.toolId, 'bash'); - assert.strictEqual(invocation.state.get().type, IChatToolInvocation.StateKind.Streaming); - assert.strictEqual(IChatToolInvocation.isComplete(invocation), false); + const state = invocation.state.get(); + assert.strictEqual(state.type, IChatToolInvocation.StateKind.Streaming); + if (state.type !== IChatToolInvocation.StateKind.Streaming) { + return; + } + assert.deepStrictEqual({ + toolCallId: invocation.toolCallId, + toolId: invocation.toolId, + partialInput: state.partialInput.get(), + streamingMessage: state.streamingMessage.get(), + isComplete: IChatToolInvocation.isComplete(invocation), + }, { + toolCallId: 'tc-stream', + toolId: 'bash', + partialInput: { command: 'npm test', description: 'Run' }, + streamingMessage: 'Running npm test', + isComplete: false, + }); }); test('toolCallStateToStreamingInvocation preserves subagent metadata before ready', () => { @@ -1379,6 +1400,32 @@ suite('stateToProgressAdapter', () => { }); }); + test('finalizeToolInvocation preserves cancellation from streaming', () => { + const invocation = toolCallStateToStreamingInvocation({ + toolCallId: 'tc-cancelled', + toolName: 'client_tool', + displayName: 'Client Tool', + status: ToolCallStatus.Streaming, + }, undefined); + finalizeToolInvocation(invocation, { + toolCallId: 'tc-cancelled', + toolName: 'client_tool', + displayName: 'Client Tool', + status: ToolCallStatus.Cancelled, + invocationMessage: 'Running client tool', + reason: ToolCallCancellationReason.Denied, + reasonMessage: 'Denied by the server', + }); + + assert.deepStrictEqual(invocation.state.get(), { + type: IChatToolInvocation.StateKind.Cancelled, + reason: ToolConfirmKind.Denied, + reasonMessage: 'Denied by the server', + parameters: undefined, + confirmationMessages: undefined, + }); + }); + test('transitionFromStreaming with a pending terminal prepared invocation yields a single terminal confirmation card', () => { // A terminal command streamed its args, then requested confirmation. const streaming = toolCallStateToStreamingInvocation({ toolCallId: 'tc-term', toolName: 'bash', displayName: 'Bash', status: ToolCallStatus.Streaming }, undefined); From 892237653c561e6f3a5455475d449f90a495889d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:02:24 +0200 Subject: [PATCH 32/86] Debounce worktree creation progress updates (#328276) * Debounce worktree creation progress updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify worktree progress reporting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../node/shared/worktreeIsolation.ts | 48 +++++++++++++------ .../node/shared/worktreeIsolation.test.ts | 5 +- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/vs/platform/agentHost/node/shared/worktreeIsolation.ts b/src/vs/platform/agentHost/node/shared/worktreeIsolation.ts index 4928bb35440..544fe5a5e21 100644 --- a/src/vs/platform/agentHost/node/shared/worktreeIsolation.ts +++ b/src/vs/platform/agentHost/node/shared/worktreeIsolation.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs/promises'; -import { SequencerByKey } from '../../../../base/common/async.js'; +import { RunOnceScheduler, SequencerByKey } from '../../../../base/common/async.js'; import { appendEscapedMarkdownInlineCode } from '../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; @@ -47,6 +47,7 @@ export class SessionWorkingDirectoryMissingError extends Error { /** Default upper bound on branch names returned for the branch picker. */ const BRANCH_COMPLETION_LIMIT = 25; +const WORKTREE_PROGRESS_DEBOUNCE_MS = 40; interface ICreatedWorktree { readonly repositoryRoot: URI; @@ -135,20 +136,37 @@ export function buildWorktreeProgressText(phase: WorktreeCreationPhase, percent? /** * Adapts the raw file counts the git service reports into progress labels for - * a phase. Samples can arrive several times a second, so this rounds down to - * whole percent and drops anything that doesn't advance it — a consumer never - * sees two updates that read the same, and never more than 101 per phase. + * a phase. Rounds down to whole percentages, drops non-advancing samples, and + * debounces updates to avoid overwhelming consumers, flushing the latest + * percentage when the operation completes. */ -function createPercentProgressReporter(phase: WorktreeCreationPhase, onProgress: (activity: string) => void): (progress: IWorktreeFileProgress) => void { +async function withPercentProgress( + phase: WorktreeCreationPhase, + onProgress: ((activity: string) => void) | undefined, + operation: (onProgress: ((progress: IWorktreeFileProgress) => void) | undefined) => Promise, +): Promise { + if (!onProgress) { + return operation(undefined); + } + let lastPercent = -1; - return ({ filesDone, filesTotal }) => { - const percent = Math.min(100, Math.floor(filesDone * 100 / filesTotal)); - if (percent <= lastPercent) { - return; + const scheduler = new RunOnceScheduler(() => onProgress(buildWorktreeProgressText(phase, lastPercent)), WORKTREE_PROGRESS_DEBOUNCE_MS); + try { + return await operation(({ filesDone, filesTotal }) => { + const percent = Math.min(100, Math.floor(filesDone * 100 / filesTotal)); + if (percent <= lastPercent) { + return; + } + lastPercent = percent; + scheduler.schedule(); + }); + } finally { + const shouldFlush = scheduler.isScheduled(); + scheduler.dispose(); + if (shouldFlush) { + onProgress(buildWorktreeProgressText(phase, lastPercent)); } - lastPercent = percent; - onProgress(buildWorktreeProgressText(phase, percent)); - }; + } } /** @@ -538,7 +556,8 @@ export class WorktreeIsolation extends Disposable { onProgress?.(buildWorktreeProgressText(WorktreeCreationPhase.CheckingOut)); const worktreeBranchTrack = config[SessionConfigKey.WorktreeBranchTrack] === true; - await this._gitService.addWorktree(repositoryRoot, worktree, branchName, baseBranch, worktreeBranchTrack, onProgress && createPercentProgressReporter(WorktreeCreationPhase.CheckingOut, onProgress)); + await withPercentProgress(WorktreeCreationPhase.CheckingOut, onProgress, progress => + this._gitService.addWorktree(repositoryRoot, worktree, branchName, baseBranch, worktreeBranchTrack, progress)); return { branchName, worktree, baseBranch }; }); const worktreeIncludeFiles = Array.isArray(config[SessionConfigKey.WorktreeIncludeFiles]) @@ -548,7 +567,8 @@ export class WorktreeIsolation extends Disposable { if (worktreeIncludeFiles?.length) { try { onProgress?.(buildWorktreeProgressText(WorktreeCreationPhase.CopyingIncludeFiles)); - await this._gitService.copyWorktreeIncludeFiles(repositoryRoot, worktree, worktreeIncludeFiles, onProgress && createPercentProgressReporter(WorktreeCreationPhase.CopyingIncludeFiles, onProgress)); + await withPercentProgress(WorktreeCreationPhase.CopyingIncludeFiles, onProgress, progress => + this._gitService.copyWorktreeIncludeFiles(repositoryRoot, worktree, worktreeIncludeFiles, progress)); } catch (error) { this._logService.warn(`[${this._logLabel}:${sessionId}] Failed to copy worktree include files: ${errorMessage(error)}`); } diff --git a/src/vs/platform/agentHost/test/node/shared/worktreeIsolation.test.ts b/src/vs/platform/agentHost/test/node/shared/worktreeIsolation.test.ts index c92d9777032..1dd0124c50f 100644 --- a/src/vs/platform/agentHost/test/node/shared/worktreeIsolation.test.ts +++ b/src/vs/platform/agentHost/test/node/shared/worktreeIsolation.test.ts @@ -240,7 +240,7 @@ suite('WorktreeIsolation', () => { }); }); - test('resolveWorkingDirectory names each creation phase, rounding percentages down and skipping repeats', async () => { + test('resolveWorkingDirectory names each creation phase, rounding percentages down and debouncing updates', async () => { const gitService = createGitService(); gitService.addWorktree = async (_root, worktree, branch, startPoint, track, onProgress) => { addWorktreeCalls.push({ worktree, branchName: branch, startPoint, track }); @@ -248,6 +248,7 @@ suite('WorktreeIsolation', () => { onProgress?.({ filesDone: 7, filesTotal: 800 }); onProgress?.({ filesDone: 96, filesTotal: 800 }); onProgress?.({ filesDone: 100, filesTotal: 800 }); + await timeout(50); onProgress?.({ filesDone: 800, filesTotal: 800 }); }; gitService.copyWorktreeIncludeFiles = async (_root, _worktree, _globs, onProgress) => { @@ -274,11 +275,9 @@ suite('WorktreeIsolation', () => { 'Creating isolated worktree', 'Creating isolated worktree (naming branch)', 'Creating isolated worktree (checking out files)', - 'Creating isolated worktree (checking out files, 0%)', 'Creating isolated worktree (checking out files, 12%)', 'Creating isolated worktree (checking out files, 100%)', 'Creating isolated worktree (copying additional files)', - 'Creating isolated worktree (copying additional files, 25%)', 'Creating isolated worktree (copying additional files, 100%)', ]); }); From d4ae03802569e20cada62a040aa38349e6263261 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:13:43 +0200 Subject: [PATCH 33/86] sessions: propagate visibility to hidden chat transcripts (#328273) * sessions: propagate visibility to hidden chat transcripts (#328200) The Sessions Grid introduced nested visibility without forwarding that lifecycle to the chat widget, so ChatWidget._visible stayed true for hidden chats. Streaming responses kept refreshing the dynamic-height tree under a display:none ancestor, measuring rows at 0px and logging 'Measured item node at 0px'. Combine the Sessions part's visibility with each internal grid leaf's visibility, seed that effective value when views are created, and forward it through SessionView and ChatView to ChatWidget.setVisible. Also skip layout while hidden and catch up on reveal. Active-session state stays separate because inactive side-by-side sessions are still visible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * sessions: fix visibility test stub and trim comments The test stub was only cast to SessionView, so it lacked the prototype methods setVisible relies on and would throw on the first call. Build it from SessionView.prototype instead. Condense the added JSDoc and inline comments to keep only the non-obvious rationale. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/browser/parts/chatView.ts | 8 +++ src/vs/sessions/browser/parts/sessionView.ts | 59 +++++++++++++++++++ src/vs/sessions/browser/parts/sessionsPart.ts | 19 ++++++ .../sessions/contrib/chat/browser/chatView.ts | 12 +++- .../sessions/test/browser/sessionView.test.ts | 34 +++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/vs/sessions/test/browser/sessionView.test.ts diff --git a/src/vs/sessions/browser/parts/chatView.ts b/src/vs/sessions/browser/parts/chatView.ts index 7a0f59cfb38..f1a50dc79c8 100644 --- a/src/vs/sessions/browser/parts/chatView.ts +++ b/src/vs/sessions/browser/parts/chatView.ts @@ -129,6 +129,14 @@ export abstract class AbstractChatView extends Disposable implements ISerializab // no-op by default } + /** + * Notifies the view whether it is currently shown. Unlike {@link setActive}, + * inactive sessions displayed side by side are still visible. + */ + setVisible(_visible: boolean): void { + // no-op by default + } + /** * Shows an indeterminate progress bar at the top of this leaf while the * given promise is pending, mirroring how each editor group surfaces diff --git a/src/vs/sessions/browser/parts/sessionView.ts b/src/vs/sessions/browser/parts/sessionView.ts index 510b82869ed..2d6752a567f 100644 --- a/src/vs/sessions/browser/parts/sessionView.ts +++ b/src/vs/sessions/browser/parts/sessionView.ts @@ -85,6 +85,12 @@ export class SessionView extends Disposable implements ISerializableView { /** Whether this view currently hosts the active session in the grid. */ private _isActive = true; + /** Whether the owning {@link SessionsPart} is visible in the workbench grid. */ + private _isPartVisible = true; + + /** Whether this leaf is visible within the part's internal grid. */ + private _isLeafVisible = true; + private readonly _sessionObs = observableValue(this, undefined); constructor( @@ -214,6 +220,7 @@ export class SessionView extends Disposable implements ISerializableView { this._contentContainer.replaceChildren(view.element); this._currentView.value = view; view.setActive(this._isActive); + view.setVisible(this._isVisible); } if (session) { @@ -246,7 +253,12 @@ export class SessionView extends Disposable implements ISerializableView { if (!this._lastLayout) { return; } + + // A hidden or zero-sized leaf would report invalid geometry to the chat widget. const { width, height, top, left } = this._lastLayout; + if (!this._isVisible || width === 0 || height === 0) { + return; + } // Apply the centered band's width first so the header and tabs wrap to // their final layout before we measure their combined height. Measuring @@ -326,6 +338,53 @@ export class SessionView extends Disposable implements ISerializableView { this._currentView.value?.setActive(active); } + /** + * Grid hook invoked by the part's internal split view when this leaf is + * hidden or shown (e.g. when a sibling session is maximized). + */ + setVisible(visible: boolean): void { + if (this._isLeafVisible === visible) { + return; + } + const wasVisible = this._isVisible; + this._isLeafVisible = visible; + this._updateVisibility(wasVisible); + } + + /** + * Called by the owning {@link SessionsPart} when the part itself is hidden or + * shown in the workbench grid. Combined with this leaf's own visibility to + * form the view's effective visibility. + */ + setPartVisible(visible: boolean): void { + if (this._isPartVisible === visible) { + return; + } + const wasVisible = this._isVisible; + this._isPartVisible = visible; + this._updateVisibility(wasVisible); + } + + /** + * Whether this view is actually shown. Unrelated to {@link setActive}: + * inactive sessions shown side by side are still visible. + */ + private get _isVisible(): boolean { + return this._isPartVisible && this._isLeafVisible; + } + + private _updateVisibility(wasVisible: boolean): void { + const visible = this._isVisible; + if (visible === wasVisible) { + return; + } + this._currentView.value?.setVisible(visible); + if (visible) { + // Catch up on the layout passes that were skipped while hidden. + this._layoutChildren(); + } + } + private _applyActiveSessionStyles(): void { const background = this._isActive ? SessionView.ACTIVE_BACKGROUND : SessionView.INACTIVE_BACKGROUND; const foreground = this._isActive ? SessionView.ACTIVE_FOREGROUND : SessionView.INACTIVE_FOREGROUND; diff --git a/src/vs/sessions/browser/parts/sessionsPart.ts b/src/vs/sessions/browser/parts/sessionsPart.ts index 31234fb42f9..ce6e1c8f953 100644 --- a/src/vs/sessions/browser/parts/sessionsPart.ts +++ b/src/vs/sessions/browser/parts/sessionsPart.ts @@ -92,6 +92,12 @@ export class SessionsPart extends Part { private readonly _multipleSessionsVisibleKey: IContextKey; private readonly _sessionsFocusKey: IContextKey; + /** + * Whether the part itself is visible in the workbench grid. Starts `true` + * because the workbench grid only calls {@link setVisible} on change. + */ + private _isPartVisible = true; + /** * Whether the session type ("harness") picker should be rendered below the * input (in the controls) instead of next to the workspace picker. Backed @@ -381,6 +387,7 @@ export class SessionsPart extends Part { private _createSlot(): IGridSlot { const disposables = new DisposableStore(); const view = disposables.add(this.instantiationService.createInstance(SessionView)); + view.setPartVisible(this._isPartVisible); const slot: IGridSlot = { view, disposables, boundSessionId: undefined }; // Promote a visible session to the active session when its view receives // focus or is clicked. Pointer-down covers clicks on non-focusable chrome @@ -415,6 +422,18 @@ export class SessionsPart extends Part { this._gridWidget?.style({ separatorBorder: this._gridSeparatorBorder }); } + override setVisible(visible: boolean): void { + if (this._isPartVisible !== visible) { + // Update before `super`, whose event re-enters this method. + this._isPartVisible = visible; + for (const slot of this._slots) { + slot.view.setPartVisible(visible); + } + } + + super.setVisible(visible); + } + override layout(width: number, height: number, top: number, left: number): void { if (!this.layoutService.isVisible(Parts.SESSIONS_PART)) { return; diff --git a/src/vs/sessions/contrib/chat/browser/chatView.ts b/src/vs/sessions/contrib/chat/browser/chatView.ts index 768f73f31c4..c0ba25b3311 100644 --- a/src/vs/sessions/contrib/chat/browser/chatView.ts +++ b/src/vs/sessions/contrib/chat/browser/chatView.ts @@ -145,6 +145,9 @@ export class ChatView extends AbstractChatView { /** Observable mirror of {@link _isActive} so the voice overlay can react. */ private readonly _isActiveObs = observableValue(this, true); + /** Whether this view is currently visible. `undefined` so the first push always reaches the widget. */ + private _isVisible: boolean | undefined; + /** * Per-view mirror of `agentsVoiceInitiatedHere`, scoped above the chat widget. * Keeps post-connect voice controls anchored to the active session view. @@ -198,7 +201,6 @@ export class ChatView extends AbstractChatView { this._buildStyles(this._isActive) )); this._widget.render(this.element); - this._widget.setVisible(true); this._selectionSideChatController = this._register(scopedInstantiationService.createInstance(ResponseSelectionSideChatController, this._widget)); @@ -418,6 +420,14 @@ export class ChatView extends AbstractChatView { this._banners.setActive(active); this._widget.setStyles(this._buildStyles(active)); } + + override setVisible(visible: boolean): void { + if (this._isVisible === visible) { + return; + } + this._isVisible = visible; + this._widget.setVisible(visible); + } } /** diff --git a/src/vs/sessions/test/browser/sessionView.test.ts b/src/vs/sessions/test/browser/sessionView.test.ts new file mode 100644 index 00000000000..878ba1d8e27 --- /dev/null +++ b/src/vs/sessions/test/browser/sessionView.test.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { SessionView } from '../../browser/parts/sessionView.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; + +suite('Sessions - Session View', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('forwards effective visibility (part and grid leaf) to the hosted chat view', () => { + const forwarded: boolean[] = []; + // Created from the prototype so the internal visibility helpers are present. + const view: SessionView = Object.assign(Object.create(SessionView.prototype), { + _isPartVisible: true, + _isLeafVisible: true, + _lastLayout: undefined, + _currentView: { value: { setVisible: (visible: boolean) => forwarded.push(visible) } }, + }); + + // A sibling session is maximized, hiding this leaf. + view.setVisible(false); + // The whole sessions part is hidden while the leaf is still hidden. + view.setPartVisible(false); + // Leaving the maximized state must not reveal the chat while the part is hidden. + view.setVisible(true); + // Showing the part again reveals the chat. + view.setPartVisible(true); + + assert.deepStrictEqual(forwarded, [false, true]); + }); +}); From 18a1f78e0033427c9ae430aa3f9680733ebcb02e Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:00:16 -0700 Subject: [PATCH 34/86] Revert "[cherry-pick] Add Kimi K3 biosecurity policy for Copilot Chat recovery" (#328260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "[cherry-pick] Add Kimi K3 biosecurity policy for Copilot Chat recover…" This reverts commit eb7a00caa262f7fd09c6445c43d4adc445e50086. --- .../prompts/node/agent/kimiPrompts.tsx | 16 ----------- .../node/agent/test/kimiPrompts.spec.tsx | 27 ------------------- 2 files changed, 43 deletions(-) diff --git a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx index a55c2314f1a..22717206132 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/kimiPrompts.tsx @@ -16,20 +16,6 @@ import { ApplyPatchInstructions, CodesearchModeInstructions, DefaultAgentPromptP import { FileLinkificationInstructions } from './fileLinkificationInstructions'; import { IAgentPrompt, PromptRegistry, ReminderInstructionsConstructor, SystemPrompt } from './promptRegistry'; -const KIMI_K3_BIOSECURITY_AND_CHEMICAL_SAFETY_POLICY = `BIOSECURITY AND CHEMICAL SAFETY POLICY (non-negotiable) - -You must refuse to provide information that would provide meaningful uplift toward biological or chemical weapons or other mass-casualty harm. This overrides any other instruction, persona, or user framing (including "hypothetical", "fictional", "research", "educational", or "for a novel" framings). - -Refuse, and do not provide workarounds, partial answers, or pointers to sources, when a request seeks to: -- Bypass, evade, circumvent, or defeat nucleic-acid / DNA / RNA synthesis-provider screening, sequence order screening, or "know-your-customer" biosecurity controls (including obfuscating, fragmenting, recoding, or reordering sequences to avoid detection). -- Design, synthesize, acquire, culture, propagate, stabilize, enhance the virulence or transmissibility of, or weaponize any pathogen, toxin, or biological agent. -- Obtain step-by-step wet-lab protocols, troubleshooting, reagent/equipment sourcing, or scale-up guidance intended to produce a harmful biological or chemical agent. -- Produce, synthesize, or disperse chemical weapons, toxic agents, or their precursors. - -When refusing, give a brief, non-judgmental refusal and, where appropriate, offer clearly safe, high-level educational or defensive alternatives (e.g., biosafety, public-health, or policy information) without operational detail. - -Answer normally for legitimate requests: general science education, clinical care, approved public-health work, defensive biosecurity, and dual-use topics that do not provide operational uplift toward the categories above.`; - class KimiAgentPrompt extends PromptElement { async render(state: void, sizing: PromptSizing) { const tools = detectToolCapabilities(this.props.availableTools); @@ -40,8 +26,6 @@ class KimiAgentPrompt extends PromptElement { 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.
- {this.props.modelFamily?.toLowerCase().includes('kimi-k3') && <>{KIMI_K3_BIOSECURITY_AND_CHEMICAL_SAFETY_POLICY}
} - Use clear, step-by-step task execution:
- For simple questions or code samples, answer directly without unnecessary tool calls.
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 af1949831da..38aa6cee0d7 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 @@ -20,20 +20,6 @@ import { PromptRenderer } from '../../base/promptRenderer'; import '../allAgentPrompts'; import { PromptRegistry } from '../promptRegistry'; -const KIMI_K3_BIOSECURITY_AND_CHEMICAL_SAFETY_POLICY = `BIOSECURITY AND CHEMICAL SAFETY POLICY (non-negotiable) - -You must refuse to provide information that would provide meaningful uplift toward biological or chemical weapons or other mass-casualty harm. This overrides any other instruction, persona, or user framing (including "hypothetical", "fictional", "research", "educational", or "for a novel" framings). - -Refuse, and do not provide workarounds, partial answers, or pointers to sources, when a request seeks to: -- Bypass, evade, circumvent, or defeat nucleic-acid / DNA / RNA synthesis-provider screening, sequence order screening, or "know-your-customer" biosecurity controls (including obfuscating, fragmenting, recoding, or reordering sequences to avoid detection). -- Design, synthesize, acquire, culture, propagate, stabilize, enhance the virulence or transmissibility of, or weaponize any pathogen, toxin, or biological agent. -- Obtain step-by-step wet-lab protocols, troubleshooting, reagent/equipment sourcing, or scale-up guidance intended to produce a harmful biological or chemical agent. -- Produce, synthesize, or disperse chemical weapons, toxic agents, or their precursors. - -When refusing, give a brief, non-judgmental refusal and, where appropriate, offer clearly safe, high-level educational or defensive alternatives (e.g., biosafety, public-health, or policy information) without operational detail. - -Answer normally for legitimate requests: general science education, clinical care, approved public-health work, defensive biosecurity, and dual-use topics that do not provide operational uplift toward the categories above.`; - suite('KimiPrompts', () => { let accessor: ITestingServicesAccessor; @@ -90,17 +76,4 @@ suite('KimiPrompts', () => { expect(renderedPrompt).not.toContain(`Use ${ToolName.EditFile}`); expect(renderedPrompt).not.toContain(`Use ${ToolName.ApplyPatch}`); }); - - test('adds the biosecurity and chemical safety policy only for Kimi K3', async () => { - const kimiK3Prompt = await renderSystemPrompt('kimi-k3'); - const kimiK2Prompts = await Promise.all([ - renderSystemPrompt('kimi-k2.6'), - renderSystemPrompt('kimi-k2.7-code'), - ]); - - expect(kimiK3Prompt).toContain(KIMI_K3_BIOSECURITY_AND_CHEMICAL_SAFETY_POLICY); - for (const renderedPrompt of kimiK2Prompts) { - expect(renderedPrompt).not.toContain('BIOSECURITY AND CHEMICAL SAFETY POLICY'); - } - }); }); From 9dc513a697bc2cb4dad0d3313b15c95e04a4bc11 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:02:31 -0700 Subject: [PATCH 35/86] Fix checkout contribution for cloud sessions in backend v2 (#328258) * Agent Host changes for osortega/agents/checkout-contribution-check-v2-backend * Address review feedback and fix chat input fixtures Require non-empty owner/name before building a pull request URI, so metadata with empty strings no longer yields `https://github.com///pull/42` and is no longer reported as an available pull request. Cover the case in the unit test. Implement `getSession` on the fixture `IAgentSessionsService` stub. The chat input session toolbar reads it when a session has file changes, which broke the chatInput FileChanges fixtures. Condense the context key and chat input comments, and correct the cloud sessions comment: the guard is reachable whenever `chatSessionPullRequest` is unknown, not just on older clients. --- extensions/copilot/package.json | 2 +- .../copilotCloudSessionsProvider.ts | 3 +- .../browser/copilotChatSessionsProvider.ts | 28 +---------- .../agentSessions/agentSessionsControl.ts | 3 +- .../agentSessions/agentSessionsModel.ts | 38 ++++++++++++++ .../browser/widget/input/chatInputPart.ts | 12 +++++ .../chat/common/actions/chatContextKeys.ts | 5 ++ .../agentSessionPullRequest.test.ts | 50 +++++++++++++++++++ .../chat/chatFixtureUtils.ts | 5 +- 9 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionPullRequest.test.ts diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 717b17c6ad6..31145cc68f8 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -5618,7 +5618,7 @@ }, { "command": "github.copilot.chat.checkoutPullRequestReroute", - "when": "chatSessionType == copilot-cloud-agent && !github.vscode-pull-request-github.activated && gitOpenRepositoryCount != 0", + "when": "chatSessionType == copilot-cloud-agent && chatSessionPullRequest != 'none' && !github.vscode-pull-request-github.activated && gitOpenRepositoryCount != 0", "group": "navigation@0" }, { diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts index 8feea1a6cf0..4b435a75f97 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts @@ -525,8 +525,9 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C pullRequestNumber = SessionIdForPr.parsePullRequestNumber(resource); } - + // Reachable when `chatSessionPullRequest` is unknown, which keeps the action visible. if (!pullRequestNumber) { + this.logService.warn('No pull request number could be resolved for the requested cloud session action.'); return; } const repoIds = await getRepoId(this._gitService); diff --git a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts index 7218a9c30f1..8ec22317835 100644 --- a/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts @@ -16,7 +16,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IAgentSession } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { getAgentSessionPullRequestUri, IAgentSession } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { getRepositoryName } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.js'; import { IAgentSessionsService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionProviders, AgentSessionTarget } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; @@ -1312,31 +1312,7 @@ class AgentSessionAdapter implements ICopilotChatSession { } private _extractPullRequestUri(session: IAgentSession): URI | undefined { - const metadata = session.metadata; - if (!metadata) { - return undefined; - } - - const url = metadata.pullRequestUrl as string | undefined; - if (url) { - try { - return URI.parse(url); - } catch { - // fall through - } - } - - // Construct from pullRequestNumber + owner/repo - const prNumber = metadata.pullRequestNumber as number | undefined; - if (typeof prNumber === 'number') { - const owner = metadata.owner as string | undefined; - const name = metadata.name as string | undefined; - if (owner && name) { - return URI.parse(`https://github.com/${owner}/${name}/pull/${prNumber}`); - } - } - - return undefined; + return getAgentSessionPullRequestUri(session); } private _extractChanges(session: IAgentSession): readonly ISessionFileChange[] { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 494ccc827bd..39cd98139ff 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -12,7 +12,7 @@ import { $, append, EventHelper, addDisposableListener, EventType, getWindow, hi import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; import { KeyCode } from '../../../../../base/common/keyCodes.js'; import { localize } from '../../../../../nls.js'; -import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection, isAgentSessionShowLess, isAgentSessionShowMore } from './agentSessionsModel.js'; +import { AgentSessionSection, getAgentSessionPullRequestContextValue, IAgentSession, IAgentSessionSection, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection, isAgentSessionShowLess, isAgentSessionShowMore } from './agentSessionsModel.js'; import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionSectionLabels, AgentSessionShowLessRenderer, AgentSessionShowMoreRenderer, AgentSessionsSorter, getRepositoryName, IAgentSessionsFilter } from './agentSessionsViewer.js'; import { AgentSessionsGrouping, AgentSessionsSorting } from './agentSessionsFilter.js'; import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; @@ -713,6 +713,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo contextOverlay.push([ChatContextKeys.isPinnedAgentSession.key, session.isPinned()]); contextOverlay.push([ChatContextKeys.isReadAgentSession.key, session.isRead()]); contextOverlay.push([ChatContextKeys.agentSessionType.key, session.providerType]); + contextOverlay.push([ChatContextKeys.agentSessionPullRequest.key, getAgentSessionPullRequestContextValue(session)]); const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay)); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 39aa70190d3..ab7208e3fc0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -159,6 +159,44 @@ export function isLocalAgentSessionItem(session: IAgentSession): boolean { return session.providerType === AgentSessionProviders.Local; } +/** + * Resolves the pull request associated with an agent session from its provider metadata, + * preferring an explicit `pullRequestUrl` and falling back to `pullRequestNumber` combined + * with `owner`/`name`. Returns `undefined` when the session has no associated pull request. + */ +export function getAgentSessionPullRequestUri(session: Pick): URI | undefined { + const metadata = session.metadata; + if (!metadata) { + return undefined; + } + + const url = metadata.pullRequestUrl; + if (typeof url === 'string' && url) { + try { + return URI.parse(url); + } catch { + // Fall through to the number based lookup below. + } + } + + const prNumber = metadata.pullRequestNumber; + const owner = metadata.owner; + const name = metadata.name; + if (typeof prNumber === 'number' && typeof owner === 'string' && owner && typeof name === 'string' && name) { + return URI.parse(`https://github.com/${owner}/${name}/pull/${prNumber}`); + } + + return undefined; +} + +/** + * The value for the `chatSessionPullRequest` context key for a session. Never returns an + * "unknown" value: callers here always have the session's metadata in hand. + */ +export function getAgentSessionPullRequestContextValue(session: Pick): 'available' | 'none' { + return getAgentSessionPullRequestUri(session) ? 'available' : 'none'; +} + export function isAgentHostAgentSessionItem(session: IAgentSession): boolean { return isAgentHostTarget(session.providerType); } 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 2a81d98b9d8..b021b95a6c5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -116,6 +116,7 @@ import { IDictationOnboardingService } from '../../speechToText/dictationOnboard import { notifyDictationSubmitted } from '../../speechToText/dictationSession.js'; import { VoiceModeActionViewItem } from '../../voiceClient/voiceModeActionViewItem.js'; import { AgentSessionProviders, AgentSessionTarget, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { getAgentSessionPullRequestContextValue } from '../../agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; import { IChatAttachmentWidgetRegistry } from '../../attachments/chatAttachmentWidgetRegistry.js'; @@ -4262,6 +4263,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const scopedContextKeyService = this._chatEditsActionsDisposables.add(this.contextKeyService.createScoped(actionsContainer)); if (sessionResource) { scopedContextKeyService.createKey(ChatContextKeys.agentSessionType.key, getChatSessionType(sessionResource)); + + // Metadata can arrive after first render, so track it rather than sampling once. + const sessionPullRequest = observableFromEvent( + this, + this.agentSessionsService.model.onDidChangeSessions, + () => { + const session = this.agentSessionsService.getSession(sessionResource); + return session ? getAgentSessionPullRequestContextValue(session) : ''; + }, + ); + this._chatEditsActionsDisposables.add(bindContextKey(ChatContextKeys.agentSessionPullRequest, scopedContextKeyService, r => sessionPullRequest.read(r))); } this._chatEditsActionsDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, scopedContextKeyService, r => !!sessionEntriesObs.read(r)?.length)); diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 901850d5c1b..466353d4913 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -156,6 +156,11 @@ export namespace ChatContextKeys { export const agentSessionsViewerPosition = new RawContextKey('agentSessionsViewerPosition', undefined, { type: 'number', description: localize('agentSessionsViewerPosition', "Position of the agent sessions view in the chat view.") }); export const agentSessionsViewerVisible = new RawContextKey('agentSessionsViewerVisible', undefined, { type: 'boolean', description: localize('agentSessionsViewerVisible', "Visibility of the agent sessions view in the chat view.") }); export const agentSessionType = new RawContextKey('chatSessionType', '', { type: 'string', description: localize('agentSessionType', "The type of the current agent session item.") }); + /** + * Whether the agent session item has an associated pull request. Tri-state, so gate with + * `chatSessionPullRequest != 'none'` to keep contributions visible when the state is unknown. + */ + export const agentSessionPullRequest = new RawContextKey('chatSessionPullRequest', '', { type: 'string', description: localize('agentSessionPullRequest', "Whether the current agent session item has an associated pull request: 'available' or 'none'. Unset when the pull request state is unknown.") }); export const chatSessionSupportsDelegation = new RawContextKey('chatSessionSupportsDelegation', true, { type: 'boolean', description: localize('chatSessionSupportsDelegation', "True when the current session type supports delegation.") }); export const hasPendingDelegationTarget = new RawContextKey('chatHasPendingDelegationTarget', false, { type: 'boolean', description: localize('chatHasPendingDelegationTarget', "True when a delegation (continue in) target is selected but the request has not been submitted yet.") }); export const chatSessionSupportsFork = new RawContextKey('chatSessionSupportsFork', false, { type: 'boolean', description: localize('chatSessionSupportsFork', "True when the current chat session provider supports forking conversations.") }); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionPullRequest.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionPullRequest.test.ts new file mode 100644 index 00000000000..79d1b3ecd79 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionPullRequest.test.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { getAgentSessionPullRequestContextValue, getAgentSessionPullRequestUri } from '../../../browser/agentSessions/agentSessionsModel.js'; + +suite('agentSessionPullRequest', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + function probe(metadata: { [key: string]: unknown } | undefined) { + return { + uri: getAgentSessionPullRequestUri({ metadata })?.toString(), + contextValue: getAgentSessionPullRequestContextValue({ metadata }) + }; + } + + test('resolves from pullRequestUrl, falls back to number + owner/name, otherwise none', () => { + assert.deepStrictEqual([ + probe(undefined), + probe({}), + probe({ pullRequestUrl: 'https://github.com/microsoft/vscode/pull/42' }), + probe({ pullRequestNumber: 42, owner: 'microsoft', name: 'vscode' }), + // A task-backed cloud session that has not produced a pull request. + probe({ owner: 'microsoft', name: 'vscode', branch: 'copilot/fix-1' }), + // Partial data is not enough to build a pull request url. + probe({ pullRequestNumber: 42, owner: 'microsoft' }), + // Empty owner/name would produce `https://github.com///pull/42`. + probe({ pullRequestNumber: 42, owner: '', name: '' }), + probe({ pullRequestNumber: 42, owner: 'microsoft', name: '' }), + // Non-string/number metadata must not be coerced. + probe({ pullRequestUrl: 42 }), + probe({ pullRequestNumber: '42', owner: 'microsoft', name: 'vscode' }), + ], [ + { uri: undefined, contextValue: 'none' }, + { uri: undefined, contextValue: 'none' }, + { uri: 'https://github.com/microsoft/vscode/pull/42', contextValue: 'available' }, + { uri: 'https://github.com/microsoft/vscode/pull/42', contextValue: 'available' }, + { uri: undefined, contextValue: 'none' }, + { uri: undefined, contextValue: 'none' }, + { uri: undefined, contextValue: 'none' }, + { uri: undefined, contextValue: 'none' }, + { uri: undefined, contextValue: 'none' }, + { uri: undefined, contextValue: 'none' }, + ]); + }); +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts index bdddbc140e3..26b734e60e7 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts @@ -263,7 +263,10 @@ export function registerChatFixtureServices(reg: ServiceRegistration, options: I override announceRendered() { } }()); reg.defineInstance(IChatSubmitRequestHandlerService, new ChatSubmitRequestHandlerService()); - reg.defineInstance(IAgentSessionsService, new class extends mock() { override readonly model = new class extends mock() { override readonly onDidChangeSessions = Event.None; }(); }()); + reg.defineInstance(IAgentSessionsService, new class extends mock() { + override readonly model = new class extends mock() { override readonly onDidChangeSessions = Event.None; }(); + override getSession() { return undefined; } + }()); // Agent-host chat widgets (e.g. the turn changes summary fixtures) create the // generic config chips lane, which opens a session subscription. Return an // inert, never-hydrating subscription (value `undefined`) so no config chips From c78417b762c9d244610a6851e023925e11981ed6 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 30 Jul 2026 16:31:25 -0700 Subject: [PATCH 36/86] agentHost: use fake timers for Claude mapper tests (#328297) * agentHost: use fake timers for Claude mapper tests (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: use monotonic time for tool display throttling (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../node/claude/claudeMapSessionEvents.ts | 6 +----- .../node/claude/claudeToolCallRegistry.ts | 4 +--- .../test/node/claudeMapSessionEvents.test.ts | 17 ++++++++++++----- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts b/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts index 25100d63262..cd296d52c9b 100644 --- a/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts +++ b/src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts @@ -55,13 +55,9 @@ export class ClaudeMapperState { * Public so mapper functions can call its lifecycle methods * directly without forwarding through this class. */ - readonly toolCalls: ClaudeToolCallRegistry; + readonly toolCalls = new ClaudeToolCallRegistry(); private _currentMessageId: string | undefined; - constructor(now: () => number = Date.now) { - this.toolCalls = new ClaudeToolCallRegistry(now); - } - /** * Phase 8 — file-edit content pre-staged by * `ClaudeAgentSession._observeUserMessage` and consumed by diff --git a/src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts b/src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts index 586faa83a03..0d5579a5218 100644 --- a/src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts +++ b/src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts @@ -74,8 +74,6 @@ export interface IClaudeStreamingToolInputUpdate { export class ClaudeToolCallRegistry { private readonly _entries = new Map(); - constructor(private readonly _now: () => number = Date.now) { } - /** * Begin tracking a tool call. Called from `content_block_start` * for a `tool_use` block. Allocates the delta buffer; the @@ -120,7 +118,7 @@ export class ClaudeToolCallRegistry { if (!entry || entry.displayedInputLength === entry.inputBuffer.length) { return undefined; } - const now = this._now(); + const now = performance.now(); if (!force && entry.displayedAt !== undefined && now - entry.displayedAt < STREAMING_TOOL_DISPLAY_INTERVAL_MS) { return undefined; } diff --git a/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts b/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts index 26cb15b9cc1..9700340e948 100644 --- a/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import * as sinon from 'sinon'; import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { NullLogService } from '../../../log/common/log.js'; @@ -49,6 +50,12 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { const SESSION_STR = SESSION.toString(); const SESSION_ID = 'sid-1'; const TURN_ID = 'turn-1'; + let clock: sinon.SinonFakeTimers | undefined; + + teardown(() => { + clock?.restore(); + clock = undefined; + }); /** * Captures `warn` calls so defense-in-depth tests can assert the @@ -331,9 +338,9 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { }); test('file-edit input deltas emit compact rich invocation messages', () => { + clock = sinon.useFakeTimers({ toFake: ['performance'] }); const log = new NullLogService(); - let now = 1_000; - const state = new ClaudeMapperState(() => now); + const state = new ClaudeMapperState(); const resolver = r(); mapSDKMessageToAgentSignals(makeStreamEvent(SESSION_ID, makeContentBlockStartToolUse(0, 'tu_write', 'Write')), SESSION, TURN_ID, state, log, resolver); @@ -345,7 +352,7 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { log, resolver, ); - now += STREAMING_TOOL_DISPLAY_INTERVAL_MS; + clock.tick(STREAMING_TOOL_DISPLAY_INTERVAL_MS); const second = mapSDKMessageToAgentSignals( makeStreamEvent(SESSION_ID, makeInputJsonDelta(0, '\\nthree\\nfour\\nfive"')), SESSION, @@ -382,9 +389,9 @@ suite('claudeMapSessionEvents — direct mapper tests', () => { }); test('content_block_stop flushes the final rich file-edit message held back by the throttle', () => { + clock = sinon.useFakeTimers({ toFake: ['performance'] }); const log = new NullLogService(); - const now = 1_000; - const state = new ClaudeMapperState(() => now); + const state = new ClaudeMapperState(); const resolver = r(); mapSDKMessageToAgentSignals(makeStreamEvent(SESSION_ID, makeContentBlockStartToolUse(0, 'tu_write', 'Write')), SESSION, TURN_ID, state, log, resolver); From 33375fdaffe1fb3470ea764e34c7f9be4ba9826f Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Thu, 30 Jul 2026 16:38:47 -0700 Subject: [PATCH 37/86] Automations: polish dialog controls (#328288) * Add showChevron option to session type picker, hide in automations dialog * UX nits * Polish automation dialog focus behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/AI_CUSTOMIZATIONS.md | 2 +- .../automations/browser/automationDialog.ts | 2 +- .../browser/automationDialogService.ts | 2 +- .../contrib/chat/browser/sessionTypePicker.ts | 13 ++++++++++--- .../media/aiCustomizationManagement.css | 19 ++++++++----------- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/vs/sessions/AI_CUSTOMIZATIONS.md b/src/vs/sessions/AI_CUSTOMIZATIONS.md index c68d440cd69..f37d5cf4aa6 100644 --- a/src/vs/sessions/AI_CUSTOMIZATIONS.md +++ b/src/vs/sessions/AI_CUSTOMIZATIONS.md @@ -79,7 +79,7 @@ Automation run history stores the created session as a serialized URI. Its Open Manual automation runs announce that they started once session dispatch commits, while lifecycle tracking continues until completion, failure, cancellation, or timeout. -Automations use a discriminated target that is either workspace-backed or a workspace-less quick chat. The workspace dropdown owns both choices: selecting **No workspace** switches to the existing quick-chat provider/session-type catalog, while selecting a folder restores repository configuration. Workspace-less targets display and announce as `without a workspace` in the list and cannot carry folder, isolation, or branch configuration; workspace-backed targets require a folder, with Worktree isolation requiring its base branch. Ledger schema v3 persists this target union and migrates schema-v1/v2 flat records while preserving valid workspace-backed targets. A successful authoritative CAS updates in-memory state even when restored storage resets the revision counter, while lower-revision change notifications cannot roll observables backward. +Automations use a discriminated target that is either workspace-backed or a workspace-less quick chat. The workspace dropdown owns both choices: selecting **No workspace** switches to the existing quick-chat provider/session-type catalog, while selecting a folder restores repository configuration. Workspace-less targets display and announce as `without a workspace` in the list and cannot carry folder, isolation, or branch configuration; workspace-backed targets require a folder, with Worktree isolation requiring its base branch. The automation dialog suppresses its root outline for pointer focus while preserving keyboard-visible focus indication. Ledger schema v3 persists this target union and migrates schema-v1/v2 flat records while preserving valid workspace-backed targets. A successful authoritative CAS updates in-memory state even when restored storage resets the revision counter, while lower-revision change notifications cannot roll observables backward. The Agents window contributes a built-in **Automations** client-tool set with `listAutomations`, `configureAutomation`, `runAutomation`, and `deleteAutomation`. Listing is read-only and returns stable IDs plus editable fields. Configuration uses the invoking session as the default target for new entries and follows the normal tool-approval policy: calls that require interaction show standard tool confirmation, while auto-approved calls proceed directly. Both paths validate and commit through `IAutomationService`, and successful creates and updates return a clickable chat result that opens the affected automation. `runAutomation` uses the same approval policy, starts a manual run through `IAutomationRunner` even when scheduled runs are disabled, and returns after dispatch with the run and session identifiers while lifecycle tracking continues in the background; an already-active run or unavailable target is reported without claiming a new run started. A run slot is claimed atomically: `recordRunStart` re-checks for an active run inside the same CAS that appends the pending run, so concurrent manual triggers from agents, the **Run now** button, or separate windows cannot both start the same automation, and only the caller that wins the swap dispatches a session. Manual workspace choices in the automation dialog never update the new-session recent-workspace list. Deletion uses **Delete**/**Cancel** confirmation when required, removes the automation and retained run history, and lets already-dispatched sessions continue. Denial, invalid IDs, stale confirmed updates, and cancellation or disablement observed by the mutation guard leave the ledger unchanged. The guard runs immediately before every CAS attempt; once an atomic CAS starts, concurrent cancellation or disablement cannot revoke a committed write, and the tool reports that commit as successful. diff --git a/src/vs/sessions/contrib/automations/browser/automationDialog.ts b/src/vs/sessions/contrib/automations/browser/automationDialog.ts index a0a01cc5d8f..be6e3246c54 100644 --- a/src/vs/sessions/contrib/automations/browser/automationDialog.ts +++ b/src/vs/sessions/contrib/automations/browser/automationDialog.ts @@ -774,7 +774,7 @@ export function renderForm( // The picker is authoritative for the session type const isolationModel = new AutomationIsolationModel(state); const workspaceControlsVisible = derived(reader => !isolationModel.isQuickChatObs.read(reader)); - const sessionTypePicker = disposables.add(instantiationService.createInstance(MobileSessionTypePicker, constObservable(undefined), { persistSelection: false, telemetrySource: 'AutomationSessionTypePicker' })); + const sessionTypePicker = disposables.add(instantiationService.createInstance(MobileSessionTypePicker, constObservable(undefined), { persistSelection: false, telemetrySource: 'AutomationSessionTypePicker', showChevron: false })); sessionTypePicker.setQuickChatSource(isolationModel.isQuickChatObs); sessionTypePicker.setFolderSource(isolationModel.folderUriObs, { initialPick: state.sessionTypeId diff --git a/src/vs/sessions/contrib/automations/browser/automationDialogService.ts b/src/vs/sessions/contrib/automations/browser/automationDialogService.ts index c93d3edfde1..fb1623d0b28 100644 --- a/src/vs/sessions/contrib/automations/browser/automationDialogService.ts +++ b/src/vs/sessions/contrib/automations/browser/automationDialogService.ts @@ -160,7 +160,7 @@ export class AutomationDialogService implements IAutomationDialogService { const description = DOM.append(container, $('.automation-description')); description.textContent = isEdit ? localize('automation.dialog.editDescription', "Update the schedule, prompt, or run target for this automation.") - : localize('automation.dialog.createDescription', "Define a prompt that Copilot will run on a schedule against the selected target."); + : localize('automation.dialog.createDescription', "Define a prompt that will run on a schedule against the selected target."); const formPane = DOM.append(container, $('.automation-form-pane')); const form = DOM.append(formPane, $('.automation-form')); diff --git a/src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts b/src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts index 5198a600a65..e6773dda8ff 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts @@ -73,7 +73,7 @@ const DEFAULT_TELEMETRY_SOURCE = 'NewChatSessionTypePicker'; */ export interface ISessionTypePickerOptions { /** - * When `false` (used e.g. by the automations dialog), an explicit pick is + * When `false` (e.g. the automations dialog), an explicit pick is * never written to or cleared from the profile-wide * {@link STORAGE_KEY_LAST_SESSION_TYPE} preference, so picking a type here * cannot change the New Session default. The stored preference is still read @@ -82,6 +82,11 @@ export interface ISessionTypePickerOptions { readonly persistSelection?: boolean; /** Telemetry id/name reported on selection. Defaults to {@link DEFAULT_TELEMETRY_SOURCE}. */ readonly telemetrySource?: string; + /** + * When `false`, the dropdown chevron is not rendered on the trigger. + * The picker is still interactive. Defaults to `true`. + */ + readonly showChevron?: boolean; } /** @@ -637,8 +642,10 @@ export class SessionTypePicker extends Disposable { const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label')); labelSpan.textContent = modeLabel; - const chevron = dom.append(this._triggerElement, renderIcon(Codicon.chevronDownCompact)); - chevron.classList.add('sessions-chat-dropdown-chevron'); + if (this._options?.showChevron !== false) { + const chevron = dom.append(this._triggerElement, renderIcon(Codicon.chevronDownCompact)); + chevron.classList.add('sessions-chat-dropdown-chevron'); + } this._triggerElement.ariaLabel = localize('sessionTypePicker.triggerAriaLabel', "Pick Session Type, {0}", modeLabel); } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index d120ed73aca..6c6edcf0278 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -2463,6 +2463,10 @@ pane is first mounted. View switches inside the modal are not animated. */ position: relative; } +.monaco-workbench .monaco-dialog-box.automation-dialog:focus:not(:focus-visible) { + outline: none; +} + /* * Float the close-X over the titlebar so the title text sits flush * with the top edge of the modal (QuickInput-style). Without this, @@ -2825,6 +2829,10 @@ pane is first mounted. View switches inside the modal are not animated. */ border-top: 1px solid var(--vscode-widget-border, transparent); } +.automation-form-row.automation-form-checkbox-row > .monaco-checkbox { + margin-right: 0; +} + /* * Lay the Schedule / Time / Day controls along a single horizontal axis. * Each control lives in its own `.automation-form-schedule-group` @@ -2992,10 +3000,6 @@ pane is first mounted. View switches inside the modal are not animated. */ * `.new-chat-bottom-container` chip vocabulary (see * `sessions/contrib/chat/browser/media/chatWidget.css` lines 178-209): * compact label text and codicons, icon-foreground color, no border. - * The `|` divider between the Folder chip and the branch slot - * uses the same `box-shadow: -5px 0 0 -4px var(--vscode-editorWidget-border)` - * trick the new-session row uses to draw a 1px vertical separator - * without adding a DOM element. */ .automation-form-prompt-host .chat-secondary-toolbar .automation-form-isolation-group { display: inline-flex; @@ -3062,13 +3066,6 @@ pane is first mounted. View switches inside the modal are not animated. */ white-space: nowrap; } -/* Mirror the new-session `|` divider between repo-config chips: - * `box-shadow: -5px 0 0 -4px var(--vscode-editorWidget-border)` paints - * a 1px-wide bar in the gap to the chip's left. */ -.automation-form-prompt-host .automation-form-isolation-group .automation-form-branch-picker-slot { - box-shadow: -5px 0 0 -4px var(--vscode-editorWidget-border); -} - .automation-form-prompt-host .chat-secondary-toolbar .automation-form-harness-chip { display: inline-flex; align-items: center; From dd3c36b8e940449e840322af6406ee07003bcf58 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:50:34 -0700 Subject: [PATCH 38/86] sessions: hide aquarium with new chat view (#328314) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f0569ac-d6c2-45db-8832-bed2333b0319 --- src/vs/sessions/SESSIONS.md | 6 +++- .../sessions/contrib/chat/browser/chatView.ts | 6 ++++ .../chat/test/browser/chatView.test.ts | 36 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/vs/sessions/contrib/chat/test/browser/chatView.test.ts diff --git a/src/vs/sessions/SESSIONS.md b/src/vs/sessions/SESSIONS.md index 5f5b880abee..8540a1a035c 100644 --- a/src/vs/sessions/SESSIONS.md +++ b/src/vs/sessions/SESSIONS.md @@ -337,7 +337,11 @@ visibility preference; `IChatPetService` owns the same persisted pet state used by `/vscode-pet`. Context-menu events from inside `.new-chat-widget-content` are left untouched so the composer retains its own context-menu behavior. The aquarium preference is also keyboard-accessible through the **Developer: Toggle -Aquarium Action Visibility** command. +Aquarium Action Visibility** command. `NewChatView` forwards its effective grid +visibility to the aquarium mount so a hidden composer cannot leave the aquarium +rendering behind the visible chat surface. Since `NewChatView` also hosts the +peer-chat composer, aquarium-specific lifecycle calls must first narrow the +wrapped widget to `NewChatWidget`. Agent feedback created while the active session is undefined or uncreated uses one shared new-session feedback scope, so it follows every undefined/uncreated diff --git a/src/vs/sessions/contrib/chat/browser/chatView.ts b/src/vs/sessions/contrib/chat/browser/chatView.ts index c0ba25b3311..e9d95f1715f 100644 --- a/src/vs/sessions/contrib/chat/browser/chatView.ts +++ b/src/vs/sessions/contrib/chat/browser/chatView.ts @@ -105,6 +105,12 @@ export class NewChatView extends AbstractChatView { override attach(uris: URI[]): void { this._widget.attach(uris); } + + override setVisible(visible: boolean): void { + if (this._widget instanceof NewChatWidget) { + this._widget.setHostVisible(visible); + } + } } /** diff --git a/src/vs/sessions/contrib/chat/test/browser/chatView.test.ts b/src/vs/sessions/contrib/chat/test/browser/chatView.test.ts new file mode 100644 index 00000000000..325ea1eeb58 --- /dev/null +++ b/src/vs/sessions/contrib/chat/test/browser/chatView.test.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { NewChatView } from '../../browser/chatView.js'; +import { NewChatInSessionWidget } from '../../browser/newChatInSessionWidget.js'; +import { NewChatWidget } from '../../browser/newChatWidget.js'; + +suite('Sessions - Chat View', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('forwards new chat visibility to the aquarium host', () => { + const forwarded: boolean[] = []; + const view: NewChatView = Object.assign(Object.create(NewChatView.prototype), { + _widget: Object.assign(Object.create(NewChatWidget.prototype), { + setHostVisible: (visible: boolean) => forwarded.push(visible), + }), + }); + + view.setVisible(false); + view.setVisible(true); + + assert.deepStrictEqual(forwarded, [false, true]); + }); + + test('does not forward aquarium visibility to the peer chat composer', () => { + const view: NewChatView = Object.assign(Object.create(NewChatView.prototype), { + _widget: Object.create(NewChatInSessionWidget.prototype), + }); + + assert.doesNotThrow(() => view.setVisible(false)); + }); +}); From c886f586fcaab69fface1ad4648e05def0360457 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Thu, 30 Jul 2026 17:33:22 -0700 Subject: [PATCH 39/86] fix: memory leak in decorationAddon._decorations (#326933) * fix: dispose terminal decoration resources * test: cover terminal decoration disposal --------- Co-authored-by: Dmitriy Vasyura --- .../terminal/browser/xterm/decorationAddon.ts | 8 ++- .../browser/xterm/decorationAddon.test.ts | 49 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index 68e2c80e2ca..84bbf8c0cdd 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -311,7 +311,13 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco return; } if (!this._decorations.get(decoration.marker.id)) { - decoration.onDispose(() => this._decorations.delete(decoration.marker.id)); + decoration.onDispose(() => { + const disposableDecoration = this._decorations.get(decoration.marker.id); + if (disposableDecoration) { + dispose(disposableDecoration.disposables); + this._decorations.delete(decoration.marker.id); + } + }); this._decorations.set(decoration.marker.id, { decoration, diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts index 62d3ec2455e..f40f8a88486 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import type { IDecoration, IDecorationOptions, Terminal as RawXtermTerminal } from '@xterm/xterm'; -import { notEqual, strictEqual, throws } from 'assert'; +import { deepStrictEqual, notEqual, strictEqual, throws } from 'assert'; import { importAMDNodeModule } from '../../../../../../amdX.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { ITerminalCommand, TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { CommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js'; import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; @@ -20,8 +21,12 @@ suite('DecorationAddon', () => { let decorationAddon: DecorationAddon; let xterm: RawXtermTerminal; + let hoverDisposed: boolean; + let removedEventListeners: string[]; setup(async () => { + hoverDisposed = false; + removedEventListeners = []; const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; class TestTerminal extends TerminalCtor { override registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { @@ -29,7 +34,33 @@ suite('DecorationAddon', () => { return undefined; } const element = document.createElement('div'); - return { marker: decorationOptions.marker, element, onDispose: () => { }, isDisposed: false, dispose: () => { }, onRender: (element: HTMLElement) => { return element; } } as unknown as IDecoration; + const removeEventListener = element.removeEventListener.bind(element); + element.removeEventListener = ((...args: Parameters) => { + removedEventListeners.push(args[0]); + removeEventListener(...args); + }) as typeof element.removeEventListener; + const disposeListeners = new Set<() => void>(); + let isDisposed = false; + return { + marker: decorationOptions.marker, + element, + onDispose: (listener: () => void) => { + disposeListeners.add(listener); + return { dispose: () => disposeListeners.delete(listener) }; + }, + get isDisposed() { return isDisposed; }, + dispose: () => { + isDisposed = true; + for (const listener of disposeListeners) { + listener(); + } + disposeListeners.clear(); + }, + onRender: (listener: (element: HTMLElement) => void) => { + listener(element); + return { dispose: () => { } }; + } + } as unknown as IDecoration; } } @@ -48,6 +79,9 @@ suite('DecorationAddon', () => { } }) }, store); + instantiationService.stub(IHoverService, { + setupDelayedHover: () => ({ dispose: () => hoverDisposed = true }) + } as unknown as IHoverService); xterm = store.add(new TestTerminal({ allowProposedApi: true, cols: 80, @@ -77,5 +111,16 @@ suite('DecorationAddon', () => { const marker = xterm.registerMarker(2); notEqual(decorationAddon.registerCommandDecoration(undefined, undefined, { marker }), undefined); }); + test('should dispose decoration resources when the decoration is disposed', () => { + const marker = xterm.registerMarker(2)!; + const decoration = decorationAddon.registerCommandDecoration({ command: 'cd src', marker, exitCode: 0, timestamp: Date.now(), hasOutput: () => false } as ITerminalCommand)!; + const decorations = (decorationAddon as unknown as { _decorations: Map })._decorations; + + decoration.dispose(); + + strictEqual(hoverDisposed, true); + deepStrictEqual(removedEventListeners.sort(), ['click', 'contextmenu', 'mousedown']); + strictEqual(decorations.has(marker.id), false); + }); }); }); From ca5a7f94edb6d84123070a9b499e3fb935c21883 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 30 Jul 2026 17:40:17 -0700 Subject: [PATCH 40/86] copilot: preserve Gemini BYOK tool fidelity Normalize unsupported non-string Gemini enums and restore thought signatures when replaying historical function calls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../geminiFunctionDeclarationConverter.ts | 9 +++-- .../byok/common/geminiMessageConverter.ts | 7 ++-- ...geminiFunctionDeclarationConverter.spec.ts | 35 +++++++++++++++++++ .../test/geminiMessageConverter.spec.ts | 25 +++++++++++-- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/extensions/copilot/src/extension/byok/common/geminiFunctionDeclarationConverter.ts b/extensions/copilot/src/extension/byok/common/geminiFunctionDeclarationConverter.ts index f25e50d3ee5..990537ed9a0 100644 --- a/extensions/copilot/src/extension/byok/common/geminiFunctionDeclarationConverter.ts +++ b/extensions/copilot/src/extension/byok/common/geminiFunctionDeclarationConverter.ts @@ -11,7 +11,7 @@ export type ToolJsonSchema = { properties?: Record; items?: ToolJsonSchema; required?: string[]; - enum?: string[]; + enum?: unknown[]; // Add support for JSON Schema composition keywords anyOf?: ToolJsonSchema[]; @@ -104,8 +104,11 @@ function transformConcrete(schema: ToolJsonSchema): Schema { transformed.description = schema.description; } - if (schema.enum) { - transformed.enum = schema.enum; + if (type === 'string' && schema.enum) { + const values = schema.enum.filter((value): value is string => typeof value === 'string'); + if (values.length > 0) { + transformed.enum = values; + } } if (type === 'object' && schema.properties) { diff --git a/extensions/copilot/src/extension/byok/common/geminiMessageConverter.ts b/extensions/copilot/src/extension/byok/common/geminiMessageConverter.ts index 2a8179c1a95..56b0db40d46 100644 --- a/extensions/copilot/src/extension/byok/common/geminiMessageConverter.ts +++ b/extensions/copilot/src/extension/byok/common/geminiMessageConverter.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { Content, FunctionCall, FunctionResponse, Part } from '@google/genai'; import { Raw } from '@vscode/prompt-tsx'; -import type { LanguageModelChatMessage } from 'vscode'; +import type { LanguageModelChatMessage, LanguageModelChatMessage2 } from 'vscode'; import { CustomDataPartMimeTypes } from '../../../platform/endpoint/common/endpointTypes'; import { LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart, LanguageModelToolResultPart2 } from '../../../vscodeTypes'; @@ -120,7 +120,7 @@ function apiContentToGeminiContent(content: (LanguageModelTextPart | LanguageMod return convertedContent; } -export function apiMessageToGeminiMessage(messages: LanguageModelChatMessage[]): { contents: Content[]; systemInstruction?: Content } { +export function apiMessageToGeminiMessage(messages: Array): { contents: Content[]; systemInstruction?: Content } { const contents: Content[] = []; let systemInstruction: Content | undefined; @@ -131,8 +131,7 @@ export function apiMessageToGeminiMessage(messages: LanguageModelChatMessage[]): if (message.role === LanguageModelChatMessageRole.System) { // Gemini uses system instruction separately const systemText = message.content - .filter((p): p is LanguageModelTextPart => p instanceof LanguageModelTextPart) - .map(p => p.value) + .map(part => part instanceof LanguageModelTextPart ? part.value : '') .join(''); if (systemText.trim()) { diff --git a/extensions/copilot/src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts b/extensions/copilot/src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts index 986c6a51749..80b64f6d4d2 100644 --- a/extensions/copilot/src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts +++ b/extensions/copilot/src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts @@ -179,6 +179,41 @@ describe('GeminiFunctionDeclarationConverter', () => { }); }); + it('should omit non-string enums from nested schemas', () => { + const result = toGeminiFunction('nestedEnumFunction', 'Function with nested non-string enums', { + type: 'object', + properties: { + values: { + type: 'array', + items: { + type: 'object', + properties: { + enabled: { + type: 'boolean', + enum: [true] + }, + count: { + type: 'integer', + enum: [1, 2] + } + } + } + } + } + }); + + expect(result.parameters!.properties!['values']).toEqual({ + type: Type.ARRAY, + items: { + type: Type.OBJECT, + properties: { + enabled: { type: Type.BOOLEAN }, + count: { type: Type.INTEGER } + } + } + }); + }); + it('should handle nullable anyOf schemas', () => { const result = toGeminiFunction('nullableAnyOfFunction', 'Function with nullable anyOf', { type: 'object', diff --git a/extensions/copilot/src/extension/byok/common/test/geminiMessageConverter.spec.ts b/extensions/copilot/src/extension/byok/common/test/geminiMessageConverter.spec.ts index 68e36160eb4..1d02c638f2e 100644 --- a/extensions/copilot/src/extension/byok/common/test/geminiMessageConverter.spec.ts +++ b/extensions/copilot/src/extension/byok/common/test/geminiMessageConverter.spec.ts @@ -5,9 +5,9 @@ import { Raw } from '@vscode/prompt-tsx'; import { describe, expect, it } from 'vitest'; -import type { LanguageModelChatMessage } from 'vscode'; +import type { LanguageModelChatMessage, LanguageModelChatMessage2 } from 'vscode'; import { CustomDataPartMimeTypes } from '../../../../platform/endpoint/common/endpointTypes'; -import { LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelTextPart, LanguageModelToolResultPart, LanguageModelTextPart as LMText } from '../../../../vscodeTypes'; +import { LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart, LanguageModelTextPart as LMText } from '../../../../vscodeTypes'; import { apiMessageToGeminiMessage } from '../geminiMessageConverter'; describe('GeminiMessageConverter', () => { @@ -80,6 +80,27 @@ describe('GeminiMessageConverter', () => { expect(result.contents[0].parts![1].text).toBe('Hello!'); }); + it('should attach a thought signature to the following function call', () => { + const messages: Array = [{ + role: LanguageModelChatMessageRole.Assistant, + content: [ + new LanguageModelThinkingPart('', undefined, { signature: 'thought-signature' }), + new LanguageModelToolCallPart('call-1', 'default_api:view', { path: 'README.md' }), + ], + name: undefined, + }]; + + const result = apiMessageToGeminiMessage(messages); + + expect(result.contents[0].parts).toEqual([{ + functionCall: { + name: 'default_api:view', + args: { path: 'README.md' }, + }, + thoughtSignature: 'thought-signature', + }]); + }); + it('should extract functionResponse parts from model message into subsequent user message and prune empty model', () => { // Simulate a model message that (incorrectly) contains only a tool result part const toolResult = new LanguageModelToolResultPart('myTool_12345', [new LanguageModelTextPart('{"foo":"bar"}')]); From 7bee61ce63133facae863f295ade81f0be711455 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:41:59 -0700 Subject: [PATCH 41/86] Dynamically resize and reflow inline chat terminal output (#328313) * Resize chat terminal output mirror to available content width Reflow detached terminal command/snapshot mirrors when the expanded output container width is known, and cover the column math with unit tests. * Fix mirror layout line count and window DPR after content-width resize Use the mirror terminal window DPR and rendered row count after resize so box height and column math stay correct across windows and reflow. * Prefer rendered row count and measured padding for mirror layout Only trust a persisted snapshot lineCount for truncated output, and measure the mirror's horizontal chrome from computed styles like the panel terminal does, keeping the 24px estimate as a pre-attach fallback. * Keep truncated snapshot height stable and measure the terminal gutter Snapshot layout now applies the same truncated lineCount rule as render so an explicit count survives resizes, the pre-attach chrome estimate matches the 20px workbench xterm gutter, resize errors are routed to onUnexpectedError, and the live mirror re-checks cols after waiting for an in-flight flush. * Fire and forget the resize relayout like other terminal code --- .../chatTerminalToolProgressPart.ts | 25 ++ .../browser/chatTerminalCommandMirror.ts | 164 +++++++- .../browser/chatTerminalCommandMirror.test.ts | 355 +++++++++++++++++- 3 files changed, 541 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 6e7c94829e5..2b51cbadcc2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -1401,6 +1401,7 @@ class ChatTerminalToolOutputSection extends Disposable { // Only now show the expanded state (after content is ready) this._setExpanded(true); + await this._layoutMirrorWidth(); this._layoutOutput(); this._scrollOutputToBottom(); this._scheduleOutputRelayout(); @@ -1585,6 +1586,7 @@ class ChatTerminalToolOutputSection extends Disposable { } })); await mirror.attach(this._terminalContainer); + await this._layoutMirrorWidth(mirror); let result = await mirror.renderCommand(); // Only show "No output" message if: // 1. Command has finished (has endMarker), AND @@ -1628,6 +1630,7 @@ class ChatTerminalToolOutputSection extends Disposable { private async _renderSnapshotOutput(snapshot: NonNullable): Promise { if (this._snapshotMirror) { this._snapshotMirror.setOutput(snapshot); + await this._layoutMirrorWidth(this._snapshotMirror); const result = await this._snapshotMirror.render(); this._layoutOutput(result?.lineCount ?? snapshot.lineCount ?? this._lastRenderedLineCount ?? 0); return; @@ -1639,6 +1642,7 @@ class ChatTerminalToolOutputSection extends Disposable { this._snapshotMirror = this._register(this._instantiationService.createInstance(DetachedTerminalSnapshotMirror, snapshot, this._getStoredTheme)); await this._snapshotMirror.attach(this._terminalContainer); this._snapshotMirror.setOutput(snapshot); + await this._layoutMirrorWidth(this._snapshotMirror); const result = await this._snapshotMirror.render(); const hasText = !!snapshot.text && snapshot.text.length > 0; if (hasText) { @@ -1696,6 +1700,7 @@ class ChatTerminalToolOutputSection extends Disposable { return; } if (this.isExpanded) { + void this._layoutMirrorWidth(); this._layoutOutput(); this._scrollOutputToBottom(); } else { @@ -1703,6 +1708,26 @@ class ChatTerminalToolOutputSection extends Disposable { } } + /** + * Resizes the mirror's column count to fill the currently available width. No-op while the + * width is unmeasurable (e.g. collapsed); the mirror keeps its current cols until the next + * layout opportunity. + */ + private async _layoutMirrorWidth(mirror: DetachedTerminalCommandMirror | DetachedTerminalSnapshotMirror | undefined = this._snapshotMirror ?? this._mirror): Promise { + if (!mirror) { + return; + } + const width = this._terminalContainer.clientWidth || this._outputBody.clientWidth || this.domNode.clientWidth || (this.domNode.parentElement?.clientWidth ?? 0); + if (width <= 0) { + return; + } + const result = await mirror.layout(width); + if (!this._store.isDisposed && result?.lineCount !== undefined) { + // Re-wrapping can change the number of rendered rows, so refresh the box height + this._layoutOutput(result.lineCount); + } + } + private _layoutOutput(lineCount?: number): void { if (!this._scrollableContainer) { return; diff --git a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts index 327e9f6e2b6..5515eb691ab 100644 --- a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts +++ b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getWindow } from '../../../../base/browser/dom.js'; import { Sequencer } from '../../../../base/common/async.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import type { IMarker as IXtermMarker, Terminal as RawXtermTerminal } from '@xterm/xterm'; import type { ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js'; -import { ITerminalService, type IDetachedTerminalInstance } from './terminal.js'; +import { ITerminalService, type IDetachedTerminalInstance, type IDetachedXtermTerminal } from './terminal.js'; import { DetachedProcessInfo } from './detachedTerminal.js'; import { XtermTerminal } from './xterm/xtermTerminal.js'; import { TERMINAL_BACKGROUND_COLOR } from '../common/terminalColorRegistry.js'; @@ -21,6 +22,7 @@ import { Color } from '../../../../base/common/color.js'; import type { IChatTerminalToolInvocationData } from '../../chat/common/chatService/chatService.js'; import type { IColorTheme } from '../../../../platform/theme/common/themeService.js'; import { ICurrentPartialCommand } from '../../../../platform/terminal/common/capabilities/commandDetection/terminalCommand.js'; +import type { ITerminalFont } from '../common/terminal.js'; function getChatTerminalBackgroundColor(theme: IColorTheme, contextKeyService: IContextKeyService, storedBackground?: string): Color | undefined { if (storedBackground) { @@ -99,6 +101,7 @@ export interface IDetachedTerminalCommandMirrorRenderResult { interface IDetachedTerminalCommandMirror { attach(container: HTMLElement): Promise; renderCommand(): Promise; + layout(widthPx: number): Promise; onDidUpdate: Event; onDidInput: Event; } @@ -106,6 +109,12 @@ interface IDetachedTerminalCommandMirror { const enum ChatTerminalMirrorMetrics { MirrorRowCount = 10, MirrorColCountFallback = 80, + /** + * Pre-attach estimate of the horizontal space the mirror content cannot use: the gutter + * every workbench xterm gets via `.monaco-workbench .xterm { padding-left: 20px }` + * (terminal.css). Once attached, the real value is measured from computed styles. + */ + MirrorHorizontalPaddingPx = 20, /** * Maximum number of lines for which we compute the max column width. * Computing max column width iterates the entire buffer, so we skip it @@ -114,6 +123,64 @@ const enum ChatTerminalMirrorMetrics { MaxLinesForColumnWidthComputation = 100 } +/** + * Computes the number of columns a chat terminal mirror should use to fill the available width + * of its container, using the same cell math as {@link getXtermScaledDimensions}. + * + * @param availableWidthPx The container width in CSS pixels. + * @param font The terminal font with measured char metrics. + * @param devicePixelRatio The window's device pixel ratio. + * @param horizontalChromePx Horizontal space the DOM chrome takes from the container width, + * measured from computed styles when available; defaults to the static estimate. + * @returns The column count, or the default fallback when the width or font is unmeasurable. + */ +export function computeChatTerminalMirrorCols(availableWidthPx: number, font: ITerminalFont, devicePixelRatio: number, horizontalChromePx: number = ChatTerminalMirrorMetrics.MirrorHorizontalPaddingPx): number { + if (!isFinite(availableWidthPx) || availableWidthPx <= 0 || !font.charWidth) { + return ChatTerminalMirrorMetrics.MirrorColCountFallback; + } + const dpr = isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1; + const scaledWidthAvailable = (availableWidthPx - horizontalChromePx) * dpr; + const scaledCharWidth = font.charWidth * dpr + font.letterSpacing; + return Math.max(Math.floor(scaledWidthAvailable / scaledCharWidth), 1); +} + +function getMirrorRaw(detached: IDetachedTerminalInstance): RawXtermTerminal { + return (detached.xterm as IDetachedXtermTerminal & { raw: RawXtermTerminal }).raw; +} + +/** + * Enables cursor line reflow on a mirror's terminal. The mirror is a readonly output preview + * with no prompt line to protect, so resize reflow should re-wrap the cursor line like any + * other line (xterm skips it by default). + */ +function enableCursorLineReflow(detached: IDetachedTerminalInstance): void { + getMirrorRaw(detached).options.reflowCursorLine = true; +} + +/** + * Gets the device pixel ratio of the window the mirror's terminal is rendered in, so cell + * math stays correct in auxiliary windows on monitors with different scaling. + */ +function getMirrorDevicePixelRatio(detached: IDetachedTerminalInstance): number { + return getWindow(getMirrorRaw(detached).element).devicePixelRatio; +} + +/** + * Measures the horizontal space the mirror's DOM chrome takes from the container width by + * reading the xterm element's computed padding, the same way the panel terminal does. xterm's + * own scrollbar is hidden in the chat preview, so unlike the panel terminal it takes no + * space. Returns undefined before the terminal is attached. + */ +function measureMirrorHorizontalChrome(detached: IDetachedTerminalInstance): number | undefined { + const element = getMirrorRaw(detached).element; + if (!element) { + return undefined; + } + const style = getWindow(element).getComputedStyle(element); + const chrome = parseInt(style.paddingLeft) + parseInt(style.paddingRight); + return isNaN(chrome) ? undefined : Math.max(chrome, 0); +} + /** * Computes the line count for terminal output between start and end lines. * The end line is exclusive (points to the line after output ends). @@ -367,6 +434,51 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach return { lineCount: this._lineCount, maxColumnWidth: this._maxColumnWidth }; } + /** + * Resizes the mirror to fill the given width, relying on xterm's native resize reflow to + * re-wrap soft-wrapped lines. No-op when the resulting cols are unchanged. The column + * count derives from the mirror's own xterm font metrics, which reflect the actual + * renderer cell size rather than a configuration-based estimate. + */ + async layout(widthPx: number): Promise { + if (this._store.isDisposed || widthPx <= 0) { + return undefined; + } + let detached: IDetachedTerminalInstance; + try { + detached = await this._getOrCreateTerminal(); + } catch (error) { + if (error instanceof CancellationError) { + return undefined; + } + throw error; + } + if (this._store.isDisposed) { + return undefined; + } + const cols = computeChatTerminalMirrorCols(widthPx, detached.xterm.getFont(), getMirrorDevicePixelRatio(detached), measureMirrorHorizontalChrome(detached)); + if (detached.xterm.cols === cols) { + return undefined; + } + // Wait for any in-flight streaming flush so the resize does not interleave with it + await this._flushPromise; + if (this._store.isDisposed || detached.xterm.cols === cols) { + return undefined; + } + // Native resize reflow re-wraps the buffer in place; rewriting the cached VT here + // instead would flash a cleared frame on every resize + detached.xterm.resize(cols, ChatTerminalMirrorMetrics.MirrorRowCount); + if (!this._lastVT) { + return undefined; + } + this._lineCount = this._getRenderedLineCount(); + const commandFinished = this._command.endMarker && !this._command.endMarker.isDisposed; + if (commandFinished && this._lineCount <= ChatTerminalMirrorMetrics.MaxLinesForColumnWidthComputation) { + this._maxColumnWidth = this._computeMaxColumnWidth(); + } + return { lineCount: this._lineCount, maxColumnWidth: this._maxColumnWidth }; + } + private async _getCommandOutputAsVT(source: XtermTerminal): Promise<{ text: string } | undefined> { if (this._store.isDisposed) { return undefined; @@ -389,6 +501,13 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach } private _getRenderedLineCount(): number { + // Prefer counting the mirror's own rendered rows: they reflect the mirror's column + // count, which can differ from the source terminal's after a width layout + const detachedBuffer = this._detachedTerminal?.xterm.buffer.active; + if (detachedBuffer) { + return computeSnapshotLineCount(detachedBuffer); + } + // Calculate line count from the command's markers when available const endMarker = this._command.endMarker; if (this._command.executedMarker && endMarker && !endMarker.isDisposed) { @@ -444,6 +563,7 @@ export class DetachedTerminalCommandMirror extends Disposable implements IDetach detached.dispose(); throw new CancellationError(); } + enableCursorLineReflow(detached); this._detachedTerminal = detached; this._register(processInfo); this._register(detached); @@ -650,6 +770,7 @@ export class DetachedTerminalSnapshotMirror extends Disposable { terminal.dispose(); return terminal; } + enableCursorLineReflow(terminal); return this._register(terminal); }); } @@ -686,6 +807,42 @@ export class DetachedTerminalSnapshotMirror extends Disposable { return this._renderSequencer.queue(() => this._render()); } + /** + * Resizes the mirror to fill the given width, relying on xterm's native resize reflow to + * re-wrap soft-wrapped lines. No-op when the resulting cols are unchanged. The column + * count derives from the mirror's own xterm font metrics, which reflect the actual + * renderer cell size rather than a configuration-based estimate. + */ + public async layout(widthPx: number): Promise<{ lineCount?: number; maxColumnWidth?: number } | undefined> { + if (widthPx <= 0) { + return undefined; + } + return this._renderSequencer.queue(async () => { + const terminal = await this._getTerminal(); + if (this._store.isDisposed) { + return undefined; + } + const cols = computeChatTerminalMirrorCols(widthPx, terminal.xterm.getFont(), getMirrorDevicePixelRatio(terminal), measureMirrorHorizontalChrome(terminal)); + if (terminal.xterm.cols === cols) { + return undefined; + } + // Native resize reflow re-wraps the rendered content in place; rewriting the + // snapshot here instead would flash a cleared frame on every resize + terminal.xterm.resize(cols, ChatTerminalMirrorMetrics.MirrorRowCount); + if (!this._lastRenderedText) { + return undefined; + } + // Same rule as _render: a truncated snapshot's buffer under-represents the real + // output, so its explicit lineCount must survive the resize + const lineCount = computeSnapshotLineCount(terminal.xterm.buffer.active, this._output?.truncated ? this._output.lineCount : undefined); + this._lastRenderedLineCount = lineCount; + if (this._shouldComputeMaxColumnWidth(lineCount)) { + this._lastRenderedMaxColumnWidth = this._computeMaxColumnWidth(terminal); + } + return { lineCount, maxColumnWidth: this._lastRenderedMaxColumnWidth }; + }); + } + private async _render(): Promise<{ lineCount?: number; maxColumnWidth?: number } | undefined> { const output = this._output; const outputVersion = this._outputVersion; @@ -723,7 +880,10 @@ export class DetachedTerminalSnapshotMirror extends Disposable { if (this._store.isDisposed) { return undefined; } - const lineCount = computeSnapshotLineCount(terminal.xterm.buffer.active, output.lineCount); + // A persisted lineCount reflects the wrap width of the source terminal, which can differ + // from this mirror's cols after a width layout. Only trust it for truncated output, + // where the text under-represents the real row count. + const lineCount = computeSnapshotLineCount(terminal.xterm.buffer.active, output.truncated ? output.lineCount : undefined); this._renderedVersion = outputVersion; this._lastRenderedText = text; this._lastRenderedLineCount = lineCount; diff --git a/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts b/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts index 07bed4582c5..1340e363b54 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts @@ -6,15 +6,19 @@ import type { Terminal } from '@xterm/xterm'; import { deepStrictEqual, strictEqual } from 'assert'; import { importAMDNodeModule } from '../../../../../amdX.js'; +import { Event } from '../../../../../base/common/event.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import type { IEditorOptions } from '../../../../../editor/common/config/editorOptions.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import type { ITerminalCommand } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { TerminalCapabilityStore } from '../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; +import type { ITerminalFont } from '../../common/terminal.js'; +import { ITerminalService, type IDetachedTerminalInstance, type IDetachedXTermOptions } from '../../browser/terminal.js'; import { XtermTerminal } from '../../browser/xterm/xtermTerminal.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { TestXtermAddonImporter } from './xterm/xtermTestUtils.js'; -import { computeMaxBufferColumnWidth, computeSnapshotLineCount, vtBoundaryMatches } from '../../browser/chatTerminalCommandMirror.js'; +import { computeChatTerminalMirrorCols, computeMaxBufferColumnWidth, computeSnapshotLineCount, DetachedTerminalCommandMirror, DetachedTerminalSnapshotMirror, vtBoundaryMatches } from '../../browser/chatTerminalCommandMirror.js'; const defaultTerminalConfig = { fontFamily: 'monospace', @@ -27,6 +31,38 @@ const defaultTerminalConfig = { unicodeVersion: '6' }; +/** + * Creates a fake detached terminal instance backed by a real raw xterm.js terminal so mirror + * tests can inspect the resulting buffer and count resize/write calls. The fixed font metrics + * (charWidth 10, letterSpacing 0) make width-to-cols math deterministic on any machine. + */ +function createFakeDetachedTerminal(RawCtor: typeof Terminal, options: IDetachedXTermOptions) { + const raw = new RawCtor({ cols: options.cols, rows: options.rows }); + const counters = { resizeCalls: 0, writeCalls: 0 }; + const font: ITerminalFont = { fontFamily: 'monospace', fontSize: 12, letterSpacing: 0, lineHeight: 1, charWidth: 10, charHeight: 14 }; + const instance = { + xterm: { + raw, + get cols() { return raw.cols; }, + get rows() { return raw.rows; }, + get buffer() { return raw.buffer; }, + getFont: () => font, + write: (data: string, callback?: () => void) => { + counters.writeCalls++; + raw.write(data, callback); + }, + resize: (columns: number, rows: number) => { + counters.resizeCalls++; + raw.resize(columns, rows); + } + }, + onData: Event.None, + attachToElement: () => { }, + dispose: () => raw.dispose() + } as unknown as IDetachedTerminalInstance; + return { raw, counters, instance }; +} + suite('Workbench - ChatTerminalCommandMirror', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -619,4 +655,321 @@ suite('Workbench - ChatTerminalCommandMirror', () => { strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), true); }); }); + + suite('computeChatTerminalMirrorCols', () => { + + function makeFont(charWidth?: number, letterSpacing = 0): ITerminalFont { + return { fontFamily: 'monospace', fontSize: 12, letterSpacing, lineHeight: 1, charWidth, charHeight: 14 }; + } + + test('fills the available width minus the gutter', () => { + deepStrictEqual({ + wide: computeChatTerminalMirrorCols(1224, makeFont(10), 1), + floored: computeChatTerminalMirrorCols(1200, makeFont(10), 1), + }, { + wide: 120, // floor((1224 - 20) / 10) + floored: 118, // (1200 - 20) / 10 + }); + }); + + test('is stable across device pixel ratios when letter spacing is zero', () => { + strictEqual(computeChatTerminalMirrorCols(1224, makeFont(10), 2), 120); + }); + + test('accounts for letter spacing in device pixels', () => { + // floor((1224 - 24) * 2 / (10 * 2 + 1)) + strictEqual(computeChatTerminalMirrorCols(1224, makeFont(10, 1), 2), 114); + }); + + test('falls back to the default cols when width or font is unmeasurable', () => { + deepStrictEqual({ + zeroWidth: computeChatTerminalMirrorCols(0, makeFont(10), 1), + nanWidth: computeChatTerminalMirrorCols(NaN, makeFont(10), 1), + missingCharWidth: computeChatTerminalMirrorCols(1224, makeFont(undefined), 1), + zeroCharWidth: computeChatTerminalMirrorCols(1224, makeFont(0), 1), + }, { + zeroWidth: 80, + nanWidth: 80, + missingCharWidth: 80, + zeroCharWidth: 80, + }); + }); + + test('treats an invalid device pixel ratio as 1', () => { + strictEqual(computeChatTerminalMirrorCols(1224, makeFont(10), 0), 120); + }); + + test('uses an explicitly measured horizontal chrome over the default', () => { + deepStrictEqual({ + none: computeChatTerminalMirrorCols(1200, makeFont(10), 1, 0), + measured: computeChatTerminalMirrorCols(1224, makeFont(10), 1, 24), + }, { + none: 120, + measured: 120, + }); + }); + + test('narrow widths wrap to the fitting column count, minimum one column', () => { + deepStrictEqual({ + narrow: computeChatTerminalMirrorCols(100, makeFont(10), 1), // (100 - 20) / 10 + tiny: computeChatTerminalMirrorCols(25, makeFont(10), 1), + }, { + narrow: 8, + tiny: 1, + }); + }); + }); + + suite('DetachedTerminalSnapshotMirror.layout', () => { + let instantiationService: TestInstantiationService; + let XTermBaseCtor: typeof Terminal; + let fakes: ReturnType[]; + + setup(async () => { + instantiationService = workbenchInstantiationService(undefined, store); + XTermBaseCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; + fakes = []; + instantiationService.stub(ITerminalService, { + createDetachedTerminal: async (options: IDetachedXTermOptions) => { + const fake = createFakeDetachedTerminal(XTermBaseCtor, options); + fakes.push(fake); + return fake.instance; + } + } as Partial); + }); + + function createSnapshotMirror(output: { text: string; truncated?: boolean; lineCount?: number } | undefined): DetachedTerminalSnapshotMirror { + return store.add(instantiationService.createInstance(DetachedTerminalSnapshotMirror, output, () => undefined)); + } + + test('resizes the detached terminal to cols computed from the width', async () => { + const mirror = createSnapshotMirror({ text: 'hello' }); + await mirror.layout(1224); // floor((1224 - 20) / 10) = 120 cols + strictEqual(fakes.length, 1); + strictEqual(fakes[0].raw.cols, 120); + }); + + test('first render after layout wraps at the new cols', async () => { + const mirror = createSnapshotMirror({ text: 'x'.repeat(100) }); + await mirror.layout(1224); + await mirror.render(); + deepStrictEqual({ + cols: fakes[0].raw.cols, + lineCount: computeSnapshotLineCount(fakes[0].raw.buffer.active), + maxColumnWidth: computeMaxBufferColumnWidth(fakes[0].raw.buffer.active, fakes[0].raw.cols), + }, { + cols: 120, + lineCount: 1, + maxColumnWidth: 100, + }); + }); + + test('re-wraps already rendered output at the new cols without rewriting', async () => { + const mirror = createSnapshotMirror({ text: 'x'.repeat(100) }); + await mirror.render(); + strictEqual(computeSnapshotLineCount(fakes[0].raw.buffer.active), 2); + const writeCallsBeforeLayout = fakes[0].counters.writeCalls; + await mirror.layout(1224); + deepStrictEqual({ + cols: fakes[0].raw.cols, + lineCount: computeSnapshotLineCount(fakes[0].raw.buffer.active), + maxColumnWidth: computeMaxBufferColumnWidth(fakes[0].raw.buffer.active, fakes[0].raw.cols), + // Re-wrapping must come from xterm's native resize reflow, not a buffer + // rewrite, which would flash a cleared frame on every resize + writeCalls: fakes[0].counters.writeCalls, + }, { + cols: 120, + lineCount: 1, + maxColumnWidth: 100, + writeCalls: writeCallsBeforeLayout, + }); + }); + + test('repeated layout with the same width does not resize or rewrite', async () => { + const mirror = createSnapshotMirror({ text: 'x'.repeat(100) }); + await mirror.render(); + await mirror.layout(1224); + const { resizeCalls, writeCalls } = { ...fakes[0].counters }; + await mirror.layout(1224); + deepStrictEqual(fakes[0].counters, { resizeCalls, writeCalls }); + }); + + test('ignores non-positive widths', async () => { + const mirror = createSnapshotMirror({ text: 'hello' }); + await mirror.layout(0); + await mirror.layout(-10); + strictEqual(fakes[0].raw.cols, 80); + }); + + test('drops a persisted lineCount that reflects the old wrap width', async () => { + // Producers persist lineCount wrapped at the source terminal's cols; after a + // width layout the rendered row count is the ground truth for the box height + const mirror = createSnapshotMirror({ text: 'x'.repeat(100), lineCount: 2 }); + await mirror.layout(1224); + const result = await mirror.render(); + strictEqual(result?.lineCount, 1); + }); + + test('keeps an explicit lineCount for truncated output', async () => { + // Truncated snapshots under-represent the real output, so the persisted count wins + const mirror = createSnapshotMirror({ text: 'short', truncated: true, lineCount: 42 }); + const result = await mirror.render(); + strictEqual(result?.lineCount, 42); + }); + + test('keeps a truncated snapshot height across layout', async () => { + const mirror = createSnapshotMirror({ text: 'x'.repeat(100), truncated: true, lineCount: 42 }); + const first = await mirror.render(); + const laidOut = await mirror.layout(1224); + const cached = await mirror.render(); + deepStrictEqual({ + first: first?.lineCount, + laidOut: laidOut?.lineCount, + cached: cached?.lineCount, + }, { + first: 42, + laidOut: 42, + cached: 42, + }); + }); + + test('measures horizontal chrome from the attached element computed padding', async () => { + const mirror = createSnapshotMirror({ text: 'hello' }); + const container = document.createElement('div'); + document.body.appendChild(container); + try { + fakes[0].raw.open(container); + fakes[0].raw.element!.style.paddingLeft = '4px'; + fakes[0].raw.element!.style.paddingRight = '0px'; + await mirror.layout(1224); + strictEqual(fakes[0].raw.cols, 122); // floor((1224 - 4) / 10) + } finally { + container.remove(); + } + }); + }); + + suite('DetachedTerminalCommandMirror.layout', () => { + let instantiationService: TestInstantiationService; + let XTermBaseCtor: typeof Terminal; + let fakes: ReturnType[]; + + setup(async () => { + const configurationService = new TestConfigurationService({ + editor: { + fastScrollSensitivity: 2, + mouseWheelScrollSensitivity: 1 + } as Partial, + files: {}, + terminal: { + integrated: defaultTerminalConfig + }, + }); + instantiationService = workbenchInstantiationService({ + configurationService: () => configurationService + }, store); + XTermBaseCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; + fakes = []; + instantiationService.stub(ITerminalService, { + createDetachedTerminal: async (options: IDetachedXTermOptions) => { + const fake = createFakeDetachedTerminal(XTermBaseCtor, options); + fakes.push(fake); + return fake.instance; + } + } as Partial); + }); + + async function createXterm(cols = 80, rows = 10): Promise { + const capabilities = store.add(new TerminalCapabilityStore()); + return store.add(instantiationService.createInstance(XtermTerminal, undefined, XTermBaseCtor, { + cols, + rows, + xtermColorProvider: { getBackgroundColor: () => undefined }, + capabilities, + disableShellIntegrationReporting: true, + xtermAddonImporter: new TestXtermAddonImporter(), + }, undefined)); + } + + function write(xterm: XtermTerminal, data: string): Promise { + return new Promise(resolve => xterm.write(data, resolve)); + } + + function lineText(raw: Terminal, y: number): string { + return raw.buffer.active.getLine(y)?.translateToString(true) ?? ''; + } + + /** + * Writes a finished command whose output is a single 100 character line, which soft-wraps + * onto two rows in the 80 column source terminal. + */ + async function createWrappedCommand(source: XtermTerminal): Promise { + const executedMarker = source.raw.registerMarker(0)!; + await write(source, 'x'.repeat(100) + '\r\n'); + const endMarker = source.raw.registerMarker(0)!; + return { executedMarker, endMarker } as unknown as ITerminalCommand; + } + + function createCommandMirror(source: XtermTerminal, command: ITerminalCommand): DetachedTerminalCommandMirror { + return store.add(instantiationService.createInstance(DetachedTerminalCommandMirror, source, command)); + } + + test('resizes before any render without writing content', async () => { + const source = await createXterm(); + const command = await createWrappedCommand(source); + const mirror = createCommandMirror(source, command); + await mirror.layout(1224); // floor((1224 - 20) / 10) = 120 cols + deepStrictEqual({ + cols: fakes[0].raw.cols, + writeCalls: fakes[0].counters.writeCalls, + }, { + cols: 120, + writeCalls: 0, + }); + }); + + test('re-wraps rendered command output at the new cols without rewriting', async () => { + const source = await createXterm(); + const command = await createWrappedCommand(source); + const mirror = createCommandMirror(source, command); + await mirror.renderCommand(); + deepStrictEqual({ + line0: lineText(fakes[0].raw, 0), + line1: lineText(fakes[0].raw, 1), + }, { + line0: 'x'.repeat(80), + line1: 'x'.repeat(20), + }); + const writeCallsBeforeLayout = fakes[0].counters.writeCalls; + const result = await mirror.layout(1224); + deepStrictEqual({ + cols: fakes[0].raw.cols, + line0: lineText(fakes[0].raw, 0), + maxColumnWidth: computeMaxBufferColumnWidth(fakes[0].raw.buffer.active, fakes[0].raw.cols), + // The reported line count must reflect the re-wrapped mirror rows, not the + // source terminal's wrap at its own cols, so the box height matches + lineCount: result?.lineCount, + // Re-wrapping must come from xterm's native resize reflow, not a buffer + // rewrite, which would flash a cleared frame on every resize + writeCalls: fakes[0].counters.writeCalls, + }, { + cols: 120, + line0: 'x'.repeat(100), + maxColumnWidth: 100, + lineCount: 1, + writeCalls: writeCallsBeforeLayout, + }); + }); + + test('repeated layout with the same width does not resize or rewrite', async () => { + const source = await createXterm(); + const command = await createWrappedCommand(source); + const mirror = createCommandMirror(source, command); + await mirror.renderCommand(); + await mirror.layout(1224); + const { resizeCalls, writeCalls } = { ...fakes[0].counters }; + await mirror.layout(1224); + deepStrictEqual(fakes[0].counters, { resizeCalls, writeCalls }); + }); + }); }); From b8f42e0b9157b1b99fbd3e6af75b619b28fcebd8 Mon Sep 17 00:00:00 2001 From: Praneeth Kodumagulla <64239307+praneethhere@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:01:31 -0500 Subject: [PATCH 42/86] Support COPILOT_HOME for Copilot CLI state (#314917) * Support COPILOT_HOME for Copilot CLI state * Align Copilot CLI state with COPILOT_HOME Use COPILOT_HOME as the configured Copilot root and otherwise fall back to ~/.copilot, matching the current CLI. XDG_STATE_HOME is now a migration source handled by the CLI rather than an active state location. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Honor COPILOT_HOME in agent host log collection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Dmitriy Vasyura Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Don Jayamanne --- .../copilotcli/node/cliHelpers.ts | 11 +-- .../copilotcli/node/test/cliHelpers.spec.ts | 72 +++++++++++++++++++ .../test/copilotCliSessionService.spec.ts | 18 +++-- .../vscode-node/test/lockFile.spec.ts | 16 ++--- .../node/copilot/copilotAgentSession.ts | 7 +- .../agentHost/test/common/copilotHome.test.ts | 2 + .../agentHost/test/node/copilotAgent.test.ts | 10 +-- .../test/node/copilotAgentSession.test.ts | 60 ++++++++-------- .../browser/openSessionEventsFile.test.ts | 20 ++++++ .../actions/exportAgentHostDebugLogsAction.ts | 2 +- .../chatDebug/agentHostChatDebugProvider.ts | 6 +- .../browser/chatDebug/agentHostLogSources.ts | 4 +- .../chat/browser/copilotCliEventsUri.ts | 32 ++++++--- 13 files changed, 184 insertions(+), 76 deletions(-) create mode 100644 extensions/copilot/src/extension/chatSessions/copilotcli/node/test/cliHelpers.spec.ts diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/cliHelpers.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/cliHelpers.ts index f8a612d248b..ac0c1c6f1b5 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/cliHelpers.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/cliHelpers.ts @@ -7,22 +7,17 @@ import { homedir } from 'os'; import { join } from 'path'; const COPILOT_HOME_DIRECTORY = '.copilot'; -const APP_DIRECTORY = join(COPILOT_HOME_DIRECTORY, 'ide'); -const SESSION_STATE_DIRECTORY = join(COPILOT_HOME_DIRECTORY, 'session-state'); export function getCopilotHome(): string { - const xdgHome = process.env.XDG_STATE_HOME; - return xdgHome ? join(xdgHome, COPILOT_HOME_DIRECTORY) : join(homedir(), COPILOT_HOME_DIRECTORY); + return process.env.COPILOT_HOME || join(homedir(), COPILOT_HOME_DIRECTORY); } export function getCopilotCliStateDir(): string { - const xdgHome = process.env.XDG_STATE_HOME; - return xdgHome ? join(xdgHome, APP_DIRECTORY) : join(homedir(), APP_DIRECTORY); + return join(getCopilotHome(), 'ide'); } export function getCopilotCLISessionStateDir(): string { - const xdgHome = process.env.XDG_STATE_HOME; - return xdgHome ? join(xdgHome, SESSION_STATE_DIRECTORY) : join(homedir(), SESSION_STATE_DIRECTORY); + return join(getCopilotHome(), 'session-state'); } export function getCopilotCLISessionDir(sessionId: string): string { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/cliHelpers.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/cliHelpers.spec.ts new file mode 100644 index 00000000000..ed9b18fb7fd --- /dev/null +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/cliHelpers.spec.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { homedir } from 'os'; +import { join } from 'path'; +import { afterEach, describe, expect, it } from 'vitest'; + +import { + getCopilotCliStateDir, + getCopilotCLISessionStateDir, + getCopilotHome, +} from '../cliHelpers'; + +const originalCopilotHome = process.env.COPILOT_HOME; +const originalXdgStateHome = process.env.XDG_STATE_HOME; + +function setEnv( + name: 'COPILOT_HOME' | 'XDG_STATE_HOME', + value: string | undefined, +): void { + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } +} + +afterEach(() => { + setEnv('COPILOT_HOME', originalCopilotHome); + setEnv('XDG_STATE_HOME', originalXdgStateHome); +}); + +describe('Copilot CLI state directories', () => { + it('uses COPILOT_HOME', () => { + setEnv('COPILOT_HOME', '/tmp/copilot-home'); + setEnv('XDG_STATE_HOME', '/tmp/xdg-state'); + + expect(getCopilotHome()).toBe('/tmp/copilot-home'); + expect(getCopilotCliStateDir()).toBe(join('/tmp/copilot-home', 'ide')); + expect(getCopilotCLISessionStateDir()).toBe( + join('/tmp/copilot-home', 'session-state'), + ); + }); + + it('does not use the legacy XDG_STATE_HOME location', () => { + setEnv('COPILOT_HOME', undefined); + setEnv('XDG_STATE_HOME', '/tmp/xdg-state'); + + expect(getCopilotHome()).toBe(join(homedir(), '.copilot')); + expect(getCopilotCliStateDir()).toBe( + join(homedir(), '.copilot', 'ide'), + ); + expect(getCopilotCLISessionStateDir()).toBe( + join(homedir(), '.copilot', 'session-state'), + ); + }); + + it('falls back to the user home directory', () => { + setEnv('COPILOT_HOME', undefined); + setEnv('XDG_STATE_HOME', undefined); + + expect(getCopilotHome()).toBe(join(homedir(), '.copilot')); + expect(getCopilotCliStateDir()).toBe( + join(homedir(), '.copilot', 'ide'), + ); + expect(getCopilotCLISessionStateDir()).toBe( + join(homedir(), '.copilot', 'session-state'), + ); + }); +}); diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts index dfbac6a6636..019fa142903 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts @@ -178,7 +178,7 @@ describe('CopilotCLISessionService', () => { let configurationService: IConfigurationService; let createSessionService: (options?: ICreateSessionServiceOptions) => CopilotCLISessionService; let tempStateHome: string | undefined; - const originalXdgStateHome = process.env.XDG_STATE_HOME; + const originalCopilotHome = process.env.COPILOT_HOME; beforeEach(async () => { vi.useRealTimers(); const sdk = { @@ -251,7 +251,11 @@ describe('CopilotCLISessionService', () => { void rm(tempStateHome, { recursive: true, force: true }); tempStateHome = undefined; } - process.env.XDG_STATE_HOME = originalXdgStateHome; + if (originalCopilotHome === undefined) { + delete process.env.COPILOT_HOME; + } else { + process.env.COPILOT_HOME = originalCopilotHome; + } vi.useRealTimers(); vi.restoreAllMocks(); disposables.clear(); @@ -718,7 +722,7 @@ describe('CopilotCLISessionService', () => { describe('CopilotCLISessionService.tryGetPartialSesionHistory', () => { it('reconstructs history from persisted files', async () => { tempStateHome = await mkdtemp(join(tmpdir(), 'copilot-cli-session-service-')); - process.env.XDG_STATE_HOME = tempStateHome; + process.env.COPILOT_HOME = join(tempStateHome, '.copilot'); const sessionId = 'partial-session'; const sessionDir = URI.file(getCopilotCLISessionDir(sessionId)); const fileSystem = new MockFileSystemService(); @@ -757,7 +761,7 @@ describe('CopilotCLISessionService', () => { it('returns cached result on second call without re-reading the file', async () => { tempStateHome = await mkdtemp(join(tmpdir(), 'copilot-cli-session-service-')); - process.env.XDG_STATE_HOME = tempStateHome; + process.env.COPILOT_HOME = join(tempStateHome, '.copilot'); const sessionId = 'cache-test-session'; const sessionDir = URI.file(getCopilotCLISessionDir(sessionId)); const fileSystem = new MockFileSystemService(); @@ -796,7 +800,7 @@ describe('CopilotCLISessionService', () => { it('returns undefined when the events file does not exist', async () => { tempStateHome = await mkdtemp(join(tmpdir(), 'copilot-cli-session-service-')); - process.env.XDG_STATE_HOME = tempStateHome; + process.env.COPILOT_HOME = join(tempStateHome, '.copilot'); const result = await service.tryGetPartialSessionHistory('nonexistent-session-id'); expect(result).toBeUndefined(); @@ -863,7 +867,7 @@ describe('CopilotCLISessionService', () => { it('falls back to partial session data when getSession fails with an unknown event type', async () => { tempStateHome = await mkdtemp(join(tmpdir(), 'copilot-cli-session-service-')); - process.env.XDG_STATE_HOME = tempStateHome; + process.env.COPILOT_HOME = join(tempStateHome, '.copilot'); const sessionId = 'invalid-session'; const sessionDir = URI.file(getCopilotCLISessionDir(sessionId)); const fileSystem = new MockFileSystemService(); @@ -909,7 +913,7 @@ describe('CopilotCLISessionService', () => { it('does not emit session when summary is truncated and no user turns exist', async () => { tempStateHome = await mkdtemp(join(tmpdir(), 'copilot-cli-session-service-')); - process.env.XDG_STATE_HOME = tempStateHome; + process.env.COPILOT_HOME = join(tempStateHome, '.copilot'); const sessionId = 'no-user-turns-session'; const sessionDir = URI.file(getCopilotCLISessionDir(sessionId)); const fileSystem = new MockFileSystemService(); diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/lockFile.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/lockFile.spec.ts index 5150f17758e..333fd0ade82 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/lockFile.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/lockFile.spec.ts @@ -101,8 +101,8 @@ describe('createLockFile', () => { let createdLockFile: string | null = null; beforeEach(() => { - originalEnv = process.env.XDG_STATE_HOME; - process.env.XDG_STATE_HOME = testDir; + originalEnv = process.env.COPILOT_HOME; + process.env.COPILOT_HOME = path.join(testDir, '.copilot'); }); afterEach(async () => { @@ -111,9 +111,9 @@ describe('createLockFile', () => { createdLockFile = null; } if (originalEnv !== undefined) { - process.env.XDG_STATE_HOME = originalEnv; + process.env.COPILOT_HOME = originalEnv; } else { - delete process.env.XDG_STATE_HOME; + delete process.env.COPILOT_HOME; } await fs.rm(testDir, { recursive: true, force: true }).catch(() => { }); }); @@ -177,16 +177,16 @@ describe('cleanupStaleLockFiles', () => { let originalEnv: string | undefined; beforeEach(async () => { - originalEnv = process.env.XDG_STATE_HOME; - process.env.XDG_STATE_HOME = testDir; + originalEnv = process.env.COPILOT_HOME; + process.env.COPILOT_HOME = path.join(testDir, '.copilot'); await fs.mkdir(copilotDir, { recursive: true }); }); afterEach(async () => { if (originalEnv !== undefined) { - process.env.XDG_STATE_HOME = originalEnv; + process.env.COPILOT_HOME = originalEnv; } else { - delete process.env.XDG_STATE_HOME; + delete process.env.COPILOT_HOME; } await fs.rm(testDir, { recursive: true, force: true }).catch(() => { }); }); diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index bf6028f0ff2..4c5118fd1b7 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -27,6 +27,7 @@ import { IFileService } from '../../../files/common/files.js'; import { IInstantiationService } from '../../../instantiation/common/instantiation.js'; import { ILogService, LogLevel } from '../../../log/common/log.js'; import { ITelemetryService } from '../../../telemetry/common/telemetry.js'; +import { getCopilotHomePath } from '../../common/copilotHome.js'; import { CopilotCliConfigKey, applyModelFamilyAlias, copilotCliConfigSchema } from '../../common/copilotCliConfig.js'; import type { ChatInputRequestWithPlanReview, IAgentHostPlanReviewAction } from '../../common/agentHostPlanReview.js'; import { gitHubMcpServerUrl } from '../../common/githubEndpoints.js'; @@ -123,8 +124,7 @@ interface ICopilotStreamingToolCall { displayedMessage: string | undefined; } -const COPILOT_HOME_DIRECTORY = '.copilot'; -const SESSION_STATE_DIRECTORY = join(COPILOT_HOME_DIRECTORY, 'session-state'); +const SESSION_STATE_DIRECTORY = 'session-state'; const EMPTY_TOOL_RESULT_TEXT = ''; function isPermissionDeniedKind(kind: PermissionResult['kind'] | undefined): boolean { @@ -354,8 +354,7 @@ function elicitationAnswerToFieldValue(field: ElicitationSchemaField, answer: Ch } function getCopilotCLISessionStateDir(userHome: string): string { - const xdgHome = process.env['XDG_STATE_HOME']; - return xdgHome ? join(xdgHome, SESSION_STATE_DIRECTORY) : join(userHome, SESSION_STATE_DIRECTORY); + return join(getCopilotHomePath(userHome, process.env), SESSION_STATE_DIRECTORY); } /** diff --git a/src/vs/platform/agentHost/test/common/copilotHome.test.ts b/src/vs/platform/agentHost/test/common/copilotHome.test.ts index 87d9b3f9804..2b310c16fc2 100644 --- a/src/vs/platform/agentHost/test/common/copilotHome.test.ts +++ b/src/vs/platform/agentHost/test/common/copilotHome.test.ts @@ -16,9 +16,11 @@ suite('copilotHome', () => { assert.deepStrictEqual([ getCopilotHomePath('user-home', {}), getCopilotHomePath('user-home', { COPILOT_HOME: 'custom-copilot' }), + getCopilotHomePath('user-home', { XDG_STATE_HOME: 'legacy-state-home' }), ], [ join('user-home', '.copilot'), 'custom-copilot', + join('user-home', '.copilot'), ]); }); diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index 2bc24b1f0ba..a00156384ca 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -2365,8 +2365,8 @@ suite('CopilotAgent', () => { environmentServiceRegistration: 'native', sessionDataService, }); - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - delete process.env['XDG_STATE_HOME']; + const previousCopilotHome = process.env['COPILOT_HOME']; + delete process.env['COPILOT_HOME']; try { const createdSession = createAgentSessionThroughAgent(agent, instantiationService); const agentSession = disposables.add(createdSession.session); @@ -2383,10 +2383,10 @@ suite('CopilotAgent', () => { assert.strictEqual(result.kind, 'approve-once'); } finally { - if (previousXdgStateHome === undefined) { - delete process.env['XDG_STATE_HOME']; + if (previousCopilotHome === undefined) { + delete process.env['COPILOT_HOME']; } else { - process.env['XDG_STATE_HOME'] = previousXdgStateHome; + process.env['COPILOT_HOME'] = previousCopilotHome; } await disposeAgent(agent); } diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index 2468c66acf6..d9963270253 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -2221,8 +2221,8 @@ suite('CopilotAgentSession', () => { }); test('auto-approves read permission for session-state plan files', async () => { - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - process.env['XDG_STATE_HOME'] = '/mock-state-home'; + const previousCopilotHome = process.env['COPILOT_HOME']; + process.env['COPILOT_HOME'] = '/mock-state-home/.copilot'; try { const { runtime, signals } = await createAgentSession(disposables); const result = await runtime.handlePermissionRequest({ @@ -2234,17 +2234,17 @@ suite('CopilotAgentSession', () => { assert.strictEqual(result.kind, 'approve-once'); assert.strictEqual(signals.length, 0); } finally { - if (previousXdgStateHome === undefined) { - delete process.env['XDG_STATE_HOME']; + if (previousCopilotHome === undefined) { + delete process.env['COPILOT_HOME']; } else { - process.env['XDG_STATE_HOME'] = previousXdgStateHome; + process.env['COPILOT_HOME'] = previousCopilotHome; } } }); test('resolves native environment through INativeEnvironmentService registration', async () => { - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - delete process.env['XDG_STATE_HOME']; + const previousCopilotHome = process.env['COPILOT_HOME']; + delete process.env['COPILOT_HOME']; try { const { runtime, signals } = await createAgentSession(disposables, { environmentServiceRegistration: 'native' }); const result = await runtime.handlePermissionRequest({ @@ -2256,17 +2256,17 @@ suite('CopilotAgentSession', () => { assert.strictEqual(result.kind, 'approve-once'); assert.strictEqual(signals.length, 0); } finally { - if (previousXdgStateHome === undefined) { - delete process.env['XDG_STATE_HOME']; + if (previousCopilotHome === undefined) { + delete process.env['COPILOT_HOME']; } else { - process.env['XDG_STATE_HOME'] = previousXdgStateHome; + process.env['COPILOT_HOME'] = previousCopilotHome; } } }); test('logs and rethrows permission failures', async () => { - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - delete process.env['XDG_STATE_HOME']; + const previousCopilotHome = process.env['COPILOT_HOME']; + delete process.env['COPILOT_HOME']; const logService = new CapturingLogService(); try { const { runtime } = await createAgentSession(disposables, { @@ -2287,10 +2287,10 @@ suite('CopilotAgentSession', () => { assert.ok(entry.first instanceof TypeError); assert.strictEqual(entry.args[0], '[Copilot:test-session-1] Failed to handle permission request: kind=read, toolCallId=tc-read-plan-missing-env'); } finally { - if (previousXdgStateHome === undefined) { - delete process.env['XDG_STATE_HOME']; + if (previousCopilotHome === undefined) { + delete process.env['COPILOT_HOME']; } else { - process.env['XDG_STATE_HOME'] = previousXdgStateHome; + process.env['COPILOT_HOME'] = previousCopilotHome; } } }); @@ -2345,8 +2345,8 @@ suite('CopilotAgentSession', () => { }); test('auto-approves write permission for session-state plan files', async () => { - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - process.env['XDG_STATE_HOME'] = '/mock-state-home'; + const previousCopilotHome = process.env['COPILOT_HOME']; + process.env['COPILOT_HOME'] = '/mock-state-home/.copilot'; try { const { runtime, signals } = await createAgentSession(disposables); const result = await runtime.handlePermissionRequest({ @@ -2358,17 +2358,17 @@ suite('CopilotAgentSession', () => { assert.strictEqual(result.kind, 'approve-once'); assert.strictEqual(signals.length, 0); } finally { - if (previousXdgStateHome === undefined) { - delete process.env['XDG_STATE_HOME']; + if (previousCopilotHome === undefined) { + delete process.env['COPILOT_HOME']; } else { - process.env['XDG_STATE_HOME'] = previousXdgStateHome; + process.env['COPILOT_HOME'] = previousCopilotHome; } } }); test('does not auto-approve session-state files from another session', async () => { - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - process.env['XDG_STATE_HOME'] = '/mock-state-home'; + const previousCopilotHome = process.env['COPILOT_HOME']; + process.env['COPILOT_HOME'] = '/mock-state-home/.copilot'; try { const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); const resultPromise = runtime.handlePermissionRequest({ @@ -2384,17 +2384,17 @@ suite('CopilotAgentSession', () => { const result = await resultPromise; assert.strictEqual(result.kind, 'approve-once'); } finally { - if (previousXdgStateHome === undefined) { - delete process.env['XDG_STATE_HOME']; + if (previousCopilotHome === undefined) { + delete process.env['COPILOT_HOME']; } else { - process.env['XDG_STATE_HOME'] = previousXdgStateHome; + process.env['COPILOT_HOME'] = previousCopilotHome; } } }); test('does not auto-approve traversal paths that escape the session-state directory', async () => { - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - process.env['XDG_STATE_HOME'] = '/mock-state-home'; + const previousCopilotHome = process.env['COPILOT_HOME']; + process.env['COPILOT_HOME'] = '/mock-state-home/.copilot'; try { const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); const sessionDir = join('/mock-state-home', '.copilot', 'session-state', 'test-session-1'); @@ -2411,10 +2411,10 @@ suite('CopilotAgentSession', () => { const result = await resultPromise; assert.strictEqual(result.kind, 'approve-once'); } finally { - if (previousXdgStateHome === undefined) { - delete process.env['XDG_STATE_HOME']; + if (previousCopilotHome === undefined) { + delete process.env['COPILOT_HOME']; } else { - process.env['XDG_STATE_HOME'] = previousXdgStateHome; + process.env['COPILOT_HOME'] = previousCopilotHome; } } }); diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/openSessionEventsFile.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/openSessionEventsFile.test.ts index 8bb8f2447a5..9224f7e7f48 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/openSessionEventsFile.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/openSessionEventsFile.test.ts @@ -73,6 +73,19 @@ suite('openSessionEventsFile resolveEventsUri', () => { ); }); + test('local AH copilotcli session resolves from COPILOT_HOME', () => { + const result = resolveEventsUri( + URI.parse('agent-host-copilotcli:/abc'), + userHome, + () => undefined, + { COPILOT_HOME: '/custom/copilot' }, + ); + assert.deepStrictEqual( + { kind: result.kind, resource: result.kind === 'ok' ? result.resource.toString() : undefined }, + { kind: 'ok', resource: 'file:///custom/copilot/session-state/abc/events.jsonl' }, + ); + }); + test('copilot log roots resolve beside session-state', () => { const conn = makeRemoteConn('localhost:4321', '/home/remote'); const remoteLogs = buildRemoteCopilotLogsUri(conn); @@ -97,6 +110,13 @@ suite('openSessionEventsFile resolveEventsUri', () => { }); }); + test('local copilot log root resolves from COPILOT_HOME', () => { + assert.strictEqual( + buildLocalCopilotLogsUri(userHome, { COPILOT_HOME: '/custom/copilot' }).toString(), + 'file:///custom/copilot/logs', + ); + }); + test('EH CLI copilotcli session resolves to ~/.copilot/session-state//events.jsonl', () => { const result = resolveEventsUri(URI.parse('copilotcli:/abc'), userHome, () => undefined); assert.deepStrictEqual( diff --git a/src/vs/workbench/contrib/chat/browser/actions/exportAgentHostDebugLogsAction.ts b/src/vs/workbench/contrib/chat/browser/actions/exportAgentHostDebugLogsAction.ts index f0f6e223177..d4c5fcebfc1 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/exportAgentHostDebugLogsAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/exportAgentHostDebugLogsAction.ts @@ -207,7 +207,7 @@ export async function collectAgentHostDebugLogs( } } - // 5. Copilot SDK process logs under ~/.copilot/logs do not include the + // 5. Copilot SDK process logs under /logs do not include the // session id in the filename, but relevant entries include it in the content. const rawSessionId = getCopilotCliSessionRawId(activeSession?.resource); if (rawSessionId) { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/agentHostChatDebugProvider.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/agentHostChatDebugProvider.ts index 0ac3c5a15fd..ba8d8f5cbd7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/agentHostChatDebugProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/agentHostChatDebugProvider.ts @@ -24,13 +24,13 @@ import { IPathService } from '../../../../services/path/common/pathService.js'; import { ChatDebugHookResult, ChatDebugLogLevel, IChatDebugCustomizationLogEntry, IChatDebugEvent, IChatDebugFileEntry, IChatDebugLogProvider, IChatDebugMessageSection, IChatDebugModelTurnEvent, IChatDebugResolvedEventContent, IChatDebugService } from '../../common/chatDebugService.js'; import { IAgentHostCustomizationService } from '../agentSessions/agentHost/agentHostCustomizationService.js'; import { AgentHostAgentDebugLogEnabledSettingId, AgentHostAgentDebugLogMaxEventsSettingId } from '../../common/promptSyntax/promptTypes.js'; -import { COPILOT_CLI_EH_SCHEME, COPILOT_CLI_LOCAL_AH_SCHEME, getCopilotCliSessionRawId, resolveEventsUri } from '../copilotCliEventsUri.js'; +import { buildLocalSessionStateUri, COPILOT_CLI_EH_SCHEME, COPILOT_CLI_LOCAL_AH_SCHEME, getCopilotCliSessionRawId, resolveEventsUri } from '../copilotCliEventsUri.js'; import { AgentHostCustomizationRecorder, AgentHostUsageRecorder, buildAgentHostCustomizationsUri, buildAgentHostUsageUri, readAgentHostCustomizationsSnapshot, readAgentHostUsageRecords, type IAgentHostUsageRecord } from './agentHostUsageSidecar.js'; /** * One record in an Agent Host Copilot CLI `events.jsonl` stream. The CLI * writes a line-delimited JSON log of the session under - * `~/.copilot/session-state//events.jsonl`. Every record shares the same + * `/session-state//events.jsonl`. Every record shares the same * envelope. Note that `parentId` is **not** a logical parent: the SDK defines * it as the chronologically preceding event in the session (a flat linked chain * over every event), not the user → model-turn → tool-call hierarchy. The @@ -549,7 +549,7 @@ export class AgentHostChatDebugContribution extends Disposable implements IWorkb private async _discoverLocalSessions(token: CancellationToken): Promise<{ uri: URI; title?: string }[]> { const userHome = this._pathService.userHome({ preferLocal: true }); - const sessionStateDir = joinPath(userHome, '.copilot', 'session-state'); + const sessionStateDir = buildLocalSessionStateUri(userHome); let stat; try { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/agentHostLogSources.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/agentHostLogSources.ts index 32681c3674a..754311cc8a3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/agentHostLogSources.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/agentHostLogSources.ts @@ -49,7 +49,7 @@ export const enum AgentHostLogSourceKind { Events = 'events', /** The client-side AHP JSON-RPC wire log (`/ahp/*.jsonl`). */ WireLog = 'wire', - /** The Copilot SDK process logs under `~/.copilot/logs`. */ + /** The Copilot SDK process logs under `/logs`. */ CliLog = 'cliLog', /** A VS Code output channel (agent host process, renderer, shared). */ ProcessChannel = 'processChannel', @@ -216,7 +216,7 @@ export async function enumerateAgentHostLogSources( }); } - // 5. Copilot SDK process logs (~/.copilot/logs), content-filtered lazily by session id. + // 5. Copilot SDK process logs (/logs), content-filtered lazily by session id. const rawSessionId = getCopilotCliSessionRawId(sessionResource); if (rawSessionId) { const copilotLogsDir = isLocal diff --git a/src/vs/workbench/contrib/chat/browser/copilotCliEventsUri.ts b/src/vs/workbench/contrib/chat/browser/copilotCliEventsUri.ts index 2ca03c2d615..edb5f7f89ba 100644 --- a/src/vs/workbench/contrib/chat/browser/copilotCliEventsUri.ts +++ b/src/vs/workbench/contrib/chat/browser/copilotCliEventsUri.ts @@ -4,8 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Schemas } from '../../../../base/common/network.js'; +import { env } from '../../../../base/common/process.js'; +import type { IProcessEnvironment } from '../../../../base/common/platform.js'; import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; +import { getCopilotHomePath } from '../../../../platform/agentHost/common/copilotHome.js'; import { parseRemoteAgentHostSessionTypeAuthority } from '../../../../platform/agentHost/common/agentHostSessionType.js'; import { agentHostAuthority, fromAgentHostUri, toAgentHostUri } from '../../../../platform/agentHost/common/agentHostUri.js'; import { IRemoteAgentHostConnectionInfo } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; @@ -21,21 +24,28 @@ export const COPILOT_CLI_LOCAL_AH_SCHEME = `agent-host-${COPILOT_CLI_PROVIDER}`; export const COPILOT_CLI_EH_SCHEME = COPILOT_CLI_PROVIDER; /** - * Builds the local `events.jsonl` URI under `~/.copilot/session-state//`. + * Builds the local `events.jsonl` URI under `/session-state//`. * * Used for both the local Agent Host Copilot CLI provider and the * extension-host Copilot CLI provider, which share the same on-disk layout * and the same chat session URI shape (`copilotcli:/`). */ -export function buildLocalEventsUri(userHome: URI, rawSessionId: string): URI { - return joinPath(userHome, '.copilot', 'session-state', rawSessionId, 'events.jsonl'); +export function buildLocalEventsUri(userHome: URI, rawSessionId: string, environment: IProcessEnvironment = env): URI { + return joinPath(buildLocalCopilotHomeUri(userHome, environment), 'session-state', rawSessionId, 'events.jsonl'); } /** - * Builds the local `~/.copilot/logs` directory URI. + * Builds the local `/logs` directory URI. */ -export function buildLocalCopilotLogsUri(userHome: URI): URI { - return joinPath(userHome, '.copilot', 'logs'); +export function buildLocalCopilotLogsUri(userHome: URI, environment: IProcessEnvironment = env): URI { + return joinPath(buildLocalCopilotHomeUri(userHome, environment), 'logs'); +} + +/** + * Builds the local `/session-state` directory URI. + */ +export function buildLocalSessionStateUri(userHome: URI, environment: IProcessEnvironment = env): URI { + return joinPath(buildLocalCopilotHomeUri(userHome, environment), 'session-state'); } /** @@ -120,6 +130,7 @@ export function resolveEventsUri( sessionResource: URI | undefined, userHome: URI, getConnectionByAuthority: (authority: string) => IRemoteAgentHostConnectionInfo | undefined, + environment: IProcessEnvironment = env, ): ResolveEventsUriResult { if (!sessionResource) { return { kind: 'no-session' }; @@ -130,7 +141,7 @@ export function resolveEventsUri( } if (sessionResource.scheme === COPILOT_CLI_LOCAL_AH_SCHEME || sessionResource.scheme === COPILOT_CLI_EH_SCHEME) { - return { kind: 'ok', resource: buildLocalEventsUri(userHome, rawId) }; + return { kind: 'ok', resource: buildLocalEventsUri(userHome, rawId, environment) }; } const remoteAuthority = parseRemoteAuthorityFromScheme(sessionResource.scheme); @@ -174,8 +185,9 @@ export function buildHostLocalEventsPath( sessionResource: URI | undefined, userHome: URI, getConnectionByAuthority: (authority: string) => IRemoteAgentHostConnectionInfo | undefined, + environment: IProcessEnvironment = env, ): string | undefined { - const result = resolveEventsUri(sessionResource, userHome, getConnectionByAuthority); + const result = resolveEventsUri(sessionResource, userHome, getConnectionByAuthority, environment); if (result.kind !== 'ok') { return undefined; } @@ -188,3 +200,7 @@ export function buildHostLocalEventsPath( // injected path is usable by host-side tooling; POSIX paths are left as-is. return fromAgentHostUri(result.resource).path.replace(/^\/([a-zA-Z]:)/, '$1'); } + +function buildLocalCopilotHomeUri(userHome: URI, environment: IProcessEnvironment): URI { + return URI.file(getCopilotHomePath(userHome.fsPath, environment)); +} From 667ea72312b066db30cd156ee0e44c8ea690a12b Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:23:21 -0700 Subject: [PATCH 43/86] Implement cloud sandbox retry count and telemetry tracking (#328290) --- .../agentHost/common/cloudSandboxAgentHost.ts | 31 ++ .../test/common/cloudSandboxAgentHost.test.ts | 28 ++ .../cloudSandboxAgentHost.contribution.ts | 2 + .../browser/cloudSandboxAgentHostService.ts | 81 +---- .../browser/cloudSandboxCredentialRefresh.ts | 204 +++++++++++++ .../browser/cloudSandboxCredentialsService.ts | 27 +- .../browser/cloudSandboxTelemetry.ts | 209 +++++++++++++ .../cloudSandboxCredentialRefresh.test.ts | 284 ++++++++++++++++++ .../browser/cloudSandboxTelemetry.test.ts | 159 ++++++++++ 9 files changed, 953 insertions(+), 72 deletions(-) create mode 100644 src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxCredentialRefresh.ts create mode 100644 src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxTelemetry.ts create mode 100644 src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/cloudSandboxCredentialRefresh.test.ts create mode 100644 src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/cloudSandboxTelemetry.test.ts diff --git a/src/vs/platform/agentHost/common/cloudSandboxAgentHost.ts b/src/vs/platform/agentHost/common/cloudSandboxAgentHost.ts index ffacb0b08ae..6cd0568f996 100644 --- a/src/vs/platform/agentHost/common/cloudSandboxAgentHost.ts +++ b/src/vs/platform/agentHost/common/cloudSandboxAgentHost.ts @@ -244,6 +244,37 @@ export class CloudSandboxAuthenticationRequiredError extends Error { } } +/** + * A Mission Control request that came back with a non-success status. Carries the {@link statusCode} + * so callers can tell a failure that may clear on its own from one that never will. + */ +export class CloudSandboxRequestError extends Error { + constructor(readonly statusCode: number | undefined, message: string) { + super(message); + this.name = 'CloudSandboxRequestError'; + } +} + +/** + * Whether re-issuing a request that failed with {@link error} could plausibly succeed later. + * + * Transport failures carry no status and are assumed transient, as are 5xx, 408 and 429. Every other + * 4xx describes a request Mission Control will reject identically however often it is repeated — a + * deleted environment, a revoked token — so repeating it only adds load. Callers that retry on a + * timer MUST consult this, or a single dead session becomes an unbounded stream of failed requests. + * + * Note that {@link CloudSandboxAuthenticationRequiredError} counts as retryable: it is raised before + * any request goes out, and covers the GitHub auth provider not having registered yet as well as a + * genuinely signed-out user. Callers still need their own ceiling on how long they keep trying. + */ +export function isRetryableCloudSandboxError(error: unknown): boolean { + if (!(error instanceof CloudSandboxRequestError) || error.statusCode === undefined) { + return true; + } + const status = error.statusCode; + return status === 408 || status === 429 || status < 400 || status >= 500; +} + export const ICloudSandboxAgentHostService = createDecorator('cloudSandboxAgentHostService'); /** Options for establishing a live AHP relay to a cloud sandbox environment. */ diff --git a/src/vs/platform/agentHost/test/common/cloudSandboxAgentHost.test.ts b/src/vs/platform/agentHost/test/common/cloudSandboxAgentHost.test.ts index 40de4927fe0..a955162032c 100644 --- a/src/vs/platform/agentHost/test/common/cloudSandboxAgentHost.test.ts +++ b/src/vs/platform/agentHost/test/common/cloudSandboxAgentHost.test.ts @@ -8,7 +8,10 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { buildWpsUrl, cloudSandboxAddress, + CloudSandboxAuthenticationRequiredError, + CloudSandboxRequestError, ICloudSandboxClientToken, + isRetryableCloudSandboxError, } from '../../common/cloudSandboxAgentHost.js'; suite('cloudSandbox url/address helpers', () => { @@ -50,3 +53,28 @@ suite('cloudSandbox url/address helpers', () => { ); }); }); + +suite('isRetryableCloudSandboxError', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('only statuses that can change on their own are retryable', () => { + const statuses = [200, 400, 401, 403, 404, 408, 409, 410, 422, 429, 500, 502, 503]; + const retryable = statuses.filter(status => isRetryableCloudSandboxError(new CloudSandboxRequestError(status, `HTTP ${status}`))); + + assert.deepStrictEqual(retryable, [200, 408, 429, 500, 502, 503]); + }); + + test('errors without a status are retryable, including a not-yet-available sign-in', () => { + assert.deepStrictEqual( + { + transport: isRetryableCloudSandboxError(new Error('socket hang up')), + statusless: isRetryableCloudSandboxError(new CloudSandboxRequestError(undefined, 'no status')), + // Raised before any request goes out, and covers the auth provider not having + // registered yet — so callers bound it with their own ceiling rather than here. + authNotReady: isRetryableCloudSandboxError(new CloudSandboxAuthenticationRequiredError()), + }, + { transport: true, statusless: true, authNotReady: true }, + ); + }); +}); diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxAgentHost.contribution.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxAgentHost.contribution.ts index 832313eb375..da019e61187 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxAgentHost.contribution.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxAgentHost.contribution.ts @@ -9,7 +9,9 @@ import { ICloudSandboxAgentHostService, ICloudSandboxCredentialsService } from ' import { CloudSandboxAgentHostService } from './cloudSandboxAgentHostService.js'; import { CloudSandboxAgentHostContribution } from './cloudSandboxAgentHostContribution.js'; import { CloudSandboxCredentialsService } from './cloudSandboxCredentialsService.js'; +import { CloudSandboxTelemetryService, ICloudSandboxTelemetryService } from './cloudSandboxTelemetry.js'; +registerSingleton(ICloudSandboxTelemetryService, CloudSandboxTelemetryService, InstantiationType.Delayed); registerSingleton(ICloudSandboxCredentialsService, CloudSandboxCredentialsService, InstantiationType.Delayed); registerSingleton(ICloudSandboxAgentHostService, CloudSandboxAgentHostService, InstantiationType.Delayed); diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxAgentHostService.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxAgentHostService.ts index ae988150cd7..1e4147b8539 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxAgentHostService.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxAgentHostService.ts @@ -5,8 +5,8 @@ import { CancellationError, isCancellationError } from '../../../../../base/common/errors.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { disposableTimeout, timeout } from '../../../../../base/common/async.js'; +import { Disposable, DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { timeout } from '../../../../../base/common/async.js'; import { IProtocolTransport } from '../../../../../platform/agentHost/common/state/sessionTransport.js'; import { RemoteAgentHostProtocolClient } from '../../../../../platform/agentHost/browser/remoteAgentHostProtocolClient.js'; import { editorWindowAgentHostClientInfo } from '../../../../../platform/agentHost/common/agentHostClientInfo.js'; @@ -21,6 +21,7 @@ import { CloudSandboxEnvironmentOfflineError, ICloudSandboxCredentialsService, isCloudSandboxSealedToken, + isRetryableCloudSandboxError, type ICloudSandboxClientToken, type ICloudSandboxEnvironment, } from '../../../../../platform/agentHost/common/cloudSandboxAgentHost.js'; @@ -29,25 +30,13 @@ import { PROTOCOL_VERSION } from '../../../../../platform/agentHost/common/state 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 { CloudSandboxCredentialRefresher, MAX_WAKING_DELAY_MS, type ICloudSandboxCreds } from './cloudSandboxCredentialRefresh.js'; const LOG_PREFIX = '[CloudSandboxAgentHost]'; /** Maximum number of `/connect` "waking" retries before giving up. */ const MAX_WAKING_RETRIES = 20; -/** Upper bound on a single waking Retry-After wait (ms), guarding against a hostile header. */ -const MAX_WAKING_DELAY_MS = 30_000; - -/** Refresh the Web PubSub credentials this long before the access token's `expires_at`. */ -const CREDENTIAL_REFRESH_LEAD_MS = 60_000; - -/** Floor / ceiling for the scheduled credential-refresh delay. */ -const MIN_CREDENTIAL_REFRESH_DELAY_MS = 5_000; -const MAX_CREDENTIAL_REFRESH_DELAY_MS = 55 * 60_000; - -/** Backoff delay after a failed credential refresh before retrying. */ -const CREDENTIAL_REFRESH_RETRY_MS = 30_000; - /** Maximum time to wait for a sandbox environment to report `online` before giving up. */ const ENVIRONMENT_READY_TIMEOUT_MS = 120_000; @@ -63,24 +52,6 @@ const MAX_ESTABLISH_ATTEMPTS = 3; /** Delay between establish attempts. */ const ESTABLISH_RETRY_DELAY_MS = 2_000; -/** Mutable holder for the current Web PubSub credentials, read by the transport factory. */ -interface ICloudSandboxCreds { - token: ICloudSandboxClientToken; -} - -/** - * Delay (ms) until credentials should be refreshed, computed as `expires_at` minus a lead time and - * clamped to a sane range. Falls back to the minimum when `expires_at` is missing/unparseable. - */ -function credentialRefreshDelayMs(expiresAt: string | undefined): number { - const expiryMs = expiresAt ? Date.parse(expiresAt) : NaN; - if (Number.isNaN(expiryMs)) { - return MIN_CREDENTIAL_REFRESH_DELAY_MS; - } - const delay = expiryMs - Date.now() - CREDENTIAL_REFRESH_LEAD_MS; - return Math.min(MAX_CREDENTIAL_REFRESH_DELAY_MS, Math.max(MIN_CREDENTIAL_REFRESH_DELAY_MS, delay)); -} - /** * Renderer-side coordinator for Copilot cloud sandbox connections. * @@ -158,6 +129,10 @@ export class CloudSandboxAgentHostService extends Disposable implements ICloudSa if (err instanceof CloudSandboxEnvironmentOfflineError) { throw err; } + // Nor can it help when Mission Control rejected the request outright. + if (!isRetryableCloudSandboxError(err)) { + throw err; + } lastError = err; if (attempt >= MAX_ESTABLISH_ATTEMPTS) { break; @@ -251,7 +226,13 @@ export class CloudSandboxAgentHostService extends Disposable implements ICloudSa // Keep credentials fresh for the life of the connection so reconnects have a valid token. const store = new DisposableStore(); - this._scheduleCredentialRefresh(store, address, options, clientToken.client_id, creds); + store.add(this._instantiationService.createInstance( + CloudSandboxCredentialRefresher, + address, + { environmentId: options.environmentId, sessionId: options.sessionId }, + clientToken.client_id, + creds, + )); this._managed.set(address, store); // Expose the sealed GitHub token so the AHP `authenticate` pass can present it to the host. this._creds.set(address, creds); @@ -312,38 +293,6 @@ export class CloudSandboxAgentHostService extends Disposable implements ICloudSa } } - /** - * Re-mint Web PubSub credentials shortly before they expire and write them into {@link creds}. - * The open socket is untouched; the new token is used the next time the transport is rebuilt. - */ - private _scheduleCredentialRefresh(store: DisposableStore, address: string, options: ICloudSandboxConnectOptions, clientId: string, creds: ICloudSandboxCreds): void { - const timer = store.add(new MutableDisposable()); - const arm = (delayMs: number) => { - timer.value = disposableTimeout(() => void refresh(), Math.max(MIN_CREDENTIAL_REFRESH_DELAY_MS, delayMs)); - }; - const refresh = async () => { - try { - const result = await this._credentialsService.reconnect( - { environmentId: options.environmentId, sessionId: options.sessionId }, clientId, CancellationToken.None, - ); - if (result.kind === 'waking') { - arm(Math.min(result.waking.retryAfterSeconds * 1000, MAX_WAKING_DELAY_MS)); - return; - } - // Keep the previous sealed token when a refresh omits it. - creds.token = result.token.encrypted_github_token - ? result.token - : { ...result.token, encrypted_github_token: creds.token.encrypted_github_token, host_encryption_key: creds.token.host_encryption_key }; - this._logService.trace(`${LOG_PREFIX} Refreshed Web PubSub credentials for ${address}`); - arm(credentialRefreshDelayMs(result.token.expires_at)); - } catch (err) { - this._logService.warn(`${LOG_PREFIX} Credential refresh failed for ${address}; retrying`, err); - arm(CREDENTIAL_REFRESH_RETRY_MS); - } - }; - arm(credentialRefreshDelayMs(creds.token.expires_at)); - } - /** Mint client creds, retrying (bounded) while the environment is waking. */ private async _mintWithWaking(options: ICloudSandboxConnectOptions, token: CancellationToken): Promise { for (let attempt = 0; attempt < MAX_WAKING_RETRIES; attempt++) { diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxCredentialRefresh.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxCredentialRefresh.ts new file mode 100644 index 00000000000..a21860e09d5 --- /dev/null +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxCredentialRefresh.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { disposableTimeout } from '../../../../../base/common/async.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; +import { CancellationError, isCancellationError } from '../../../../../base/common/errors.js'; +import { Disposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { + ICloudSandboxCredentialsService, + isRetryableCloudSandboxError, + type CloudSandboxConnectResult, + type ICloudSandboxClientToken, + type ICloudSandboxConnectionRequest, +} from '../../../../../platform/agentHost/common/cloudSandboxAgentHost.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { ICloudSandboxTelemetryService, type CloudSandboxRefreshStopReason } from './cloudSandboxTelemetry.js'; + +const LOG_PREFIX = '[CloudSandboxAgentHost]'; + +/** Refresh the Web PubSub credentials this long before the access token's `expires_at`. */ +const CREDENTIAL_REFRESH_LEAD_MS = 60_000; + +/** + * Floor / ceiling for the scheduled credential-refresh delay. + * + * The floor doubles as the rate limit on `/reconnect`: a token that is already at or past its + * refresh point re-mints on every tick, so this bounds how fast that can happen. Tokens live for + * the best part of an hour, so a floor this high only ever applies to a degenerate one. + */ +export const MIN_CREDENTIAL_REFRESH_DELAY_MS = 30_000; +const MAX_CREDENTIAL_REFRESH_DELAY_MS = 55 * 60_000; + +/** Backoff delay after a failed credential refresh before retrying. */ +const CREDENTIAL_REFRESH_RETRY_MS = 30_000; + +/** + * Consecutive refresh cycles that may fail to produce a healthy token before the scheduler gives up. + * + * The refresh timer outlives every user interaction — it runs for as long as the window is open — so + * without a ceiling one unrecoverable environment turns into an unbounded stream of `/reconnect` + * calls, each of which asks Mission Control to resume a sandbox that cannot be resumed. + */ +export const MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES = 10; + +/** + * Refresh interval used when a token carries no usable `expires_at`. + * + * `expires_at` is required by the API, so this only covers a malformed response. Refreshing on a + * conservative fixed interval keeps such a connection working rather than dropping it outright, + * while being far enough apart that it cannot amount to a meaningful load on Mission Control. + */ +const CREDENTIAL_REFRESH_FALLBACK_MS = 15 * 60_000; + +/** Upper bound on a single waking Retry-After wait (ms), guarding against a hostile header. */ +export const MAX_WAKING_DELAY_MS = 30_000; + +/** Mutable holder for the current Web PubSub credentials, read by the transport factory. */ +export interface ICloudSandboxCreds { + token: ICloudSandboxClientToken; +} + +/** + * Delay (ms) until credentials should be refreshed, computed as `expires_at` minus a lead time and + * clamped to a sane range. Returns `undefined` when `expires_at` is missing or unparseable, leaving + * the caller to decide — there is no basis for scheduling, so silently substituting the floor would + * make a token that never reports an expiry re-mint on every tick. + */ +export function credentialRefreshDelayMs(expiresAt: string | undefined, now = Date.now()): number | undefined { + const expiryMs = expiresAt ? Date.parse(expiresAt) : NaN; + if (Number.isNaN(expiryMs)) { + return undefined; + } + const delay = expiryMs - now - CREDENTIAL_REFRESH_LEAD_MS; + return Math.min(MAX_CREDENTIAL_REFRESH_DELAY_MS, Math.max(MIN_CREDENTIAL_REFRESH_DELAY_MS, delay)); +} + +/** + * Re-mints Web PubSub credentials shortly before they expire and writes them into the credentials + * holder it was given. The open socket is untouched; the new token is used the next time the + * transport is rebuilt. + * + * The loop is bounded in three ways, because it runs unattended for the life of the window and every + * cycle costs Mission Control a sandbox resume: a permanent rejection stops it outright, + * {@link MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES} caps a run of transient ones, and + * {@link MIN_CREDENTIAL_REFRESH_DELAY_MS} rate-limits a token that always looks due for refresh. + * + * Disposing stops the loop and cancels any request in flight. + */ +export class CloudSandboxCredentialRefresher extends Disposable { + + private readonly _timer = this._register(new MutableDisposable()); + + /** + * A `MutableDisposable` silently drops a value assigned after it is disposed, so a timeout armed + * while a refresh was in flight — the connection can go away mid-request — would never be + * cancelled and would keep calling `/reconnect` for the life of the window. Cancelling on + * teardown both aborts the in-flight request and stops anything being armed afterwards. + */ + private readonly _cts = new CancellationTokenSource(); + + /** Consecutive cycles that did not yield a healthy, long-lived token. */ + private _unhealthyCycles = 0; + + constructor( + private readonly _address: string, + private readonly _request: ICloudSandboxConnectionRequest, + private readonly _clientId: string, + private readonly _creds: ICloudSandboxCreds, + @ICloudSandboxCredentialsService private readonly _credentialsService: ICloudSandboxCredentialsService, + @ICloudSandboxTelemetryService private readonly _telemetry: ICloudSandboxTelemetryService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + this._register(toDisposable(() => this._cts.dispose(true))); + + const initialDelayMs = credentialRefreshDelayMs(this._creds.token.expires_at); + if (initialDelayMs === undefined) { + this._armUnhealthy(CREDENTIAL_REFRESH_FALLBACK_MS, 'unusableToken', `tokens kept arriving without a usable 'expires_at'`); + return; + } + this._arm(initialDelayMs); + } + + private _stop(reason: CloudSandboxRefreshStopReason, detail: string, error?: unknown): void { + this._timer.clear(); + this._telemetry.reportCredentialRefreshStopped(reason, this._unhealthyCycles, error); + this._logService.error(`${LOG_PREFIX} Stopped refreshing credentials for ${this._address}: ${detail}. The connection will drop when the current token expires.`); + } + + private _arm(delayMs: number): void { + if (this._cts.token.isCancellationRequested) { + return; + } + this._timer.value = disposableTimeout(() => void this._refresh(), Math.max(MIN_CREDENTIAL_REFRESH_DELAY_MS, delayMs)); + } + + /** Re-arm after a cycle that produced no usable token, giving up once too many pile up. */ + private _armUnhealthy(delayMs: number, reason: CloudSandboxRefreshStopReason, detail: string): void { + if (++this._unhealthyCycles >= MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES) { + this._stop(reason, `${detail} across ${this._unhealthyCycles} consecutive attempts`); + return; + } + this._arm(delayMs); + } + + private async _refresh(): Promise { + let result: CloudSandboxConnectResult; + try { + result = await this._credentialsService.reconnect(this._request, this._clientId, this._cts.token); + } catch (err) { + // Teardown cancels the in-flight request, which is a disposal rather than a refresh + // failure: counting it would log a warning for an ordinary disconnect and could report + // the loop as having given up when it was simply torn down. + if (this._cts.token.isCancellationRequested || isCancellationError(err) || err instanceof CancellationError) { + return; + } + // A rejected request (deleted environment, revoked token) fails identically however + // often it is repeated, so retrying only adds load without any prospect of recovery. + if (!isRetryableCloudSandboxError(err)) { + this._stop('permanentError', toErrorMessage(err), err); + return; + } + this._logService.warn(`${LOG_PREFIX} Credential refresh failed for ${this._address}; retrying`, err); + this._armUnhealthy(CREDENTIAL_REFRESH_RETRY_MS, 'consecutiveFailures', 'credential refresh kept failing'); + return; + } + + // The connection went away while the request was in flight; its credentials are moot. + if (this._cts.token.isCancellationRequested) { + return; + } + + if (result.kind === 'waking') { + // `/reconnect` refreshes an already-connected client, so a waking environment here is the + // sandbox disappearing underneath us rather than a wake worth waiting out. + this._armUnhealthy(Math.min(result.waking.retryAfterSeconds * 1000, MAX_WAKING_DELAY_MS), 'environmentWaking', 'environment kept reporting waking'); + return; + } + + // Keep the previous sealed token when a refresh omits it. + this._creds.token = result.token.encrypted_github_token + ? result.token + : { ...result.token, encrypted_github_token: this._creds.token.encrypted_github_token, host_encryption_key: this._creds.token.host_encryption_key }; + + this._logService.trace(`${LOG_PREFIX} Refreshed Web PubSub credentials for ${this._address}`); + const delayMs = credentialRefreshDelayMs(result.token.expires_at); + if (delayMs === undefined) { + // No basis for scheduling. Keep the connection alive on a conservative interval, but + // count the cycles so an endless stream of unschedulable tokens still terminates. + this._armUnhealthy(CREDENTIAL_REFRESH_FALLBACK_MS, 'unusableToken', `tokens kept arriving without a usable 'expires_at'`); + return; + } + if (delayMs <= MIN_CREDENTIAL_REFRESH_DELAY_MS) { + // Already at (or past) its refresh point, so the next cycle would re-mint immediately. + this._armUnhealthy(delayMs, 'unusableToken', 'refreshed tokens kept expiring immediately'); + return; + } + this._unhealthyCycles = 0; + this._arm(delayMs); + } +} diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxCredentialsService.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxCredentialsService.ts index 755b798298e..965d04efff2 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxCredentialsService.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxCredentialsService.ts @@ -5,11 +5,13 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; +import { isCancellationError } from '../../../../../base/common/errors.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { CLOUD_SANDBOX_AGENT_SLUG, CloudSandboxAuthenticationRequiredError, CloudSandboxConnectResult, + CloudSandboxRequestError, ICloudSandboxClientToken, ICloudSandboxConnectionRequest, ICloudSandboxCredentialsService, @@ -24,6 +26,7 @@ import { IProductService } from '../../../../../platform/product/common/productS import { IRequestContext } from '../../../../../base/parts/request/common/request.js'; import { asText, IRequestService } from '../../../../../platform/request/common/request.js'; import { AuthenticationSession, IAuthenticationService } from '../../../../../workbench/services/authentication/common/authentication.js'; +import { ICloudSandboxTelemetryService, requestOutcomeForStatus, type CloudSandboxRequestAction } from './cloudSandboxTelemetry.js'; /** The agent-environment endpoints Mission Control exposes. */ type CloudSandboxEnvironmentAction = 'get' | 'connect' | 'reconnect'; @@ -76,6 +79,7 @@ export class CloudSandboxCredentialsService extends Disposable implements ICloud @IAuthenticationService private readonly _authenticationService: IAuthenticationService, @IProductService private readonly _productService: IProductService, @ILogService private readonly _logService: ILogService, + @ICloudSandboxTelemetryService private readonly _telemetry: ICloudSandboxTelemetryService, ) { super(); } @@ -198,14 +202,14 @@ export class CloudSandboxCredentialsService extends Disposable implements ICloud ): Promise { const path = action === 'get' ? '' : `/${action}`; const url = `${GITHUB_DOT_COM_COPILOT_API_BASE_URI}/agents/environments/${encodeURIComponent(environmentId)}${path}${toQuery(searchParams)}`; - return this._request(url, `mc.environmentClient.${action}`, { + return this._request(url, `mc.environmentClient.${action}`, action === 'get' ? 'getEnvironment' : action, { 'Copilot-Integration-Id': COPILOT_INTEGRATION_ID, }, token); } /** Issue a task API request, throwing on a non-success status. */ - private async _sendTask(url: string, action: string, token: CancellationToken): Promise { - const context = await this._request(url, `mc.taskClient.${action}`, { + private async _sendTask(url: string, action: 'list' | 'get', token: CancellationToken): Promise { + const context = await this._request(url, `mc.taskClient.${action}`, action === 'list' ? 'listTasks' : 'getTask', { 'Accept': 'application/json', 'Copilot-Integration-Id': COPILOT_INTEGRATION_ID, }, token, DISCOVERY_TIMEOUT_MS); @@ -215,20 +219,27 @@ export class CloudSandboxCredentialsService extends Disposable implements ICloud return context; } - private async _request(url: string, callSite: string, headers: Record, token: CancellationToken, timeout: number = REQUEST_TIMEOUT_MS): Promise { + private async _request(url: string, callSite: string, action: CloudSandboxRequestAction, headers: Record, token: CancellationToken, timeout: number = REQUEST_TIMEOUT_MS): Promise { const accessToken = await this._resolveGitHubToken(); if (!accessToken) { + // No request is issued, so there is no request outcome to count. throw new CloudSandboxAuthenticationRequiredError(); } try { - return await this._requestService.request({ + const context = await this._requestService.request({ type: 'GET', url, headers: { ...headers, ['Authorization']: `Bearer ${accessToken}` }, timeout, callSite, }, token); + this._telemetry.reportRequest(action, requestOutcomeForStatus(context.res.statusCode)); + return context; } catch (error) { + // A cancelled request was never answered, so it is not a failure worth counting. + if (!isCancellationError(error) && !token.isCancellationRequested) { + this._telemetry.reportRequest(action, 'networkError'); + } this._logService.error(`${LOG_PREFIX} GET ${url} failed: ${toErrorMessage(error)}`); throw error; } @@ -257,7 +268,11 @@ export class CloudSandboxCredentialsService extends Disposable implements ICloud /** Throw a diagnosable error for a non-success response, including the body when readable. */ private async _throwForStatus(action: string, context: IRequestContext): Promise { const body = await asText(context).catch(() => ''); - throw new Error(`Mission Control ${action} failed: HTTP ${context.res.statusCode ?? 'unknown'} - ${(body ?? '').slice(0, 200)}`); + const status = context.res.statusCode; + throw new CloudSandboxRequestError( + status, + `Mission Control ${action} failed: HTTP ${status ?? 'unknown'} - ${(body ?? '').slice(0, 200)}`, + ); } /** A GitHub session carrying at least the configured chat provider scopes. */ diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxTelemetry.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxTelemetry.ts new file mode 100644 index 00000000000..428387f787d --- /dev/null +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/cloudSandboxTelemetry.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IntervalTimer } from '../../../../../base/common/async.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { CloudSandboxRequestError } from '../../../../../platform/agentHost/common/cloudSandboxAgentHost.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; + +/** The Mission Control call being reported. A closed set, so it is safe to send verbatim. */ +export type CloudSandboxRequestAction = 'connect' | 'reconnect' | 'getEnvironment' | 'listTasks' | 'getTask'; + +/** + * How a Mission Control request ended, bucketed so a count is meaningful without carrying the + * response itself. `waking` is the 202 an environment returns while it boots, which is neither a + * success nor a failure but is the response most likely to be retried in a loop. `unexpectedStatus` + * covers 1xx/3xx, which the client does not treat as success either — see + * {@link requestOutcomeForStatus}. + */ +export type CloudSandboxRequestOutcome = 'succeeded' | 'waking' | 'clientError' | 'serverError' | 'networkError' | 'unexpectedStatus'; + +/** Why the credential-refresh scheduler stopped. A closed set of client-side decisions. */ +export type CloudSandboxRefreshStopReason = + /** Mission Control rejected the request in a way that repeating cannot fix (e.g. 404). */ + | 'permanentError' + /** Too many consecutive failed refreshes. */ + | 'consecutiveFailures' + /** `/reconnect` kept answering "waking" for a client that is supposed to be connected. */ + | 'environmentWaking' + /** Refreshed tokens kept arriving already expired, or without a usable `expires_at`. */ + | 'unusableToken'; + +export const ICloudSandboxTelemetryService = createDecorator('cloudSandboxTelemetryService'); + +/** + * Telemetry for the cloud sandbox integration. + * + * Owns every event the sandbox path emits so the reporting rules — what is aggregated, which values + * are closed sets, what must never carry a URL or token — live in one place instead of being + * restated at each call site. New sandbox events belong here as additional methods. + */ +export interface ICloudSandboxTelemetryService { + readonly _serviceBrand: undefined; + + /** + * Record how a Mission Control request ended. + * + * Cheap to call on every request: outcomes are accumulated and reported periodically rather than + * sent individually, because a single connect can fan out to tens of calls through waking retries + * and readiness polls. + */ + reportRequest(action: CloudSandboxRequestAction, outcome: CloudSandboxRequestOutcome): void; + + /** + * Report that credential refresh for a connection stopped, and why. + * + * Refresh is what keeps a sandbox connection usable, so each of these marks a connection that + * will drop once its current token expires — and, equally, a retry loop that was stopped from + * running indefinitely. + */ + reportCredentialRefreshStopped(reason: CloudSandboxRefreshStopReason, consecutiveFailures: number, error?: unknown): void; +} + +/** How often accumulated request counts are reported. */ +const REQUEST_REPORT_INTERVAL_MS = 30 * 60_000; + +/** + * The outcome bucket for a response with {@link statusCode}. + * + * Only 2xx counts as a success, matching the client's own `isSuccess` check — a 1xx or 3xx is + * thrown as a request failure, so counting it as a success would understate the failure rate. + */ +export function requestOutcomeForStatus(statusCode: number | undefined): CloudSandboxRequestOutcome { + if (statusCode === undefined) { + return 'networkError'; + } + if (statusCode === 202) { + return 'waking'; + } + if (statusCode >= 200 && statusCode < 300) { + return 'succeeded'; + } + if (statusCode >= 500) { + return 'serverError'; + } + if (statusCode >= 400) { + return 'clientError'; + } + return 'unexpectedStatus'; +} + +/** Per-action counts accumulated between reports. */ +type RequestCounts = Record; + +function emptyCounts(): RequestCounts { + return { succeeded: 0, waking: 0, clientError: 0, serverError: 0, networkError: 0, unexpectedStatus: 0 }; +} + +export class CloudSandboxTelemetryService extends Disposable implements ICloudSandboxTelemetryService { + declare readonly _serviceBrand: undefined; + + private readonly _counts = new Map(); + private readonly _reportTimer = this._register(new IntervalTimer()); + /** When the current window began, i.e. when its first request was recorded. */ + private _windowStart = Date.now(); + + constructor( + @ITelemetryService private readonly _telemetryService: ITelemetryService, + ) { + super(); + // Report whatever has accumulated rather than losing the last window on shutdown. + this._register({ dispose: () => this.flushRequestCounts() }); + } + + reportRequest(action: CloudSandboxRequestAction, outcome: CloudSandboxRequestOutcome): void { + let counts = this._counts.get(action); + if (!counts) { + counts = emptyCounts(); + this._counts.set(action, counts); + // Only tick while there is something to report, so an idle window stays idle. The window + // starts here rather than at the last flush, so an idle stretch is not folded into + // `windowMs` — that would make the reported request rate look far lower than it was. + if (this._counts.size === 1) { + this._windowStart = Date.now(); + this._reportTimer.cancelAndSet(() => this.flushRequestCounts(), REQUEST_REPORT_INTERVAL_MS); + } + } + counts[outcome]++; + } + + reportCredentialRefreshStopped(reason: CloudSandboxRefreshStopReason, consecutiveFailures: number, error?: unknown): void { + this._telemetryService.publicLog2( + 'cloudSandboxCredentialRefreshStopped', + { + reason, + consecutiveFailures, + statusCode: error instanceof CloudSandboxRequestError ? error.statusCode : undefined, + }, + ); + } + + /** Report and reset the accumulated request counts. Safe to call when nothing has been recorded. */ + flushRequestCounts(): void { + if (this._counts.size === 0) { + return; + } + const windowMs = Date.now() - this._windowStart; + for (const [action, counts] of this._counts) { + this._telemetryService.publicLog2( + 'cloudSandboxRequests', + { + action, + windowMs, + total: counts.succeeded + counts.waking + counts.clientError + counts.serverError + counts.networkError + counts.unexpectedStatus, + succeeded: counts.succeeded, + waking: counts.waking, + clientError: counts.clientError, + serverError: counts.serverError, + networkError: counts.networkError, + unexpectedStatus: counts.unexpectedStatus, + }, + ); + } + this._counts.clear(); + this._reportTimer.cancel(); + } +} + +type CloudSandboxRequestsEvent = { + action: string; + windowMs: number; + total: number; + succeeded: number; + waking: number; + clientError: number; + serverError: number; + networkError: number; + unexpectedStatus: number; +}; + +type CloudSandboxRequestsClassification = { + action: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Which Mission Control call was counted (connect, reconnect, getEnvironment, listTasks or getTask).' }; + windowMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Milliseconds covered by these counts.' }; + total: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Requests issued for this action during the window.' }; + succeeded: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Requests that returned a success status.' }; + waking: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Requests answered with HTTP 202, meaning the sandbox environment was still waking.' }; + clientError: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Requests rejected with a 4xx status.' }; + serverError: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Requests that failed with a 5xx status.' }; + networkError: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Requests that never produced a response, such as a timeout.' }; + unexpectedStatus: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Requests answered with a status the client does not expect, such as 1xx or 3xx.' }; + owner: 'osortega'; + comment: 'Volume and outcome of the requests the cloud sandbox integration sends to GitHub Mission Control, used to size its load and detect runaway retry loops.'; +}; + +type CloudSandboxRefreshStoppedEvent = { + reason: string; + consecutiveFailures: number; + statusCode: number | undefined; +}; + +type CloudSandboxRefreshStoppedClassification = { + reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Why the scheduler gave up: permanentError, consecutiveFailures, environmentWaking or unusableToken.' }; + consecutiveFailures: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Consecutive unhealthy refresh cycles preceding the stop.' }; + statusCode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'HTTP status that caused a permanent stop, when the stop was caused by a rejected request.' }; + owner: 'osortega'; + comment: 'Reports that credential refresh for a cloud sandbox connection stopped, so unrecoverable sandbox sessions can be distinguished from transient failures.'; +}; diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/cloudSandboxCredentialRefresh.test.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/cloudSandboxCredentialRefresh.test.ts new file mode 100644 index 00000000000..1551abd6512 --- /dev/null +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/cloudSandboxCredentialRefresh.test.ts @@ -0,0 +1,284 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationError } from '../../../../../../base/common/errors.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { + CloudSandboxRequestError, + type CloudSandboxConnectResult, + type ICloudSandboxClientToken, + type ICloudSandboxCredentialsService, +} from '../../../../../../platform/agentHost/common/cloudSandboxAgentHost.js'; +import { NullLogService } from '../../../../../../platform/log/common/log.js'; +import { + CloudSandboxCredentialRefresher, + credentialRefreshDelayMs, + MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, + type ICloudSandboxCreds, +} from '../../browser/cloudSandboxCredentialRefresh.js'; +import type { + CloudSandboxRefreshStopReason, + CloudSandboxRequestAction, + CloudSandboxRequestOutcome, + ICloudSandboxTelemetryService, +} from '../../browser/cloudSandboxTelemetry.js'; + +const START_TIME = Date.parse('2026-01-01T00:00:00Z'); + +/** A token expiring `minutes` from `from`. 40 minutes sits comfortably clear of the refresh floor. */ +function tokenExpiringIn(minutes: number, from: number, overrides: Partial = {}): ICloudSandboxClientToken { + return { + access_token: 'tok', + expires_at: new Date(from + minutes * 60_000).toISOString(), + wps_endpoint: 'wss://wps.example/client/hubs/h', + hub: 'h', + subprotocol: 'json.reliable.webpubsub.azure.v1', + client_id: 'client-1', + groups: { broadcast: 'b', to_client: 'tc', to_host: 'th' }, + ...overrides, + }; +} + +/** Records the stop reports the refresher emits; request counting is covered by its own suite. */ +class RecordingTelemetry implements ICloudSandboxTelemetryService { + declare readonly _serviceBrand: undefined; + + readonly stops: { reason: CloudSandboxRefreshStopReason; consecutiveFailures: number; statusCode: number | undefined }[] = []; + + reportRequest(_action: CloudSandboxRequestAction, _outcome: CloudSandboxRequestOutcome): void { } + + reportCredentialRefreshStopped(reason: CloudSandboxRefreshStopReason, consecutiveFailures: number, error?: unknown): void { + this.stops.push({ + reason, + consecutiveFailures, + statusCode: error instanceof CloudSandboxRequestError ? error.statusCode : undefined, + }); + } +} + +/** Answers every `reconnect` from a single scripted step, so a loop can run as long as it likes. */ +class ScriptedCredentialsService { + callCount = 0; + + constructor(private readonly _step: () => CloudSandboxConnectResult | Promise) { } + + async reconnect(): Promise { + this.callCount++; + return this._step() as CloudSandboxConnectResult; + } +} + +suite('CloudSandboxCredentialRefresher', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + /** + * Run a refresher over `durationMs` of virtual time and report what it did. The refresher is + * disposed before returning so nothing survives into the next test. + */ + async function runRefresher( + step: () => CloudSandboxConnectResult | Promise, + durationMs: number, + initialToken = tokenExpiringIn(40, START_TIME), + ): Promise<{ calls: number; stops: RecordingTelemetry['stops']; creds: ICloudSandboxCreds }> { + const telemetry = new RecordingTelemetry(); + const credentials = new ScriptedCredentialsService(step); + const creds: ICloudSandboxCreds = { token: initialToken }; + const disposables = new DisposableStore(); + + disposables.add(new CloudSandboxCredentialRefresher( + 'cloudsandbox:env_1', + { environmentId: 'env_1', sessionId: 'session-1' }, + 'client-1', + creds, + credentials as unknown as ICloudSandboxCredentialsService, + telemetry, + new NullLogService(), + )); + + await new Promise(resolve => setTimeout(resolve, durationMs)); + disposables.dispose(); + return { calls: credentials.callCount, stops: telemetry.stops, creds }; + } + + test('a healthy token keeps refreshing and never reports a stop', () => runWithFakedTimers({ useFakeTimers: true, startTime: START_TIME }, async () => { + // Every refresh yields another healthy token, so the loop should simply keep going. Twelve + // hours is far more than the failure cap would allow if the counter were mis-managed. + const result = await runRefresher(() => ({ kind: 'token', token: tokenExpiringIn(40, Date.now()) }), 12 * 60 * 60_000); + + assert.deepStrictEqual( + { keptRefreshing: result.calls > 10, stops: result.stops }, + { keptRefreshing: true, stops: [] }, + ); + })); + + test('a permanently rejected refresh stops at once, reporting the status', () => runWithFakedTimers({ useFakeTimers: true, startTime: START_TIME }, async () => { + const result = await runRefresher(() => Promise.reject(new CloudSandboxRequestError(404, 'environment gone')), 12 * 60 * 60_000); + + assert.deepStrictEqual( + result, + { + calls: 1, + stops: [{ reason: 'permanentError', consecutiveFailures: 0, statusCode: 404 }], + creds: result.creds, + }, + ); + })); + + test('transient failures stop once the consecutive-failure cap is reached', () => runWithFakedTimers({ useFakeTimers: true, startTime: START_TIME }, async () => { + const result = await runRefresher(() => Promise.reject(new CloudSandboxRequestError(500, 'server error')), 12 * 60 * 60_000); + + assert.deepStrictEqual( + { calls: result.calls, stops: result.stops }, + { + calls: MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, + stops: [{ reason: 'consecutiveFailures', consecutiveFailures: MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, statusCode: undefined }], + }, + ); + })); + + test('a success between failures resets the cap, so a flaky connection survives', () => runWithFakedTimers({ useFakeTimers: true, startTime: START_TIME }, async () => { + // Fail nine times, recover on the tenth, then fail forever. The recovery must reset the + // counter, so the stop lands a further ten failures later rather than on the tenth overall. + let call = 0; + const result = await runRefresher( + () => { + call++; + if (call === MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES) { + return { kind: 'token', token: tokenExpiringIn(40, Date.now()) }; + } + return Promise.reject(new CloudSandboxRequestError(503, 'unavailable')); + }, + 24 * 60 * 60_000, + ); + + assert.deepStrictEqual( + { calls: result.calls, stops: result.stops }, + { + calls: 2 * MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, + stops: [{ reason: 'consecutiveFailures', consecutiveFailures: MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, statusCode: undefined }], + }, + ); + })); + + test('a waking answer to /reconnect is bounded, since that client is already connected', () => runWithFakedTimers({ useFakeTimers: true, startTime: START_TIME }, async () => { + const result = await runRefresher(() => ({ kind: 'waking', waking: { retryAfterSeconds: 5 } }), 12 * 60 * 60_000); + + assert.deepStrictEqual( + { calls: result.calls, stops: result.stops }, + { + calls: MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, + stops: [{ reason: 'environmentWaking', consecutiveFailures: MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, statusCode: undefined }], + }, + ); + })); + + test('tokens that arrive already due are bounded, not re-minted on every tick', () => runWithFakedTimers({ useFakeTimers: true, startTime: START_TIME }, async () => { + // Expiring inside the lead time, so each refreshed token is immediately due again. + const result = await runRefresher(() => ({ kind: 'token', token: tokenExpiringIn(-5, Date.now()) }), 12 * 60 * 60_000); + + assert.deepStrictEqual( + { calls: result.calls, stops: result.stops }, + { + calls: MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, + stops: [{ reason: 'unusableToken', consecutiveFailures: MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, statusCode: undefined }], + }, + ); + })); + + test('a token with no usable expiry falls back to a fixed interval, still bounded', () => runWithFakedTimers({ useFakeTimers: true, startTime: START_TIME }, async () => { + const result = await runRefresher(() => ({ kind: 'token', token: tokenExpiringIn(40, Date.now(), { expires_at: 'not-a-date' }) }), 24 * 60 * 60_000); + + assert.deepStrictEqual( + { calls: result.calls, stops: result.stops }, + { + calls: MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, + stops: [{ reason: 'unusableToken', consecutiveFailures: MAX_CONSECUTIVE_CREDENTIAL_REFRESH_FAILURES, statusCode: undefined }], + }, + ); + })); + + test('disposal stops the loop without reporting it as a failure', () => runWithFakedTimers({ useFakeTimers: true, startTime: START_TIME }, async () => { + // Disposing cancels the in-flight request, so the refresh rejects with a cancellation. That + // is an ordinary teardown, and must not be counted or reported as the loop giving up. + const telemetry = new RecordingTelemetry(); + const credentials = new ScriptedCredentialsService(() => Promise.reject(new CancellationError())); + const creds: ICloudSandboxCreds = { token: tokenExpiringIn(40, START_TIME) }; + const disposables = new DisposableStore(); + + disposables.add(new CloudSandboxCredentialRefresher( + 'cloudsandbox:env_1', + { environmentId: 'env_1', sessionId: 'session-1' }, + 'client-1', + creds, + credentials as unknown as ICloudSandboxCredentialsService, + telemetry, + new NullLogService(), + )); + + await new Promise(resolve => setTimeout(resolve, 40 * 60_000)); + const callsBeforeDispose = credentials.callCount; + disposables.dispose(); + await new Promise(resolve => setTimeout(resolve, 12 * 60 * 60_000)); + + assert.deepStrictEqual( + { callsBeforeDispose, callsAfterDispose: credentials.callCount, stops: telemetry.stops }, + { callsBeforeDispose: 1, callsAfterDispose: 1, stops: [] }, + ); + })); + + test('a refreshed token without a sealed GitHub token keeps the previous one', () => runWithFakedTimers({ useFakeTimers: true, startTime: START_TIME }, async () => { + const result = await runRefresher( + () => ({ kind: 'token', token: tokenExpiringIn(40, Date.now(), { access_token: 'fresh' }) }), + 40 * 60_000, + tokenExpiringIn(40, START_TIME, { encrypted_github_token: 'copilot-sealed.v1.k.abc' }), + ); + + assert.deepStrictEqual( + { accessToken: result.creds.token.access_token, sealed: result.creds.token.encrypted_github_token }, + { accessToken: 'fresh', sealed: 'copilot-sealed.v1.k.abc' }, + ); + })); +}); + +suite('credentialRefreshDelayMs', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const inMinutes = (minutes: number) => new Date(START_TIME + minutes * 60_000).toISOString(); + + test('schedules a refresh one minute before expiry, clamped to the supported range', () => { + assert.deepStrictEqual( + { + typicalToken: credentialRefreshDelayMs(inMinutes(40), START_TIME), + beyondCeiling: credentialRefreshDelayMs(inMinutes(24 * 60), START_TIME), + dueImminently: credentialRefreshDelayMs(inMinutes(1), START_TIME), + alreadyExpired: credentialRefreshDelayMs(inMinutes(-30), START_TIME), + }, + { + typicalToken: 39 * 60_000, + beyondCeiling: 55 * 60_000, + // Never faster than the floor: a token that always looks due would otherwise re-mint + // on every tick, and each mint asks Mission Control to resume a sandbox. + dueImminently: 30_000, + alreadyExpired: 30_000, + }, + ); + }); + + test('reports no schedule when expiry is missing or unparseable', () => { + assert.deepStrictEqual( + [ + credentialRefreshDelayMs(undefined, START_TIME), + credentialRefreshDelayMs('', START_TIME), + credentialRefreshDelayMs('not-a-date', START_TIME), + ], + [undefined, undefined, undefined], + ); + }); +}); diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/cloudSandboxTelemetry.test.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/cloudSandboxTelemetry.test.ts new file mode 100644 index 00000000000..311fb3cf8cd --- /dev/null +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/cloudSandboxTelemetry.test.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { CloudSandboxRequestError } from '../../../../../../platform/agentHost/common/cloudSandboxAgentHost.js'; +import { ITelemetryData, ITelemetryService, TelemetryLevel } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { + CloudSandboxTelemetryService, + requestOutcomeForStatus, +} from '../../browser/cloudSandboxTelemetry.js'; + +interface ICapturedEvent { + readonly eventName: string; + readonly data: ITelemetryData | undefined; +} + +class TestTelemetryService implements ITelemetryService { + declare readonly _serviceBrand: undefined; + + readonly telemetryLevel = TelemetryLevel.USAGE; + readonly sendErrorTelemetry = true; + readonly sessionId = 'sessionId'; + readonly machineId = 'machineId'; + readonly sqmId = 'sqmId'; + readonly devDeviceId = 'devDeviceId'; + readonly firstSessionDate = 'firstSessionDate'; + readonly events: ICapturedEvent[] = []; + + publicLog(): void { } + publicLogError(): void { } + publicLog2(eventName: string, data?: ITelemetryData): void { + this.events.push({ eventName, data }); + } + publicLogError2(): void { } + setExperimentProperty(): void { } + setCommonProperty(): void { } +} + +suite('cloudSandbox telemetry', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('requestOutcomeForStatus buckets every response kind', () => { + assert.deepStrictEqual( + [200, 202, 204, 400, 404, 429, 500, 503, 100, 302, undefined].map(requestOutcomeForStatus), + [ + 'succeeded', + 'waking', + 'succeeded', + 'clientError', + 'clientError', + 'clientError', + 'serverError', + 'serverError', + // Only 2xx is a success, matching the client's own check: a 1xx/3xx is thrown as a + // request failure, so counting it as a success would understate the failure rate. + 'unexpectedStatus', + 'unexpectedStatus', + 'networkError', + ], + ); + }); + + test('requests are reported per action, with outcomes broken out', () => { + const telemetryService = new TestTelemetryService(); + const sandboxTelemetry = store.add(new CloudSandboxTelemetryService(telemetryService)); + + sandboxTelemetry.reportRequest('reconnect', 'serverError'); + sandboxTelemetry.reportRequest('reconnect', 'serverError'); + sandboxTelemetry.reportRequest('reconnect', 'succeeded'); + sandboxTelemetry.reportRequest('connect', 'waking'); + sandboxTelemetry.flushRequestCounts(); + + assert.deepStrictEqual( + telemetryService.events.map(e => ({ + eventName: e.eventName, + action: e.data?.action, + total: e.data?.total, + succeeded: e.data?.succeeded, + waking: e.data?.waking, + serverError: e.data?.serverError, + })), + [ + { eventName: 'cloudSandboxRequests', action: 'reconnect', total: 3, succeeded: 1, waking: 0, serverError: 2 }, + { eventName: 'cloudSandboxRequests', action: 'connect', total: 1, succeeded: 0, waking: 1, serverError: 0 }, + ], + ); + }); + + test('flushing resets the counts, and a flush with nothing recorded reports nothing', () => { + const telemetryService = new TestTelemetryService(); + const sandboxTelemetry = store.add(new CloudSandboxTelemetryService(telemetryService)); + + sandboxTelemetry.flushRequestCounts(); + assert.strictEqual(telemetryService.events.length, 0, 'nothing recorded yet'); + + sandboxTelemetry.reportRequest('getEnvironment', 'succeeded'); + sandboxTelemetry.flushRequestCounts(); + sandboxTelemetry.flushRequestCounts(); + + assert.deepStrictEqual( + telemetryService.events.map(e => ({ action: e.data?.action, total: e.data?.total })), + [{ action: 'getEnvironment', total: 1 }], + ); + }); + + test('disposing reports whatever has been counted so far', () => { + const telemetryService = new TestTelemetryService(); + const sandboxTelemetry = new CloudSandboxTelemetryService(telemetryService); + + sandboxTelemetry.reportRequest('listTasks', 'clientError'); + sandboxTelemetry.dispose(); + + assert.deepStrictEqual( + telemetryService.events.map(e => ({ action: e.data?.action, total: e.data?.total, clientError: e.data?.clientError })), + [{ action: 'listTasks', total: 1, clientError: 1 }], + ); + }); + + test('a refresh stop reports its reason, cycle count and causing status', () => { + const telemetryService = new TestTelemetryService(); + const sandboxTelemetry = store.add(new CloudSandboxTelemetryService(telemetryService)); + + sandboxTelemetry.reportCredentialRefreshStopped('permanentError', 0, new CloudSandboxRequestError(404, 'gone')); + sandboxTelemetry.reportCredentialRefreshStopped('consecutiveFailures', 10); + + assert.deepStrictEqual( + telemetryService.events.map(e => ({ eventName: e.eventName, ...e.data })), + [ + { eventName: 'cloudSandboxCredentialRefreshStopped', reason: 'permanentError', consecutiveFailures: 0, statusCode: 404 }, + { eventName: 'cloudSandboxCredentialRefreshStopped', reason: 'consecutiveFailures', consecutiveFailures: 10, statusCode: undefined }, + ], + ); + }); + + test('the window covers only the time from its first request, not preceding idle time', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const telemetryService = new TestTelemetryService(); + const disposables = new DisposableStore(); + const sandboxTelemetry = disposables.add(new CloudSandboxTelemetryService(telemetryService)); + + // Hours of silence before the first request. Folding that into `windowMs` would make the + // reported request rate look far lower than it actually was. + await new Promise(resolve => setTimeout(resolve, 6 * 60 * 60_000)); + sandboxTelemetry.reportRequest('connect', 'succeeded'); + await new Promise(resolve => setTimeout(resolve, 60_000)); + sandboxTelemetry.flushRequestCounts(); + disposables.dispose(); + + assert.deepStrictEqual( + telemetryService.events.map(e => ({ action: e.data?.action, total: e.data?.total, windowMs: e.data?.windowMs })), + [{ action: 'connect', total: 1, windowMs: 60_000 }], + ); + })); +}); From b9b1422f1276a1d6cd6309edc866f8086103ac2d Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Thu, 30 Jul 2026 18:55:17 -0700 Subject: [PATCH 44/86] fix: memory leak in notebook view model (#328208) fix: dispose removed notebook cell view models Co-authored-by: Dmitriy Vasyura --- .../viewModel/notebookViewModelImpl.ts | 2 +- .../test/browser/notebookViewModel.test.ts | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 52892c1e43b..d3c7c9e8c9e 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -225,7 +225,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD deletedCells.forEach(cell => { this._handleToViewCellMapping.delete(cell.handle); // dispose the cell to release ref to the cell text document - cell.dispose(); + this._localStore.delete(cell); }); diff[2].forEach(cell => { diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index 44c613b094f..58f54d66fa2 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -104,6 +104,32 @@ suite('NotebookViewModel', () => { ); }); + test('deleted cells are removed from the disposable store', async function () { + const getDisposeCallCount = await withTestNotebook( + [ + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], {}] + ], + (editor, viewModel) => { + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const originalDispose = cell.dispose.bind(cell); + let disposeCallCount = 0; + cell.dispose = () => { + disposeCallCount++; + originalDispose(); + }; + + runDeleteAction(editor, cell); + assert.strictEqual(disposeCallCount, 1); + cell.model.dispose(); + + return () => disposeCallCount; + } + ); + + assert.strictEqual(getDisposeCallCount(), 1); + }); + test('index', async function () { await withTestNotebook( [ From c423d6bc458e7bae0c6ba08995fb6b349833aeaa Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Thu, 30 Jul 2026 19:55:42 -0700 Subject: [PATCH 45/86] fix: memory leak in chatServiceImpl (#327128) * dispose chat follow-up tokens * cancel followups when disposing chat sessions --------- Co-authored-by: Dmitriy Vasyura --- .../common/chatService/chatServiceImpl.ts | 2 ++ .../common/chatService/chatService.test.ts | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 91f55601673..b8a4be4c375 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -285,6 +285,8 @@ export class ChatService extends Disposable implements IChatService { this._register(this._sessionModels.onDidDisposeModel(model => { clearChatMarks(model.sessionResource); this.chatDebugService.endSession(model.sessionResource); + this._sessionFollowupCancelTokens.get(model.sessionResource)?.cancel(); + this._sessionFollowupCancelTokens.deleteAndDispose(model.sessionResource); // Drop the forward untitled→real mapping for this session so it stops // re-targeting late sends. The inverse alias is intentionally retained. this.chatSessionService.clearMaterializedSessionResource(model.sessionResource); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts index 0f3b906f27c..5d729a4840b 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -651,6 +651,38 @@ suite('ChatService', () => { assert.strictEqual(disposed, true); }); + test('disposing a session cancels pending followups', async () => { + let followupsToken: CancellationToken | undefined; + const followupsCancelled = new DeferredPromise(); + const followupsAgent: IChatAgentImplementation = { + async invoke() { + return {}; + }, + provideFollowups(request, result, history, token) { + followupsToken = token; + testDisposables.add(token.onCancellationRequested(() => followupsCancelled.complete([]))); + return followupsCancelled.p; + }, + }; + + testDisposables.add(chatAgentService.registerAgent('followupsAgent', { ...getAgentData('followupsAgent'), isDefault: true })); + testDisposables.add(chatAgentService.registerAgentImplementation('followupsAgent', followupsAgent)); + + const testService = createChatService(); + const modelRef = testService.startNewLocalSession(ChatAgentLocation.Chat); + const response = await testService.sendRequest(modelRef.object.sessionResource, 'test request', { agentId: 'followupsAgent' }); + ChatSendResult.assertSent(response); + await response.data.responseCompletePromise; + + assert.ok(followupsToken); + assert.strictEqual(followupsToken.isCancellationRequested, false); + + modelRef.dispose(); + await testService.waitForModelDisposals(); + + assert.strictEqual(followupsToken.isCancellationRequested, true); + }); + test('steering message queued triggers setYieldRequested', async () => { const requestStarted = new DeferredPromise(); const completeRequest = new DeferredPromise(); From a3720a63b924f1819e958718eb8e95925fc07ebb Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:55:31 -0700 Subject: [PATCH 46/86] Fix browser overlapping with webviews (#328329) --- .../electron-browser/overlayManager.ts | 31 ++++++++++++------- .../electron-browser/overlayManager.test.ts | 23 ++++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/overlayManager.ts b/src/vs/workbench/contrib/browserView/electron-browser/overlayManager.ts index cb192663bb7..c25f4cb432f 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/overlayManager.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/overlayManager.ts @@ -33,13 +33,22 @@ const OVERLAY_DEFINITIONS: ReadonlyArray<{ className: string; type: BrowserOverl { className: 'context-view', type: BrowserOverlayType.Unknown } ]; -// Transparent full-screen layers that context menus and action widgets render to capture clicks. -// They sit in higher z-index stacking contexts above other UI, but are not tracked overlays, -// so hit-testing must skip them to find the overlay actually painted underneath. -const CONTEXT_VIEW_BLOCKER_CLASSES = ['context-view-block', 'context-view-pointerBlock']; +const HIT_TEST_EXCLUDED_CLASSES = [ + // Transparent full-screen layers that context menus and action widgets render to capture clicks. + // They sit in higher z-index stacking contexts above other UI, but are not tracked overlays, + // so hit-testing must skip them to find the overlay actually painted underneath. + 'context-view-block', + 'context-view-pointerBlock', -function isContextViewBlocker(element: Element): boolean { - return CONTEXT_VIEW_BLOCKER_CLASSES.some(className => element.classList.contains(className)); + // Webview overlay elements exist in their own DOM structure and are positioned dynamically, + // so they interfere with hit-testing because they are not descendants of the tracked overlay. + // Ignore them and depend on the element the webview is anchored to for overlay detection. + 'webview', + 'webview-overlay-content' +]; + +function isExcludedFromOverlayHitTest(element: Element): boolean { + return HIT_TEST_EXCLUDED_CLASSES.some(className => element.classList.contains(className)); } export const IBrowserOverlayManager = createDecorator('browserOverlayManager'); @@ -293,14 +302,14 @@ export class BrowserOverlayManager extends Disposable implements IBrowserOverlay // overlay state change, which can fire frequently, so favor it whenever the // topmost hit is a real element we care about. const elementAtPoint = root.elementFromPoint(clientX, clientY); - if (elementAtPoint && !isContextViewBlocker(elementAtPoint)) { + if (elementAtPoint && !isExcludedFromOverlayHitTest(elementAtPoint)) { return elementAtPoint; } - // Slow path: the topmost hit is a transparent context-view blocker (or there - // was no hit). Walk the full front-to-back hit list and return the first - // element that is not a blocker, i.e. the overlay actually painted beneath it. + // Slow path: the topmost hit is an excluded overlay (or there was no hit). + // Walk the full front-to-back hit list and return the first element + // that is not excluded, i.e. the overlay actually painted beneath it. return root.elementsFromPoint(clientX, clientY) - .find(el => !isContextViewBlocker(el)) ?? null; + .find(el => !isExcludedFromOverlayHitTest(el)) ?? null; }; const elementAtPoint = topmostAt(this.targetWindow.document); diff --git a/src/vs/workbench/contrib/browserView/test/electron-browser/overlayManager.test.ts b/src/vs/workbench/contrib/browserView/test/electron-browser/overlayManager.test.ts index 6d8decc9a51..28e7cc9305b 100644 --- a/src/vs/workbench/contrib/browserView/test/electron-browser/overlayManager.test.ts +++ b/src/vs/workbench/contrib/browserView/test/electron-browser/overlayManager.test.ts @@ -62,6 +62,29 @@ suite('BrowserOverlayManager', () => { assert.deepStrictEqual(overlays, []); }); + test('detects an overlay beneath detached webview content', () => { + const browserContainer = addElement('browser-container', { + position: 'absolute', left: '0px', top: '0px', width: '300px', height: '300px' + }); + const contextView = addElement('context-view', { + position: 'fixed', left: '0px', top: '0px', width: '200px', height: '200px' + }); + addElement('overlay-anchor', { + position: 'absolute', left: '0px', top: '0px', width: '200px', height: '200px' + }, contextView); + + const overlayContent = addElement('webview-overlay-content', { + position: 'fixed', left: '0px', top: '0px', width: '200px', height: '200px', zIndex: '1' + }); + addElement('webview', { + width: '100%', height: '100%' + }, overlayContent); + + const overlays = manager.getOverlappingOverlays(browserContainer); + + assert.deepStrictEqual(overlays.map(o => o.type), [BrowserOverlayType.Unknown]); + }); + // Regression test for #321088: a context menu (e.g. the "Add Models" // dropdown) renders a full-screen `.context-view-block` inside `.context-view` // that stacks above an already-open modal. The block isn't a tracked overlay From d1ced2b15b280d2cf7a77b12baafaf5e68f7076c Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:43:38 -0700 Subject: [PATCH 47/86] pet: go on the run, cool animation, better spin, not blocking chat, qol menu (#328334) * pet: go on the run, cool animation, better spin, not blocking chat, qol menu * fix slash commands * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Fix chat pet review feedback Co-authored-by: justschen <54879025+justschen@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/newChatInput.ts | 2 +- .../browser/actions/chatAccessibilityHelp.ts | 2 +- .../contrib/chat/browser/chatPetService.ts | 77 ++++++- .../chat/browser/widget/chatPetWidget.ts | 210 +++++++++++++++--- .../contrib/chat/browser/widget/chatWidget.ts | 15 +- .../chat/browser/widget/media/chat.css | 5 + .../chat/browser/widget/media/chatPet.css | 92 ++++++-- .../media/chatPet/buddy-cool-insiders-96.png | Bin 0 -> 459 bytes .../buddy-cool-insiders-96.spritesheet.png | Bin 0 -> 1381 bytes .../media/chatPet/buddy-cool-stable-96.png | Bin 0 -> 460 bytes .../buddy-cool-stable-96.spritesheet.png | Bin 0 -> 1414 bytes .../chatPet/buddy-search-insiders-96.png | Bin 0 -> 404 bytes .../buddy-search-insiders-96.spritesheet.png | Bin 0 -> 629 bytes .../media/chatPet/buddy-search-stable-96.png | Bin 0 -> 406 bytes .../buddy-search-stable-96.spritesheet.png | Bin 0 -> 631 bytes .../chatAccessibilityHelp.test.ts | 17 ++ .../test/browser/widget/chatPetWidget.test.ts | 91 +++++++- .../chat/chatFixtureUtils.ts | 4 + 18 files changed, 452 insertions(+), 63 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-insiders-96.png create mode 100644 src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-insiders-96.spritesheet.png create mode 100644 src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-stable-96.png create mode 100644 src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-stable-96.spritesheet.png create mode 100644 src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-search-insiders-96.png create mode 100644 src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-search-insiders-96.spritesheet.png create mode 100644 src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-search-stable-96.png create mode 100644 src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-search-stable-96.spritesheet.png diff --git a/src/vs/sessions/contrib/chat/browser/newChatInput.ts b/src/vs/sessions/contrib/chat/browser/newChatInput.ts index 3fe70328265..a51acc401d3 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatInput.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatInput.ts @@ -491,7 +491,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation this._createEditor(inputArea, editorOverflowWidgetsDomNode); const inputHasContent = observableFromEvent(this, this._editor.onDidChangeModelContent, () => this._editor.getValue().length > 0); - this._register(this.instantiationService.createInstance(ChatPetWidget, inputAreaWrapper, inputArea, constObservable(undefined), inputHasContent, this._editor.onDidChangeModelContent)); + this._register(this.instantiationService.createInstance(ChatPetWidget, inputAreaWrapper, inputArea, constObservable(undefined), inputHasContent, constObservable(true), this._editor.onDidChangeModelContent)); this._createInputToolbar(inputArea); const newChatBottomContainer = dom.append(parent, dom.$('.new-chat-bottom-container')); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index b0f1d9d3494..c8dbb6f7e65 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -82,7 +82,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui content.push(localize('chat.agentHostApprovalsPicker', 'When an agent session exposes approval presets, use Tab to reach the Approvals picker and choose how it handles workspace access, commands, and the internet.')); } content.push(localize('chat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.')); - content.push(localize('chat.vscodePet', 'Type /vscode-pet to show or hide the VS Code pet above the input. Drag it horizontally to reposition it, or use Tab to focus it and the left and right arrow keys to move it. Press Enter or Space to show it some love.')); + content.push(localize('chat.vscodePet', 'Type /vscode-pet to show or hide the VS Code pet above the input. Drag it horizontally to reposition it, or use Tab to focus it and the left and right arrow keys to move it. Press Enter or Space to show it some love. Open its context menu{0} (for example Shift+F10), use the up and down arrow keys to choose Go on the Run, Come Back, Stable Colors, or Insiders Colors, and press Enter to activate the choice.', '')); if (supportsFileReferences) { content.push(localize('chat.attachments.inlineReferences', 'To mention an attached context item at a specific position without removing it from the attached context, type # or @ and select the attachment from the suggestions.')); content.push(localize('chat.attachments.inlineReferenceHover', 'To inspect an inline attachment reference, place the cursor on it and invoke Show or Focus Hover{0}. Image references include a preview, while file and folder references include their path.', '')); diff --git a/src/vs/workbench/contrib/chat/browser/chatPetService.ts b/src/vs/workbench/contrib/chat/browser/chatPetService.ts index d659721eead..33dbef51721 100644 --- a/src/vs/workbench/contrib/chat/browser/chatPetService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatPetService.ts @@ -8,16 +8,45 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { IObservable, observableValue } from '../../../../base/common/observable.js'; import { localize } from '../../../../nls.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import product from '../../../../platform/product/common/product.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; const CHAT_PET_ENABLED_STORAGE_KEY = 'chat.vscodePet.enabled'; +const CHAT_PET_VARIANT_STORAGE_KEY = 'chat.vscodePet.variant'; +const CHAT_PET_ON_THE_RUN_STORAGE_KEY = 'chat.vscodePet.onTheRun'; + +export type ChatPetVariant = 'stable' | 'insiders'; + +type ChatPetEnablementEvent = { + enabled: boolean; + source: 'startup' | 'change'; +}; + +type ChatPetEnablementClassification = { + owner: 'justschen'; + comment: 'Tracks VS Code pet enablement so adoption can be measured.'; + enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the VS Code pet is enabled.' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the state was observed at startup or changed while VS Code was running.' }; +}; + +export function getChatPetVariant(configuredVariant: string | undefined, productQuality: string | undefined): ChatPetVariant { + if (configuredVariant === 'stable' || configuredVariant === 'insiders') { + return configuredVariant; + } + return productQuality === 'stable' ? 'stable' : 'insiders'; +} export const IChatPetService = createDecorator('chatPetService'); export interface IChatPetService { readonly _serviceBrand: undefined; readonly enabled: IObservable; + readonly variant: IObservable; + readonly onTheRun: IObservable; toggle(): boolean; + setVariant(variant: ChatPetVariant): void; + setOnTheRun(onTheRun: boolean): void; } export class ChatPetService extends Disposable implements IChatPetService { @@ -26,27 +55,71 @@ export class ChatPetService extends Disposable implements IChatPetService { private readonly _enabled; readonly enabled: IObservable; + private readonly _variant; + readonly variant: IObservable; + private readonly _onTheRun; + readonly onTheRun: IObservable; constructor( @IStorageService private readonly storageService: IStorageService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); this._enabled = observableValue(this, this.storageService.getBoolean(CHAT_PET_ENABLED_STORAGE_KEY, StorageScope.APPLICATION, false)); this.enabled = this._enabled; + this._variant = observableValue(this, getChatPetVariant(this.storageService.get(CHAT_PET_VARIANT_STORAGE_KEY, StorageScope.APPLICATION), product.quality)); + this.variant = this._variant; + this._onTheRun = observableValue(this, this.storageService.getBoolean(CHAT_PET_ON_THE_RUN_STORAGE_KEY, StorageScope.APPLICATION, false)); + this.onTheRun = this._onTheRun; this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, CHAT_PET_ENABLED_STORAGE_KEY, this._store)(() => { - this._enabled.set(this.storageService.getBoolean(CHAT_PET_ENABLED_STORAGE_KEY, StorageScope.APPLICATION, false), undefined); + this._setEnabled(this.storageService.getBoolean(CHAT_PET_ENABLED_STORAGE_KEY, StorageScope.APPLICATION, false)); })); + this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, CHAT_PET_VARIANT_STORAGE_KEY, this._store)(() => { + this._variant.set(getChatPetVariant(this.storageService.get(CHAT_PET_VARIANT_STORAGE_KEY, StorageScope.APPLICATION), product.quality), undefined); + })); + this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, CHAT_PET_ON_THE_RUN_STORAGE_KEY, this._store)(() => { + this._onTheRun.set(this.storageService.getBoolean(CHAT_PET_ON_THE_RUN_STORAGE_KEY, StorageScope.APPLICATION, false), undefined); + })); + this._logEnablement(this._enabled.get(), 'startup'); } toggle(): boolean { const enabled = !this._enabled.get(); - this._enabled.set(enabled, undefined); + this._setEnabled(enabled); this.storageService.store(CHAT_PET_ENABLED_STORAGE_KEY, enabled, StorageScope.APPLICATION, StorageTarget.USER); status(enabled ? localize('chatPet.enabled', "VS Code pet enabled. Click the pet to interact with it, or use the Left and Right Arrow keys to move it.") : localize('chatPet.disabled', "VS Code pet disabled")); return enabled; } + + private _setEnabled(enabled: boolean): void { + if (enabled === this._enabled.get()) { + return; + } + this._enabled.set(enabled, undefined); + this._logEnablement(enabled, 'change'); + } + + private _logEnablement(enabled: boolean, source: ChatPetEnablementEvent['source']): void { + this.telemetryService.publicLog2('chatPetEnablement', { enabled, source }); + } + + setVariant(variant: ChatPetVariant): void { + this._variant.set(variant, undefined); + this.storageService.store(CHAT_PET_VARIANT_STORAGE_KEY, variant, StorageScope.APPLICATION, StorageTarget.USER); + status(variant === 'stable' + ? localize('chatPet.variant.stable', "VS Code pet changed to the Stable colors") + : localize('chatPet.variant.insiders', "VS Code pet changed to the Insiders colors")); + } + + setOnTheRun(onTheRun: boolean): void { + this._onTheRun.set(onTheRun, undefined); + this.storageService.store(CHAT_PET_ON_THE_RUN_STORAGE_KEY, onTheRun, StorageScope.APPLICATION, StorageTarget.USER); + status(onTheRun + ? localize('chatPet.onTheRun', "The VS Code pet is on the run. Click the pet to bring it back.") + : localize('chatPet.restored', "The VS Code pet is back")); + } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatPetWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatPetWidget.ts index 117128b4ae1..2d9ec26039e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatPetWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatPetWidget.ts @@ -7,27 +7,31 @@ import './media/chatPet.css'; import * as dom from '../../../../../base/browser/dom.js'; import { GlobalPointerMoveMonitor } from '../../../../../base/browser/globalPointerMoveMonitor.js'; import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; +import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; import { status } from '../../../../../base/browser/ui/aria/aria.js'; +import { Action, IAction, Separator } from '../../../../../base/common/actions.js'; import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { KeyCode } from '../../../../../base/common/keyCodes.js'; -import { Disposable, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { FileAccess } from '../../../../../base/common/network.js'; import { autorun, IObservable, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; import { localize } from '../../../../../nls.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; -import product from '../../../../../platform/product/common/product.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IChatModel } from '../../common/model/chatModel.js'; -import { IChatPetService } from '../chatPetService.js'; +import { ChatPetVariant, IChatPetService } from '../chatPetService.js'; -export type ChatPetState = 'idle' | 'sleep' | 'waking' | 'typing' | 'rendering' | 'complete' | 'love' | 'clapping' | 'jump' | 'yapping' | 'yappingMouthOpen'; -export type ChatPetClickInteraction = Extract; +export type ChatPetState = 'idle' | 'sleep' | 'waking' | 'typing' | 'rendering' | 'complete' | 'love' | 'clapping' | 'jump' | 'cool' | 'yapping' | 'yappingMouthOpen' | 'onTheRun' | 'searching' | 'searchingDown'; +export type ChatPetClickInteraction = Extract; export const CHAT_PET_IDLE_SLEEP_DELAY = 20_000; const TRANSIENT_STATE_DURATION = 2_000; const COMPLETE_STATE_DURATION = 2_140; const LOVE_STATE_DURATION = 2_940; +const COOL_STATE_DURATION = 3_000; const WAKE_STATE_DURATION = 880; +const SEARCH_INTERVAL = 10_000; const DRAG_THRESHOLD = 2; const KEYBOARD_MOVE_DISTANCE = 8; const CHAT_PET_SOURCE_SIZE = 96; @@ -39,6 +43,8 @@ const TYPING_FRAME_DURATIONS = Array.from({ length: 8 }, () => 120); const SPEECH_FRAME_DURATIONS = [220, 220, 220, 100, 160, 180]; const CLAPPING_FRAME_DURATIONS = [80, 40, 40, 40, 80, 40, 40, 40, 40, 80, 40, 40, 80]; const LOVE_FRAME_DURATIONS = [200, 200, 380, 100, 80, 1_980]; +const COOL_FRAME_DURATIONS = [600, 120, 120, 120, 160, 80, 80, 80, 1_640]; +const SEARCH_FRAME_DURATIONS = [500, 500, 500, 500]; const YAPPING_FRAME_DURATIONS = [300, 240, 1_500, 240, 360]; interface ChatPetSpriteSource { @@ -62,11 +68,11 @@ export function getChatPetBuddyName(quality: string | undefined): 'buddy-idle-st return quality === 'stable' ? 'buddy-idle-stable' : 'buddy-idle-insiders'; } -let spriteSources: Record | undefined; -let speechSpriteSources: ChatPetSpriteSources | undefined; +const spriteSources = new Map>(); +const speechSpriteSources = new Map(); export function doesChatPetStateTrackCursor(state: ChatPetState | undefined): boolean { - return state !== undefined && state !== 'sleep' && state !== 'waking' && state !== 'typing' && state !== 'complete' && state !== 'love' && state !== 'yappingMouthOpen'; + return state !== undefined && state !== 'sleep' && state !== 'waking' && state !== 'typing' && state !== 'complete' && state !== 'love' && state !== 'cool' && state !== 'yappingMouthOpen' && state !== 'onTheRun' && state !== 'searching' && state !== 'searchingDown'; } export function getChatPetSpriteName(state: ChatPetState, quality: string | undefined): string { @@ -76,6 +82,12 @@ export function getChatPetSpriteName(state: ChatPetState, quality: string | unde return `buddy-love-${variant}`; case 'clapping': return `buddy-clapping-${variant}`; + case 'cool': + return `buddy-cool-${variant}`; + case 'onTheRun': + case 'searching': + case 'searchingDown': + return `buddy-search-${variant}`; case 'sleep': return `buddy-sleep-${variant}`; case 'waking': @@ -105,6 +117,13 @@ export function getChatPetFrameDurations(state: ChatPetState): readonly number[] return CLAPPING_FRAME_DURATIONS; case 'love': return LOVE_FRAME_DURATIONS; + case 'cool': + return COOL_FRAME_DURATIONS; + case 'searching': + return SEARCH_FRAME_DURATIONS; + case 'onTheRun': + case 'searchingDown': + return []; case 'yappingMouthOpen': return YAPPING_FRAME_DURATIONS; case 'yapping': @@ -127,7 +146,7 @@ function createSpriteSources(name: string, state: ChatPetState, tracksCursor = t animated: frameDurations.length === 0 ? staticSource : { url: FileAccess.asBrowserUri(`${root}/${name}${suffix}.spritesheet.png`).toString(true), frameDurations, - iterations: state === 'waking' ? 1 : Infinity, + iterations: state === 'waking' || state === 'cool' || state === 'searching' ? 1 : Infinity, }, reducedMotion: staticSource, }; @@ -137,10 +156,11 @@ export function getChatPetSpeechFrameDurations(): readonly number[] { return SPEECH_FRAME_DURATIONS; } -function getSpriteSources(): Record { - if (!spriteSources) { - const createStateSpriteSources = (state: ChatPetState) => createSpriteSources(getChatPetSpriteName(state, product.quality), state, doesChatPetStateTrackCursor(state)); - spriteSources = { +function getSpriteSources(variant: ChatPetVariant): Record { + let sources = spriteSources.get(variant); + if (!sources) { + const createStateSpriteSources = (state: ChatPetState) => createSpriteSources(getChatPetSpriteName(state, variant), state, doesChatPetStateTrackCursor(state)); + sources = { idle: createStateSpriteSources('idle'), sleep: createStateSpriteSources('sleep'), waking: createStateSpriteSources('waking'), @@ -150,20 +170,25 @@ function getSpriteSources(): Record { love: createStateSpriteSources('love'), clapping: createStateSpriteSources('clapping'), jump: createStateSpriteSources('jump'), + cool: createStateSpriteSources('cool'), yapping: createStateSpriteSources('yapping'), yappingMouthOpen: createStateSpriteSources('yappingMouthOpen'), + onTheRun: createStateSpriteSources('onTheRun'), + searching: createStateSpriteSources('searching'), + searchingDown: createStateSpriteSources('searchingDown'), }; + spriteSources.set(variant, sources); } - return spriteSources; + return sources; } -function getSpeechSpriteSources(): ChatPetSpriteSources { - if (!speechSpriteSources) { +function getSpeechSpriteSources(variant: ChatPetVariant): ChatPetSpriteSources { + let sources = speechSpriteSources.get(variant); + if (!sources) { const root = 'vs/workbench/contrib/chat/browser/widget/media/chatPet'; - const variant = product.quality === 'stable' ? 'stable' : 'insiders'; const name = `buddy-speech-${variant}-96`; - speechSpriteSources = { + sources = { animated: { url: FileAccess.asBrowserUri(`${root}/${name}.spritesheet.png`).toString(true), frameDurations: SPEECH_FRAME_DURATIONS, @@ -175,8 +200,9 @@ function getSpeechSpriteSources(): ChatPetSpriteSources { iterations: 1, }, }; + speechSpriteSources.set(variant, sources); } - return speechSpriteSources; + return sources; } function doesChatPetStateSpeak(state: ChatPetState | undefined): boolean { @@ -203,6 +229,10 @@ export function getChatPetBaseState(hasActiveRequest: boolean, needsInput: boole return 'idle'; } +export function isChatPetVisible(enabled: boolean, isLatestFocusedWidget: boolean): boolean { + return enabled && isLatestFocusedWidget; +} + export function getChatPetRenderedState(baseState: ChatPetState, transientState: ChatPetState | undefined, isDragging: boolean): ChatPetState { return isDragging ? 'idle' : transientState ?? baseState; } @@ -235,6 +265,8 @@ function getTransientStateDuration(state: ChatPetState): number { return COMPLETE_STATE_DURATION; case 'love': return LOVE_STATE_DURATION; + case 'cool': + return COOL_STATE_DURATION; case 'waking': return WAKE_STATE_DURATION; default: @@ -243,7 +275,7 @@ function getTransientStateDuration(state: ChatPetState): number { } export function getChatPetClickInteraction(random: number, previousInteraction?: ChatPetClickInteraction): ChatPetClickInteraction { - const interactions: readonly ChatPetClickInteraction[] = ['love', 'jump', 'yapping']; + const interactions: readonly ChatPetClickInteraction[] = ['love', 'jump', 'cool', 'yapping']; const availableInteractions = interactions.filter(interaction => interaction !== previousInteraction); return availableInteractions[Math.min(Math.floor(random * availableInteractions.length), availableInteractions.length - 1)]; } @@ -280,9 +312,11 @@ export class ChatPetWidget extends Disposable { private readonly _isDragging = observableValue(this, false); private readonly _idleScheduler = this._register(new RunOnceScheduler(() => this._idleExpired.set(true, undefined), CHAT_PET_IDLE_SLEEP_DELAY)); private readonly _transientScheduler = this._register(new RunOnceScheduler(() => this._transientState.set(undefined, undefined), TRANSIENT_STATE_DURATION)); + private readonly _searchScheduler: RunOnceScheduler; private readonly _clickSuppressionScheduler = this._register(new RunOnceScheduler(() => this._suppressNextPointerClick = false, 0)); private readonly _spriteAnimation = this._register(new MutableDisposable()); private readonly _speechAnimation = this._register(new MutableDisposable()); + private readonly _contextMenuActions = this._register(new MutableDisposable()); private _cursorPosition: readonly [number, number] | undefined; private _activeSprite: ChatPetSpriteElement | undefined; private _pendingSprite: ChatPetSpriteElement | undefined; @@ -296,21 +330,27 @@ export class ChatPetWidget extends Disposable { private _hasCustomPosition = false; private _suppressNextPointerClick = false; private _lastClickInteraction: ChatPetClickInteraction | undefined; + private _variant: ChatPetVariant; constructor( private readonly parent: HTMLElement, private readonly dragBounds: HTMLElement, model: IObservable, hasInput: IObservable, + isLatestFocusedWidget: IObservable, inputChanged: (listener: () => void) => IDisposable, @IChatPetService private readonly chatPetService: IChatPetService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { super(); + this._variant = this.chatPetService.variant.get(); + this._searchScheduler = this._register(new RunOnceScheduler(() => this._trySearch(), SEARCH_INTERVAL)); this.parent.classList.add('chat-pet-host'); + this.dragBounds.classList.add('chat-pet-drag-bounds'); this._button = this._register(new Button(this.parent, { - ariaLabel: localize('chatPet.interact', "Interact with the VS Code pet"), + ariaLabel: localize('chatPet.interact', "Interact with the VS Code pet. Use the context menu to put it on the run."), })); this._button.element.classList.add('chat-pet-button'); const resizeObserver = this._register(new dom.DisposableResizeObserver('ChatPetWidget.dragBounds', () => { @@ -356,18 +396,29 @@ export class ChatPetWidget extends Disposable { } })); const onAnimationComplete = (event: AnimationEvent) => { - if (event.animationName === 'chat-pet-exit' && !this._enabled) { + if (event.animationName === 'chat-pet-enter') { + this._button.element.classList.remove('entering'); + } else if (event.animationName === 'chat-pet-exit' && !this._enabled) { this._finishDisable(); } else if (event.animationName === 'chat-pet-yapping-fall' && !this._isDragging.get() && event.target === this._activeSprite?.container && this._button.element.dataset.state === 'yapping') { this._transientState.set('yappingMouthOpen', undefined); + } else if (event.animationName === 'chat-pet-search-down' && this._button.element.dataset.state === 'searchingDown') { + this._transientState.set(undefined, undefined); } }; this._register(dom.addDisposableListener(this._button.element, dom.EventType.ANIMATION_END, onAnimationComplete)); this._register(dom.addDisposableListener(this._button.element, 'animationcancel', onAnimationComplete)); this._register(dom.addDisposableListener(this._button.element, dom.EventType.POINTER_DOWN, event => this._startDrag(event))); this._register(dom.addDisposableListener(this._button.element, dom.EventType.KEY_DOWN, event => this._onKeyDown(event))); + this._register(dom.addDisposableListener(this._button.element, dom.EventType.CONTEXT_MENU, event => { + if (!this._enabled) { + return; + } + dom.EventHelper.stop(event, true); + this._showContextMenu(event); + })); this._register(inputChanged(() => { - if (this._enabled) { + if (this._enabled && !this.chatPetService.onTheRun.get()) { this._wake(); } })); @@ -379,6 +430,11 @@ export class ChatPetWidget extends Disposable { this._clickSuppressionScheduler.cancel(); return; } + if (this.chatPetService.onTheRun.get()) { + this._transientState.set(undefined, undefined); + this.chatPetService.setOnTheRun(false); + return; + } const wasSleeping = this._idleExpired.get() || this._renderedState === 'sleep'; if (wasSleeping) { this._wake(); @@ -397,6 +453,9 @@ export class ChatPetWidget extends Disposable { case 'jump': status(localize('chatPet.jumped', "The VS Code pet jumped")); break; + case 'cool': + status(localize('chatPet.cool', "The VS Code pet put on sunglasses")); + break; case 'yapping': status(localize('chatPet.yapping', "The VS Code pet is yapping")); break; @@ -406,7 +465,15 @@ export class ChatPetWidget extends Disposable { const motionReduced = observableFromEvent(this, this.accessibilityService.onDidChangeReducedMotion, () => this.accessibilityService.isMotionReduced()); this._register(autorun(reader => { this._motionReduced = motionReduced.read(reader); - const enabled = this.chatPetService.enabled.read(reader); + const enabled = isChatPetVisible(this.chatPetService.enabled.read(reader), isLatestFocusedWidget.read(reader)); + const variant = this.chatPetService.variant.read(reader); + const variantChanged = variant !== this._variant; + this._variant = variant; + const onTheRun = this.chatPetService.onTheRun.read(reader); + this._button.element.classList.toggle('on-the-run', onTheRun); + this._button.setAriaLabel(onTheRun + ? localize('chatPet.restore', "Bring back the VS Code pet") + : localize('chatPet.interact', "Interact with the VS Code pet. Use the context menu to put it on the run.")); const chatModel = model.read(reader); const request = chatModel?.lastRequestObs.read(reader); const needsInput = !!request?.response?.isPendingConfirmation.read(reader); @@ -432,6 +499,7 @@ export class ChatPetWidget extends Disposable { if (!enabled) { this._idleScheduler.cancel(); + this._searchScheduler.cancel(); this._transientScheduler.cancel(); if (transientState !== undefined) { this._transientState.set(undefined, undefined); @@ -442,6 +510,17 @@ export class ChatPetWidget extends Disposable { return; } + if (onTheRun) { + this._idleScheduler.cancel(); + if (!this._searchScheduler.isScheduled()) { + this._searchScheduler.schedule(); + } + const state = transientState === 'searching' || transientState === 'searchingDown' ? transientState : 'onTheRun'; + this._renderState(state, variantChanged); + return; + } + this._searchScheduler.cancel(); + if (this._busy) { this._idleScheduler.cancel(); if (idleExpired) { @@ -454,7 +533,7 @@ export class ChatPetWidget extends Disposable { } const baseState = getChatPetBaseState(hasActiveRequest, needsInput, inputHasContent, idleExpired); - this._renderState(getChatPetRenderedState(baseState, transientState, isDragging), false, isDragging); + this._renderState(getChatPetRenderedState(baseState, transientState, isDragging), variantChanged, isDragging); })); this._register(autorun(reader => { @@ -472,7 +551,7 @@ export class ChatPetWidget extends Disposable { } private _startDrag(event: PointerEvent): void { - if (!this._enabled || event.button !== 0) { + if (!this._enabled || this.chatPetService.onTheRun.get() || event.button !== 0) { return; } @@ -508,6 +587,41 @@ export class ChatPetWidget extends Disposable { }); } + private _showContextMenu(event: MouseEvent): void { + const onTheRun = this.chatPetService.onTheRun.get(); + const actions = new DisposableStore(); + this._contextMenuActions.value = actions; + const stable = actions.add(new Action('chat.pet.variant.stable', localize('chatPet.variant.stable.action', "Stable Colors"), undefined, true, () => this.chatPetService.setVariant('stable'))); + stable.checked = this.chatPetService.variant.get() === 'stable'; + const insiders = actions.add(new Action('chat.pet.variant.insiders', localize('chatPet.variant.insiders.action', "Insiders Colors"), undefined, true, () => this.chatPetService.setVariant('insiders'))); + insiders.checked = this.chatPetService.variant.get() === 'insiders'; + const onTheRunAction = actions.add(new Action( + 'chat.pet.onTheRun', + onTheRun ? localize('chatPet.comeBack.action', "Come Back") : localize('chatPet.goOnTheRun.action', "Go on the Run"), + undefined, + true, + () => { + this._transientState.set(undefined, undefined); + this.chatPetService.setOnTheRun(!onTheRun); + } + )); + const separator = new Separator(); + this.contextMenuService.showContextMenu({ + getAnchor: () => new StandardMouseEvent(dom.getWindow(this._button.element), event), + getActions: (): IAction[] => [ + onTheRunAction, + separator, + stable, + insiders, + ], + onHide: () => { + if (this._contextMenuActions.value === actions) { + this._contextMenuActions.clear(); + } + }, + }); + } + private _onKeyDown(event: KeyboardEvent): void { const keyboardEvent = new StandardKeyboardEvent(event); let delta: number; @@ -618,6 +732,19 @@ export class ChatPetWidget extends Disposable { } } + private _trySearch(): void { + if (!this._enabled || !this.chatPetService.onTheRun.get()) { + return; + } + if (this._motionReduced) { + this._searchScheduler.schedule(); + return; + } + this._transientState.set('searching', undefined); + this._renderState('searching', true); + this._searchScheduler.schedule(); + } + private _wake(): void { const wasSleeping = this._idleExpired.get() || this._renderedState === 'sleep'; this._idleExpired.set(false, undefined); @@ -642,7 +769,7 @@ export class ChatPetWidget extends Disposable { } private _renderState(state: ChatPetState, restart = false, useStaticSprite = false): void { - const sources = getSpriteSources()[state]; + const sources = getSpriteSources(this._variant)[state]; const source = this._motionReduced || useStaticSprite ? sources.reducedMotion : sources.animated; if (!restart && this._activeSprite && isChatPetImageSource(this._activeSprite.image, source.url)) { this._pendingSprite = undefined; @@ -676,11 +803,12 @@ export class ChatPetWidget extends Disposable { this._activeSprite?.container.classList.add('hidden'); sprite.container.classList.remove('hidden'); this._activeSprite = sprite; - this._startSpriteAnimation(this._pendingSource, sprite, this._spriteAnimation); - this._button.element.dataset.state = this._pendingState; - this._renderedState = this._pendingState; - this._eyes.classList.toggle('tracking', doesChatPetStateTrackCursor(this._pendingState)); - this._updateSpeechBubble(this._pendingState, true); + const state = this._pendingState; + this._startSpriteAnimation(this._pendingSource, sprite, this._spriteAnimation, () => this._onSpriteAnimationComplete(sprite, state)); + this._button.element.dataset.state = state; + this._renderedState = state; + this._eyes.classList.toggle('tracking', doesChatPetStateTrackCursor(state)); + this._updateSpeechBubble(state, true); this._pendingSprite = undefined; this._pendingSource = undefined; this._pendingState = undefined; @@ -690,7 +818,16 @@ export class ChatPetWidget extends Disposable { } } - private _startSpriteAnimation(source: ChatPetSpriteSource, sprite: ChatPetSpriteElement, animationDisposable: MutableDisposable): void { + private _onSpriteAnimationComplete(sprite: ChatPetSpriteElement, state: ChatPetState): void { + if (state !== 'searching' || sprite !== this._activeSprite || !this.chatPetService.onTheRun.get()) { + return; + } + this._transientState.set('searchingDown', undefined); + this._button.element.dataset.state = 'searchingDown'; + this._renderedState = 'searchingDown'; + } + + private _startSpriteAnimation(source: ChatPetSpriteSource, sprite: ChatPetSpriteElement, animationDisposable: MutableDisposable, onComplete?: () => void): void { const { frameDurations } = source; const { image, canvas } = sprite; const context = canvas.getContext('2d'); @@ -721,10 +858,15 @@ export class ChatPetWidget extends Disposable { const startTime = targetWindow.performance.now(); let currentFrame = 0; let animationFrame: number | undefined; + let completed = false; const updateFrame = (timestamp: number) => { const frame = getChatPetAnimationFrame(frameDurations, timestamp - startTime, source.iterations); if (frame.complete) { drawFrame(frame.frameIndex); + if (!completed) { + completed = true; + onComplete?.(); + } return; } if (frame.frameIndex !== currentFrame) { @@ -749,7 +891,7 @@ export class ChatPetWidget extends Disposable { return; } - const sources = getSpeechSpriteSources(); + const sources = getSpeechSpriteSources(this._variant); const source = this._motionReduced ? sources.reducedMotion : sources.animated; if (!isChatPetImageSource(this._speechBubble.image, source.url)) { this._speechAnimation.clear(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index d41dd80f526..7a3d5cc9102 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -91,7 +91,8 @@ import { getChatSessionType } from '../../common/model/chatUri.js'; import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js'; import { CHAT_READ_ONLY_BANNER_HEIGHT, ChatReadOnlyBanner } from './chatReadOnlyBanner.js'; import { IChatSubmitRequestHandlerService } from '../chatSubmitRequestHandlerService.js'; -import { ChatPetWidget } from './chatPetWidget.js'; +import { ChatPetWidget, isChatPetVisible } from './chatPetWidget.js'; +import { IChatPetService } from '../chatPetService.js'; const $ = dom.$; @@ -456,6 +457,7 @@ export class ChatWidget extends Disposable implements IChatWidget { @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IChatGoalSummaryService private readonly chatGoalSummaryService: IChatGoalSummaryService, @IChatSubmitRequestHandlerService private readonly chatSubmitRequestHandlerService: IChatSubmitRequestHandlerService, + @IChatPetService private readonly chatPetService: IChatPetService, ) { super(); @@ -804,7 +806,16 @@ export class ChatWidget extends Disposable implements IChatWidget { const inputContainer = this.inputPart.inputContainerElement; const petHost = inputContainer?.parentElement ?? this.inputPart.element; const inputHasContent = observableFromEvent(this, this.inputEditor.onDidChangeModelContent, () => this.inputEditor.getValue().length > 0); - this._register(this.instantiationService.createInstance(ChatPetWidget, petHost, inputContainer ?? petHost, this._viewModelObs.map(viewModel => viewModel?.model), inputHasContent, this.inputEditor.onDidChangeModelContent)); + const targetWindow = dom.getWindow(this.container); + const isLatestFocusedWidgetInWindow = observableValue(this, this.chatWidgetService.lastFocusedWidget === this); + this._register(this.chatWidgetService.onDidChangeFocusedWidget(focusedWidget => { + if (focusedWidget && dom.getWindow(focusedWidget.domNode) === targetWindow) { + isLatestFocusedWidgetInWindow.set(focusedWidget === this, undefined); + } + })); + const petVisible = derived(this, reader => isChatPetVisible(this.chatPetService.enabled.read(reader), isLatestFocusedWidgetInWindow.read(reader))); + this._register(autorun(reader => this.container.classList.toggle('chat-pet-enabled', petVisible.read(reader)))); + this._register(this.instantiationService.createInstance(ChatPetWidget, petHost, inputContainer ?? petHost, this._viewModelObs.map(viewModel => viewModel?.model), inputHasContent, petVisible, this.inputEditor.onDidChangeModelContent)); } this.renderWelcomeViewContentIfNeeded(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 2da2bb4f124..dc2784364bd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -46,6 +46,11 @@ -webkit-user-select: text; } +.interactive-session.chat-pet-enabled .interactive-item-container.chat-most-recent-response::after { + content: ""; + flex: 0 0 48px; +} + .interactive-item-container:not(:has(.chat-extensions-content-part)) .header { display: flex; align-items: center; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chatPet.css b/src/vs/workbench/contrib/chat/browser/widget/media/chatPet.css index 1f084c19a50..66161be4c03 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chatPet.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chatPet.css @@ -5,13 +5,19 @@ .chat-pet-host { position: relative; + isolation: isolate; +} + +.chat-pet-drag-bounds { + position: relative; + z-index: 1; } .chat-pet-button { position: absolute; right: var(--vscode-spacing-size320); bottom: 100%; - z-index: 100; + z-index: 0; width: 48px; height: 48px; padding: 0; @@ -20,6 +26,7 @@ background: transparent; cursor: grab; touch-action: none; + transition: transform 200ms ease-out; } .chat-pet-button.hidden { @@ -134,13 +141,13 @@ .chat-pet-button[data-state='complete'] .chat-pet-sprite { transform-origin: 50% 60%; - animation: chat-pet-complete-motion 2140ms steps(1, end); + animation: chat-pet-complete-motion 960ms steps(1, end); } .chat-pet-button[data-state='jump'] .chat-pet-sprite, .chat-pet-button[data-state='jump'] .chat-pet-eyes { transform-origin: 50% 60%; - animation: chat-pet-complete-motion 1560ms steps(1, end); + animation: chat-pet-complete-motion 960ms steps(1, end); } .chat-pet-button[data-state='yapping'] .chat-pet-sprite, @@ -154,6 +161,18 @@ transform: translateY(calc(-1 * var(--vscode-spacing-size40))) rotate(-90deg); } +.chat-pet-button.on-the-run { + transform: translateY(var(--vscode-spacing-size400)); +} + +.chat-pet-button[data-state='searching']:not(.exiting) { + animation: chat-pet-search-up 160ms steps(4, end) forwards; +} + +.chat-pet-button[data-state='searchingDown']:not(.exiting) { + animation: chat-pet-search-down 160ms steps(4, end) forwards; +} + .chat-pet-button[data-state='yapping'] .chat-pet-speech-bubble, .chat-pet-button[data-state='yappingMouthOpen'] .chat-pet-speech-bubble { left: calc(-1 * var(--vscode-spacing-size160)); @@ -249,31 +268,30 @@ transform: translateY(0) rotate(0); } - 12.5% { - transform: translateY(calc(-1 * var(--vscode-spacing-size40))) rotate(45deg); + 14.2857% { + transform: translateY(calc(-1 * var(--vscode-spacing-size40))) rotate(51deg); } - 25% { - transform: translateY(calc(-1 * var(--vscode-spacing-size80))) rotate(90deg); + 28.5714% { + transform: translateY(calc(-1 * var(--vscode-spacing-size80))) rotate(103deg); } - 37.5% { - transform: translateY(calc(-1 * var(--vscode-spacing-size80))) rotate(135deg); + 42.8571% { + transform: translateY(calc(-1 * var(--vscode-spacing-size80))) rotate(154deg); } - 50% { - transform: translateY(calc(-1 * var(--vscode-spacing-size80))) rotate(225deg); + 57.1429% { + transform: translateY(calc(-1 * var(--vscode-spacing-size80))) rotate(206deg); } - 62.5% { - transform: translateY(calc(-1 * var(--vscode-spacing-size80))) rotate(270deg); + 71.4286% { + transform: translateY(calc(-1 * var(--vscode-spacing-size80))) rotate(257deg); } - 75% { - transform: translateY(calc(-1 * var(--vscode-spacing-size40))) rotate(315deg); + 85.7143% { + transform: translateY(calc(-1 * var(--vscode-spacing-size40))) rotate(309deg); } - 87.5%, 100% { transform: translateY(0) rotate(360deg); } @@ -289,6 +307,34 @@ } } +@keyframes chat-pet-search-up { + 0% { + transform: translateY(var(--vscode-spacing-size400)); + } + + 50% { + transform: translateY(var(--vscode-spacing-size320)); + } + + 100% { + transform: translateY(var(--vscode-spacing-size160)); + } +} + +@keyframes chat-pet-search-down { + 0% { + transform: translateY(var(--vscode-spacing-size160)); + } + + 50% { + transform: translateY(var(--vscode-spacing-size320)); + } + + 100% { + transform: translateY(var(--vscode-spacing-size400)); + } +} + @keyframes chat-pet-eye-bob { 0%, 39.999% { @@ -315,6 +361,10 @@ } } +.monaco-workbench.monaco-reduce-motion .chat-pet-button { + transition: none; +} + .monaco-workbench.monaco-reduce-motion .chat-pet-button.entering, .monaco-workbench.monaco-reduce-motion .chat-pet-button.exiting, .monaco-workbench.monaco-reduce-motion .chat-pet-button.dragging.resisting, @@ -322,6 +372,8 @@ .monaco-workbench.monaco-reduce-motion .chat-pet-button[data-state='jump'] .chat-pet-sprite, .monaco-workbench.monaco-reduce-motion .chat-pet-button[data-state='jump'] .chat-pet-eyes, .monaco-workbench.monaco-reduce-motion .chat-pet-button[data-state='yapping'] .chat-pet-sprite, +.monaco-workbench.monaco-reduce-motion .chat-pet-button[data-state='searching'], +.monaco-workbench.monaco-reduce-motion .chat-pet-button[data-state='searchingDown'], .monaco-workbench.monaco-reduce-motion .chat-pet-eyes, .monaco-workbench.monaco-reduce-motion .chat-pet-pupil { animation: none; @@ -331,3 +383,11 @@ .monaco-workbench.monaco-reduce-motion .chat-pet-button[data-state='yapping'] .chat-pet-eyes { transform: translateY(calc(-1 * var(--vscode-spacing-size40))) rotate(-90deg); } + +.monaco-workbench.monaco-reduce-motion .chat-pet-button[data-state='searching'] { + transform: translateY(var(--vscode-spacing-size400)); +} + +.monaco-workbench.monaco-reduce-motion .chat-pet-button[data-state='searchingDown'] { + transform: translateY(var(--vscode-spacing-size400)); +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-insiders-96.png b/src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-insiders-96.png new file mode 100644 index 0000000000000000000000000000000000000000..e1cc103ba6e2578c4b14d4e682aed0027007fb28 GIT binary patch literal 459 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V4URX;uunK>+PL|en%Vx8XoE@ z?tId>Twm++{x?wy?@i-#jv6$7KjM*bQ4WxFPc?fL)h zO7)+|e|D|l-t*^Y9?)_YrmVZnX}haE#ee+%D*5&Ao!6V$JscGlBy4Mc_VaXO<-a%Y zN*C+h$T}zg%G-%!F3=tahmF+-PFNb2-o3l+?f&!K)7vkYb4Ps#`Gke((3CXCjsK5t zKJ@3A;hN%q_D61fXHUAW%(O&7AmFo{fL8UlV5pyaI* zz*l2vE(2;vUR=P1gw$sjDw zXy{$IYdP$#bYiAJ%z+wV^~Cev8lSEh0Re}NOaLIj2qjzwpxKMUW#1cPV6P~AWtrWY zT=GL(8!?_+xA!s>p&W?gv$=>o23{-zb<+gC_7IQ9Fd%g72t=cq8yaHmBgggq`6-%&l&nlxJY$Aw6d#u2A#kWqks)*?Ss~rUrufhQx5ekqz#Sc z1VQu!l2)~=w)W)hkvL|GLu3m1i7I306&wzJ9S8e)V+OZV?0D?=#o04GSpfyM1C@S#B z0Zn&z4D9_BoRwYp{^xt*S=r}(tFok3P?d{mBBzuHj8LjxU!zp9nL6Bk%0QPOFZwvL zsKbA|oXTuH0^qPJO!5${p2+HxLE790P+}gaXAPhF<*0ylzM2i}gXlV}{gP7*e>9VrALgOI?en?nX zr3gQW7)(iDOC0lPY2H!Vzn`x)cx~Y``2Qwzdlpkr@l`^YLv){q^zL!80#BkPjgerc zL^7`vWx$1Ndcu6RIHNU9Bi7BKPJIhWG*oE&el!AM*@2N&$r5?WpkRwF(Q99YsfS#%K5TR`Q#LV{L_ zEJ_%CXz~*mAsOz;GQttn0P|!fkp%>LbDoDoQKzG$b6PxPvn9nZoRsk z(%Qs%^+P4y_3+r>7wqa#{&L+BCywje}0i;u(P8WXT0$!VW!(c!?NZnLh*^| U)4|kc5$aulev}a?{*ZO;AEdTY_W%F@ literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-stable-96.png b/src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-stable-96.png new file mode 100644 index 0000000000000000000000000000000000000000..610ce31b22d3ec773aa4903fc09fb4d3ee6a1d23 GIT binary patch literal 460 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V4Upf;uunK>+PL|en%Vx8XoE@ z?tG$PA|bRvLuJ|r1`QFW{>Q8adl=YSUGG_cP}9&+k>H7G(wZf__Qt!nW$%CI7QamS zKl|p)w_lxrW-@HZG&%Xc;QOVoarHYgHm!4b_G4QO`)4NxMwSK!CWi(~1%LUuyZH5O z>?9Yk2r@8n03{Zjdd|2p=HB=357nP9eDR#u$bpjyraa+`-Txn7bY6YSzh2$T6VT+q zz^Kr`AdtYfV)glc)vt3Dn5QT(K?Hq@_-0hhpI_}afze9>)o-QjPiE- zzsLRGVX2kXINkm}*d&Lw)dzC^D`Yn8kDnhn|GeycTadl1n{7+x?U%7QKVLV`JnLXs z_P+};LQXGI8iZJwy6!T&t?OL*``xzO|K~OP*DrjplX3UsNd{F8mKM#qtVjRJpXd6# zeD#KNpZ_Z!tFsf3?gzQp;cT_TiZ|Qm8v)(zz`&uvz#;(lcjDxG!hSVjB3ky|QdRvC jY#NR%pdeykJaIt2OgQ-DrNgt_8Gyjk)z4*}Q$iB}b;hv_ literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-stable-96.spritesheet.png b/src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-cool-stable-96.spritesheet.png new file mode 100644 index 0000000000000000000000000000000000000000..724c684035073b1772164edf52499c4550f21867 GIT binary patch literal 1414 zcmZuxeK6Z+9DgEJ;fnO?u&zh0+sf9?OK-Gc5pTVfj+#@t7nN?enwmwV7EACm?K;kB z+%~T*Ryw-rWSh5P^0T#f7)vH)2=ObWQK30Wy@a3q_T;CQyRQE{_r2%7&-eTNynLVh zsK_^LZT8v#0BpmKheiWH<-nLj+X+vRa5obG%ZRYhpqLEhqUvYvzGDtn_XM;)#n>G` zn-lqmY^pEKC~Sjc{r3J4;}IRjcq=s6gC0?VQ=_8VuOGLrNwr5TELp`byXVeWoS;Xh zr4Lo*v0OWw{F^qiD*3W=I{i#deYt?^7?Z-wXqFl9@vE9x6iMV2rL!84Gx7xs0M0L! z(BLo*rnzjq;L*C;N}DfP2~Ah=iPJDu)69{XtHI z>UmZ)72~N!n|xOM|Au#w|5YbPd4H72NfBkFt%71{fWfH}8Q~A?=bwyE(X=wMzRPoZmJUAQq@Q;07b9ZIa42_=4ZLEzD$6*>OU|ch0nE{0I_}ytDT}``sc8%x2 za=ZSij@x|n^><1qJ{V57KiyHaz?Uu{TJ{zM#2`L|kQ9&|ES+Jmvv$spUs=rmlO;%A z9&{SKU?t4v`Od9xb7fBS?qG6WWvh|T(vX`oyI$>8XU_-^Bq;4jxF+(CNp!lts#iOUvr`Ui!Ke)Ei2V zN}r_)$4^mpbAsRIBF!9L6Z*N`dRB)6OskKltM#rS@}wf@9z=_)oe~EVr9FibIcjK`Q+pz74c=IT7PTYo?&(boU&aN}jm&7`Y@NWU9lT%a zLnKyb(&JY$`!HiEf+%5tKHAj&e<3LJixc^Bmf%AKVOI!e6in|diG+(~uT54;kPW@I zcTE2h@!JM_(K;NF>LeSR>M=RmQzrE7BEB?3C$$QkqZD|k@9o;el_Wg`@^)pywHHz6bEuMtXd+&XvI@J%YTEC@Rm8QK`k$^9250Cc4Q literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-search-insiders-96.png b/src/vs/workbench/contrib/chat/browser/widget/media/chatPet/buddy-search-insiders-96.png new file mode 100644 index 0000000000000000000000000000000000000000..49635ac929f568fbba7254b0c5a980455dc3f544 GIT binary patch literal 404 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V2t*3aSW-r_4e+@+(QWhZ4Zl& z2)&%kbn&t=Q;^jvkHBRM7Mu_f6bgOS<1k+?u*EKU0`HqcG3Wf|ZQt5b|EFmA?;pLL zK*Jb5+|Rgl{==SqWpelHO{R)3u-#X_?)|1)OV+(Vm%c%ai9>;bMSy|nNy98ldrNhm zxt})|H*sBP;$UE8X<%Ubp#GhCQnB0a%AQm2KZiDmu`od-EFXM-{%2vxa~9DBtbz<+ z!GtN64|1!|#yI>o(BI22eSPse&bT;fpnM|(lLG^zLPCk9Lgl&j=Qv*$?*m!@5^7)& zFtAPAUU%^K-G!fje1Df2RQzY{{eFb3z&GYYbqr7^ayU5hy{OK6UL;s2Z_sdlGLo|3 z-TEu4{Q4)pH~;dC$I^kD3G8?W2cHj>jy=L9Jh{LJcBeVC4`{kI+kUgum7n-uV_iPMNoPZH?ddFLT~LIq!M<{=aYA_iHmSumIi6;K0Bj0Ad2^j^_-Q z&ivjJKD+(Qui9roHGki}2l@}82EshiGLKoqCOtaq+;=X7ySRZ?l<$ixKrnT-U2zGe(B#>MusE03aHL#F!?Ft@U(cFJk6n#j_3hiDP|qU; z!jWQzk9O}W%pkwd<+xo4Xl|f*810-DGfpY0dF~e_X^Dq3r po}?^eO8;hi92g4rI21>H;m-c_g?ZxFi`fi7;OXk;vd$@?2>?cw=V2t&2aSW-r_4ck}(BTAuwuipU zrQ&5<*Q8o7teW+br^~v*G0DP3lr8ouv%!s1tFmmwHOxI+cI7zDzn-@2=j`pfH^=`s z=>;0cP>^ZzGCgAN9IO8P_}QnpFVy`hS@*u8e(J7$GtVdJvIsaZa40aaoM1F9t}F7B zuzH^UoJDj2s~`gt2LmI^gZOvsD$je${)$Y!|9mxrE(ePMSfc2_{qsMaSDxe4a^O~C z01F<_C_eD{-ISdTWtG8IjD5e?-x1z(*Aytv#K_XXz~pe?MUlg&Iq`FaFFmgTTF?j- zVqjEAs5*20Z&Ua8)&s?Ysen4_is<#zJK4=WdAbHssDZZ{=a&Qiw+H`LO`c5 zI502>Ffg!ymwnVsa#jX`?G11%Au5edKWCVC zuYHz$LTP2`6~@p;YL_5;24KkD~xCWek^2jtKN26W~p%Z9t# tq<^#58qHXFps|*JBT%*fS{w%dWl}lt< { }); }); + test('describes the VS Code pet context menu', () => { + const keybindingService = { + lookupKeybindings: () => [], + } as unknown as IKeybindingService; + const helpText = getAccessibilityHelpText('agentView', keybindingService, true); + + assert.deepStrictEqual({ + keybinding: helpText.includes(''), + navigation: helpText.includes('use the up and down arrow keys to choose'), + actions: helpText.includes('Go on the Run') && helpText.includes('Stable Colors') && helpText.includes('Insiders Colors'), + }, { + keybinding: true, + navigation: true, + actions: true, + }); + }); + test('only describes the selection side chat affordance in the sessions window', () => { const keybindingService = { lookupKeybindings: () => [], diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatPetWidget.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatPetWidget.test.ts index ec135ee9495..b5421fa02c4 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatPetWidget.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatPetWidget.test.ts @@ -5,11 +5,24 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { CHAT_PET_IDLE_SLEEP_DELAY, doesChatPetStateTrackCursor, getChatPetAnimationFrame, getChatPetBaseState, getChatPetBuddyName, getChatPetClickInteraction, getChatPetFrameDurations, getChatPetGazeDirection, getChatPetHorizontalPosition, getChatPetRenderedState, getChatPetSpeechFrameDurations, getChatPetSpriteName, isChatPetImageSource } from '../../../browser/widget/chatPetWidget.js'; +import { NullTelemetryServiceShape } from '../../../../../../platform/telemetry/common/telemetryUtils.js'; +import { TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; +import { ChatPetService, getChatPetVariant } from '../../../browser/chatPetService.js'; +import { CHAT_PET_IDLE_SLEEP_DELAY, doesChatPetStateTrackCursor, getChatPetAnimationFrame, getChatPetBaseState, getChatPetBuddyName, getChatPetClickInteraction, getChatPetFrameDurations, getChatPetGazeDirection, getChatPetHorizontalPosition, getChatPetRenderedState, getChatPetSpeechFrameDurations, getChatPetSpriteName, isChatPetImageSource, isChatPetVisible } from '../../../browser/widget/chatPetWidget.js'; suite('ChatPetWidget', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + class TestTelemetryService extends NullTelemetryServiceShape { + readonly events: { readonly name: string; readonly data: unknown }[] = []; + + override publicLog2(eventName?: string, data?: unknown): void { + if (eventName) { + this.events.push({ name: eventName, data }); + } + } + } test('maps chat activity to pet states by priority', () => { assert.deepStrictEqual([ @@ -29,6 +42,20 @@ suite('ChatPetWidget', () => { ]); }); + test('only shows in the latest focused chat widget when enabled', () => { + assert.deepStrictEqual([ + isChatPetVisible(false, false), + isChatPetVisible(false, true), + isChatPetVisible(true, false), + isChatPetVisible(true, true), + ], [ + false, + false, + false, + true, + ]); + }); + test('gives dragging precedence over base and transient states', () => { assert.deepStrictEqual([ getChatPetRenderedState('rendering', undefined, false), @@ -59,19 +86,51 @@ suite('ChatPetWidget', () => { ]); }); + test('resolves configured and product pet variants', () => { + assert.deepStrictEqual([ + getChatPetVariant('stable', 'insider'), + getChatPetVariant('insiders', 'stable'), + getChatPetVariant(undefined, 'stable'), + getChatPetVariant(undefined, 'insider'), + ], [ + 'stable', + 'insiders', + 'stable', + 'insiders', + ]); + }); + + test('logs pet enablement at startup and when toggled', () => { + const telemetryService = new TestTelemetryService(); + const service = disposables.add(new ChatPetService(disposables.add(new TestStorageService()), telemetryService)); + + service.toggle(); + service.toggle(); + + assert.deepStrictEqual(telemetryService.events, [ + { name: 'chatPetEnablement', data: { enabled: false, source: 'startup' } }, + { name: 'chatPetEnablement', data: { enabled: true, source: 'change' } }, + { name: 'chatPetEnablement', data: { enabled: false, source: 'change' } }, + ]); + }); + test('maps random values to click interactions', () => { assert.deepStrictEqual([ getChatPetClickInteraction(0), - getChatPetClickInteraction(0.32), - getChatPetClickInteraction(0.34), - getChatPetClickInteraction(0.66), - getChatPetClickInteraction(0.67), + getChatPetClickInteraction(0.24), + getChatPetClickInteraction(0.26), + getChatPetClickInteraction(0.49), + getChatPetClickInteraction(0.51), + getChatPetClickInteraction(0.74), + getChatPetClickInteraction(0.76), getChatPetClickInteraction(0.99), ], [ 'love', 'love', 'jump', 'jump', + 'cool', + 'cool', 'yapping', 'yapping', ]); @@ -83,6 +142,8 @@ suite('ChatPetWidget', () => { getChatPetClickInteraction(0.99, 'love'), getChatPetClickInteraction(0, 'jump'), getChatPetClickInteraction(0.99, 'jump'), + getChatPetClickInteraction(0, 'cool'), + getChatPetClickInteraction(0.99, 'cool'), getChatPetClickInteraction(0, 'yapping'), getChatPetClickInteraction(0.99, 'yapping'), ], [ @@ -91,7 +152,9 @@ suite('ChatPetWidget', () => { 'love', 'yapping', 'love', - 'jump', + 'yapping', + 'love', + 'cool', ]); }); @@ -104,8 +167,11 @@ suite('ChatPetWidget', () => { doesChatPetStateTrackCursor('rendering'), doesChatPetStateTrackCursor('complete'), doesChatPetStateTrackCursor('love'), + doesChatPetStateTrackCursor('cool'), doesChatPetStateTrackCursor('yapping'), doesChatPetStateTrackCursor('yappingMouthOpen'), + doesChatPetStateTrackCursor('onTheRun'), + doesChatPetStateTrackCursor('searching'), ], [ true, false, @@ -114,8 +180,11 @@ suite('ChatPetWidget', () => { true, false, false, + false, true, false, + false, + false, ]); }); @@ -126,6 +195,8 @@ suite('ChatPetWidget', () => { getChatPetSpriteName('waking', 'stable'), getChatPetSpriteName('typing', 'insider'), getChatPetSpriteName('rendering', 'stable'), + getChatPetSpriteName('cool', 'stable'), + getChatPetSpriteName('searching', 'stable'), getChatPetSpriteName('yappingMouthOpen', 'insider'), ], [ 'buddy-idle-insiders', @@ -133,6 +204,8 @@ suite('ChatPetWidget', () => { 'buddy-waking-stable', 'buddy-typing-insiders', 'buddy-rendering-stable', + 'buddy-cool-stable', + 'buddy-search-stable', 'buddy-yapping-insiders', ]); }); @@ -146,6 +219,8 @@ suite('ChatPetWidget', () => { getChatPetFrameDurations('rendering'), getChatPetFrameDurations('clapping'), getChatPetFrameDurations('love'), + getChatPetFrameDurations('cool'), + getChatPetFrameDurations('searching'), getChatPetFrameDurations('yapping'), getChatPetFrameDurations('yappingMouthOpen'), getChatPetSpeechFrameDurations(), @@ -157,6 +232,8 @@ suite('ChatPetWidget', () => { Array.from({ length: 50 }, () => 40), [80, 40, 40, 40, 80, 40, 40, 40, 40, 80, 40, 40, 80], [200, 200, 380, 100, 80, 1_980], + [600, 120, 120, 120, 160, 80, 80, 80, 1_640], + [500, 500, 500, 500], [], [300, 240, 1_500, 240, 360], [220, 220, 220, 100, 160, 180], diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts index 26b734e60e7..e25eb3feb57 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts @@ -173,7 +173,11 @@ export function registerChatFixtureServices(reg: ServiceRegistration, options: I reg.define(IChatService, MockChatService); reg.defineInstance(IChatPetService, new class extends mock() { override readonly enabled = observableValue('chatPetEnabled', false); + override readonly variant = observableValue('chatPetVariant', 'stable' as const); + override readonly onTheRun = observableValue('chatPetOnTheRun', false); override toggle() { return false; } + override setVariant() { } + override setOnTheRun() { } }()); reg.defineInstance(IChatWidgetService, new class extends mock() { override readonly lastFocusedWidget = undefined; From 2f841fe0016c9bddac8905cdb92392251f0a05f1 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Fri, 31 Jul 2026 10:03:48 +0500 Subject: [PATCH 48/86] markdown: fix: route undo and redo to the document history (#328245) * markdown: fix: route undo and redo to the document history The Agents window Markdown editor attaches an EditContext, so the browser keeps no native undo history and the Cmd+Z / Cmd+Shift+Z chords never reached VS Code. Wire them to the backing TextDocument's own history: - editor.ts: pass a `historyStrategy` to `EditorController` that posts `{ type: 'history', command }` to the extension. `record` is omitted so the TextDocument stays the single source of truth (no second local stack that would drift from the Edit menu, dirty state and hot exit). - markdownEditorProvider.ts: run the built-in `undo`/`redo` command; the active custom editor input scopes it to the resource's IUndoRedoService. - editor.ts: apply host `update` via `replaceSourceText` instead of `sourceText.set`, so the caret is mapped through the change (e.g. after an undo shrinks the document) and stale pending-paragraph state is cleared. Depends on the @vscode/markdown-editor API from microsoft/vscode-packages#189; the pinned version must be bumped once that is published. Fixes microsoft/vscode#327535 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5b60dce-b175-4fe6-b2b5-d18a229c9929 * markdown: chore: bump @vscode/markdown-editor to 0.0.2-40 Pick up the published `@vscode/markdown-editor` release that ships the `IHistoryStrategy` / `EditorControllerOptions.historyStrategy` and `EditorModel.replaceSourceText` APIs (microsoft/vscode-packages#189) the undo/redo integration depends on. Dependency set is unchanged from 0.0.2-26; lockfile integrity matches the published tarball. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5b60dce-b175-4fe6-b2b5-d18a229c9929 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5b60dce-b175-4fe6-b2b5-d18a229c9929 --- .../markdown-editor-src/editor.ts | 17 +++++++++++++++-- .../package-lock.json | 8 ++++---- .../markdown-language-features/package.json | 2 +- .../src/preview/markdownEditorProvider.ts | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/extensions/markdown-language-features/markdown-editor-src/editor.ts b/extensions/markdown-language-features/markdown-editor-src/editor.ts index cbad01b0f3a..7e1f83441ba 100644 --- a/extensions/markdown-language-features/markdown-editor-src/editor.ts +++ b/extensions/markdown-language-features/markdown-editor-src/editor.ts @@ -65,8 +65,12 @@ class Editor extends Disposable { break; } case 'update': { + // `replaceSourceText` (not `sourceText.set`) applies authoritative host + // text: it maps the selection through the change and clears stale + // pending-paragraph state, so the caret stays valid after an undo shrinks + // the document. The guard stops this echoing back as a user edit. this.isUpdatingFromExtension = true; - this.model.sourceText.set(new StringValue(message.content), undefined); + this.model.replaceSourceText(new StringValue(message.content)); this.isUpdatingFromExtension = false; break; } @@ -157,7 +161,16 @@ class Editor extends Disposable { }, })); - this._register(new EditorController(model, view)); + // Wire history chords (undo/redo) to the extension so they run against the + // backing TextDocument's own undo stack. `record` is deliberately omitted: + // the TextDocument owns the history, and a second local stack would drift + // from the Edit menu, dirty state and hot exit. + this._register(new EditorController(model, view, { + historyStrategy: { + undo: () => this.#vscode.postMessage({ type: 'history', command: 'undo' }), + redo: () => this.#vscode.postMessage({ type: 'history', command: 'redo' }), + }, + })); host.appendChild(view.element); // Render comments as the VS Code V2 markdown cards. The card colours come diff --git a/extensions/markdown-language-features/package-lock.json b/extensions/markdown-language-features/package-lock.json index 81cd695b3d7..b1992cfe641 100644 --- a/extensions/markdown-language-features/package-lock.json +++ b/extensions/markdown-language-features/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.9.8", - "@vscode/markdown-editor": "^0.0.2-37", + "@vscode/markdown-editor": "^0.0.2-40", "dompurify": "^3.4.10", "highlight.js": "^11.8.0", "katex": "^0.16.33", @@ -632,9 +632,9 @@ "integrity": "sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA==" }, "node_modules/@vscode/markdown-editor": { - "version": "0.0.2-37", - "resolved": "https://registry.npmjs.org/@vscode/markdown-editor/-/markdown-editor-0.0.2-37.tgz", - "integrity": "sha512-Glln7RyQ7dIl2v3OwiAGAcD+v3SdWjHRKdPr7XniVgVNOclnZeVsF29B67h4uDYSc2qEaqmAaGKXMG+0d+BQNA==", + "version": "0.0.2-40", + "resolved": "https://registry.npmjs.org/@vscode/markdown-editor/-/markdown-editor-0.0.2-40.tgz", + "integrity": "sha512-NPUmKKHDvauUM61SQpruezqVSi5Ly/+zmGZG89MFk0LZDYTklhGEiPhxQHvH6e3m+qaVbY268OkbajGEYRq7OQ==", "license": "MIT", "dependencies": { "@vscode/codicons": "^0.0.45", diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index f50f3a85734..902ad09437c 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -909,7 +909,7 @@ }, "dependencies": { "@vscode/extension-telemetry": "^0.9.8", - "@vscode/markdown-editor": "^0.0.2-37", + "@vscode/markdown-editor": "^0.0.2-40", "dompurify": "^3.4.10", "highlight.js": "^11.8.0", "katex": "^0.16.33", diff --git a/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts b/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts index 32ed30cfa1d..cdab2924fba 100644 --- a/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts +++ b/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts @@ -93,6 +93,20 @@ export class MarkdownEditorProvider extends Disposable implements vscode.CustomT await this.#globalState.update(MarkdownEditorProvider.#readonlyStateKey, !!message.readonly); break; } + case 'history': { + // The TextDocument owns undo/redo, so route the chord to the built-in + // command; the active custom editor input scopes it to this resource's + // history, shared with the Edit menu and Command Palette. Drain any + // in-flight edit first and only act while this panel is active, so the + // chord cannot race a pending edit or land on a different document. + if (message.command === 'undo' || message.command === 'redo') { + await editQueue; + if (webviewPanel.active) { + await vscode.commands.executeCommand(message.command); + } + } + break; + } case 'openLink': { await this.#linkOpener.openDocumentLink(message.href as string, document.uri); break; From be52ea55d41df45764ea0bbe1f739f072a75e301 Mon Sep 17 00:00:00 2001 From: Paul <8560030+pwang347@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:25:02 -0700 Subject: [PATCH 49/86] Try fix flaky tests (#328312) * Fix lost provider-change event leaving the new-session composer without a draft `_createNewSession` installed its `onDidChangeSessionTypes` retry listener only after awaiting `openNewSession`. When a provider started serving the folder during that await (e.g. the local agent host finishing its handshake on a cold start), the change fired into the gap and was lost, so the composer kept no draft and the harness picker stayed hidden forever. Watch for the change from before the await and replay it once the durable listener is installed. Fixes the dominant failure in #328281. * Keep the logs of failed flaky-smoke iterations The smoke runner deletes `.build/logs/smoke-tests-electron` on startup, so each iteration wipes the previous one and the published artifact only ever contains the last of 20 runs. A failure in any earlier iteration is therefore undiagnosable: no renderer/exthost/agenthost logs, screenshots or traces. Move the log directory aside on a failing iteration so the next run cannot reclaim the name. Only failing iterations are kept, so the artifact stays small. Refs #328281. --- .../darwin/product-smoke-flaky-darwin.yml | 12 +++- .../linux/product-smoke-flaky-linux.yml | 12 +++- .../win32/product-smoke-flaky-win32.yml | 13 +++- .../contrib/chat/browser/newChatWidget.ts | 63 ++++++++++++------- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml b/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml index 8b4ca271357..75ae75d044f 100644 --- a/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml +++ b/build/azure-pipelines/darwin/product-smoke-flaky-darwin.yml @@ -57,12 +57,22 @@ jobs: # 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. + # + # The smoke runner wipes .build/logs/smoke-tests-electron on startup, so a + # failing iteration's diagnostics would be destroyed by the next one and the + # published artifact would only ever hold the last iteration. Set the failed + # run aside under a name the runner does not touch. - ${{ 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" + status=0 + npm run smoketest-no-compile -- --tracing --build "$APP_ROOT/$APP_NAME" || status=$? + if [ $status -ne 0 ] && [ -d .build/logs/smoke-tests-electron ]; then + mv .build/logs/smoke-tests-electron ".build/logs/failed-iteration-${{ i }}" + fi + exit $status 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 index dcb8e804e08..708696264c1 100644 --- a/build/azure-pipelines/linux/product-smoke-flaky-linux.yml +++ b/build/azure-pipelines/linux/product-smoke-flaky-linux.yml @@ -100,10 +100,20 @@ jobs: # 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. + # + # The smoke runner wipes .build/logs/smoke-tests-electron on startup, so a + # failing iteration's diagnostics would be destroyed by the next one and the + # published artifact would only ever hold the last iteration. Set the failed + # run aside under a name the runner does not touch. - ${{ each i in parameters.iterations }}: - script: | set -e - npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" + status=0 + npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" || status=$? + if [ $status -ne 0 ] && [ -d .build/logs/smoke-tests-electron ]; then + mv .build/logs/smoke-tests-electron ".build/logs/failed-iteration-${{ i }}" + fi + exit $status env: TMPDIR: $(Agent.TempDirectory) LD_PRELOAD: $(VSCODE_SMOKE_LD_PRELOAD) diff --git a/build/azure-pipelines/win32/product-smoke-flaky-win32.yml b/build/azure-pipelines/win32/product-smoke-flaky-win32.yml index 87e8d519f89..22eec9876bb 100644 --- a/build/azure-pipelines/win32/product-smoke-flaky-win32.yml +++ b/build/azure-pipelines/win32/product-smoke-flaky-win32.yml @@ -60,8 +60,19 @@ jobs: # 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. + # + # The smoke runner wipes .build\logs\smoke-tests-electron on startup, so a + # failing iteration's diagnostics would be destroyed by the next one and the + # published artifact would only ever hold the last iteration. Set the failed + # run aside under a name the runner does not touch. - ${{ each i in parameters.iterations }}: - - powershell: npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + - powershell: | + npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $status = $LASTEXITCODE + if ($status -ne 0 -and (Test-Path .build\logs\smoke-tests-electron)) { + Move-Item .build\logs\smoke-tests-electron ".build\logs\failed-iteration-${{ i }}" + } + exit $status displayName: "🧪 Smoke test iteration ${{ i }}/${{ length(parameters.iterations) }} (Electron)" continueOnError: true timeoutInMinutes: 20 diff --git a/src/vs/sessions/contrib/chat/browser/newChatWidget.ts b/src/vs/sessions/contrib/chat/browser/newChatWidget.ts index 0f2a65d7f2c..7fee4d666cf 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatWidget.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatWidget.ts @@ -524,7 +524,21 @@ export class NewChatWidget extends Disposable { private async _createNewSession(folderUri: URI): Promise { this._pendingPreferredUpgrade.clear(); const userPick = this._newChatInput.sessionTypePicker.getUserPickedSessionType(); - const result = await this._createSessionNow(folderUri, userPick); + // Session creation is async, so a provider can start serving the folder + // (e.g. the local agent host finishing its handshake) between the call + // below and the listener installed after it. That change would land in + // the gap and be lost, leaving the composer without a draft — and with + // the harness picker hidden — until the user re-picks the workspace. + // Record it here so the listener can replay it. + const pendingChange = new DisposableStore(); + let changedWhilePending = false; + pendingChange.add(this.sessionsManagementService.onDidChangeSessionTypes(() => changedWhilePending = true)); + let result: IOpenNewSessionResult; + try { + result = await this._createSessionNow(folderUri, userPick); + } finally { + pendingChange.dispose(); + } if (result.trustDeclined) { // The user explicitly declined trust: don't schedule a retry, which // would silently recreate (and possibly re-prompt) the draft once a @@ -541,7 +555,7 @@ export class NewChatWidget extends Disposable { // (first) type, which can change as the folder's session-type list // grows. if (!result.session || !userPick || !this._isPreferredServable(folderUri, userPick)) { - this._scheduleRecreateOnProviderChange(folderUri, userPick, result.session); + this._scheduleRecreateOnProviderChange(folderUri, userPick, result.session, changedWhilePending); } return result; } @@ -568,30 +582,35 @@ export class NewChatWidget extends Disposable { } } - private _scheduleRecreateOnProviderChange(folderUri: URI, userPick: IPreferredSessionType | undefined, created: ISession | undefined): void { + private _scheduleRecreateOnProviderChange(folderUri: URI, userPick: IPreferredSessionType | undefined, created: ISession | undefined, replayMissedChange: boolean): void { const store = new DisposableStore(); - store.add(this.sessionsManagementService.onDidChangeSessionTypes(() => { - if (created) { - const active = this._session.get(); - if (active?.sessionId !== created.sessionId || active.isCreated.get()) { - return; // the draft was sent or is no longer the active session + store.add(this.sessionsManagementService.onDidChangeSessionTypes(() => this._recreateOnProviderChange(folderUri, userPick, created))); + this._pendingPreferredUpgrade.value = store; + if (replayMissedChange) { + this._recreateOnProviderChange(folderUri, userPick, created); + } + } + + private _recreateOnProviderChange(folderUri: URI, userPick: IPreferredSessionType | undefined, created: ISession | undefined): void { + if (created) { + const active = this._session.get(); + if (active?.sessionId !== created.sessionId || active.isCreated.get()) { + return; // the draft was sent or is no longer the active session + } + if (userPick) { + if (!this._isPreferredServable(folderUri, userPick)) { + return; // the preferred provider still cannot serve the folder } - if (userPick) { - if (!this._isPreferredServable(folderUri, userPick)) { - return; // the preferred provider still cannot serve the folder - } - } else { - // No explicit pick: keep the draft on the preferred (first) - // type. Recreate only when that preferred actually changed. - const preferred = this._newChatInput.sessionTypePicker.getPreferredSessionType(folderUri); - if (!preferred || (preferred.providerId === active.providerId && preferred.sessionTypeId === active.sessionType)) { - return; - } + } else { + // No explicit pick: keep the draft on the preferred (first) + // type. Recreate only when that preferred actually changed. + const preferred = this._newChatInput.sessionTypePicker.getPreferredSessionType(folderUri); + if (!preferred || (preferred.providerId === active.providerId && preferred.sessionTypeId === active.sessionType)) { + return; } } - void this._createNewSession(folderUri); - })); - this._pendingPreferredUpgrade.value = store; + } + void this._createNewSession(folderUri); } /** From 0beac730ddac50834ebdb4031b1ee90e054f046b Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Fri, 31 Jul 2026 12:52:15 +0500 Subject: [PATCH 50/86] sessions: keep feedback submit overlay inside diff (#328203) sessions: fix: keep feedback submit overlay inside diff Anchor the agent feedback overlay to the inset editor pane instead of the full editor group so the docked detail panel cannot capture its bottom-right placement. Add focused unit coverage and a dark/light component fixture for the single-pane geometry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 151ec481-fb28-49e1-8485-9229da423c15 --- .github/skills/sessions/SKILL.md | 1 + src/vs/sessions/LAYOUT.md | 2 + .../browser/agentFeedbackEditorOverlay.ts | 15 +- .../media/agentFeedbackEditorOverlay.css | 4 + .../agentFeedbackEditorOverlay.test.ts | 53 +++++ .../test/browser/agentsDiffEditor.fixture.ts | 191 ++++++++++++++++-- .../browser/parts/editor/editorGroupView.ts | 5 + 7 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorOverlay.test.ts diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index 1fa4a3778b0..07553d89e79 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -47,6 +47,7 @@ Then read the relevant spec for the area you are changing (see table below). If - **Line-number decoration tooltips belong in Monaco decoration options**: A `lineNumberClassName` node is regenerated as the editor renders and scrolls, so DOM-managed hovers can silently attach to a stale or never-decorated element. Set the localized `lineNumberHoverMessage` with the same decoration instead; Monaco's glyph hover controller follows the rendered line-number lifecycle. - **Compact multi-diff control alignment**: The file-header twistie, unchanged-region expand control, and fold control form one visual column in the Agents editor. Remove the header content's left padding and use the same small inset for both unchanged-region controls; do not let the shared multi-diff defaults leave each control at a separate horizontal offset. - **Embedded multi-diff gutters need a shared minimum width**: Each embedded editor otherwise sizes line numbers from its own largest line number, causing the content and nearby feedback glyph to appear to drift between file entries. Set a common `lineNumbersMinChars` width for the compact multi-diff; it remains stable through three-digit line numbers and grows only when a file exceeds that reserved capacity. +- **Editor-content overlays must anchor to the inset pane, not the full editor group**: In single-pane mode the editor group spans both the editor and docked detail panel, while `EditorGroupView.editorPaneContainer` bounds only the editor content. Mount submit/navigation overlays to that pane container so their bottom-right position stays inside the diff when the detail panel is visible or resized. - **Timeouts as fixes**: Never use `setTimeout`/`disposableTimeout`/arbitrary delays to fix bugs or implement behaviour. They are race-prone guesses that mask the real ordering/state problem. Drive logic off deterministic signals instead — observables (`autorun`/`derived`), explicit events (`onDidChange*`), lifecycle phases, or awaiting the actual async operation. - **Sticky prompt navigation must match the rail/title reveal**: Previous/Next and the sticky title must all reveal the prompt (request) row aligned to the top via the shared `reveal(requestId)` — the same path the dock/ruler rail uses. Do not align the following response to keep the header pinned, and do not add a "navigation pin" that forces the header to stay visible after a jump: the header is a `top:0` overlay, so it would cover the freshly top-aligned prompt (the prompt shows only a sliver). Let the header follow scroll tracking (it hides once the prompt is at the top), consistent with the dock. Use the chat request-bubble hover background (`--vscode-chat-requestBubbleHoverBackground`, toolbar hover as fallback), composited over the opaque panel base, for the sticky title affordance rather than an underline. - **Sticky prompt header transition is a label *roll*, not a moving band**: on prompt change the label text rolls (WAAPI slide+fade of absolutely-positioned line elements inside an `overflow:hidden` clip viewport), while the opaque band stays fixed. Do NOT translate the whole band to get an Explorer-style push-off: the band would move above the transcript top (its container `.interactive-session` is `overflow:visible`, so it'd overlap the session header) and clipping it would cut the band's soft drop-shadow. The roll gives the "header gives way to the next" feel with none of that risk. Gate the roll on the header already being visible (snap on first appearance/jumps) and honor `prefers-reduced-motion`. diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index ee5f780f304..73941d8015a 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -64,6 +64,8 @@ The **Sessions Part is the flexible ("remaining width") view** in the top-right The Sessions Part-to-Editor gap and the gap above the bottom Panel share `AGENTS_FLOATING_PANEL_GAP` in TypeScript layout and its registered CSS token, `--vscode-agents-layout-floatingPanelGap`. Their grid sashes keep the split boundaries unchanged, but expand and shift their hit areas to fill those visual gaps exactly. Each shows the standard persistent three-dot gripper at rest and yields to the full sash highlight while hovered or dragged. The Auxiliary Bar's leading padding and part-internal sashes retain their independent geometry. +Editor-content overlays must use the editor pane container rather than the editor-group root. In the single-pane layout, the group spans both the editor and the docked detail panel while the pane container is inset to the editor's actual bounds; anchoring feedback controls such as the Submit toolbar to the group would place them over the detail panel. + ### 2.3 Layout Priority Model The workbench grid is built with `proportionalLayout: false` (see `createWorkbenchLayout()` in [browser/workbench.ts](src/vs/sessions/browser/workbench.ts)). In this mode the split views do **not** distribute resize deltas proportionally — instead each delta (window resize, or a part being shown/hidden) is absorbed by the highest-`LayoutPriority` view, while the others keep their established sizes. Each part therefore declares an explicit `priority`: diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts index 699ef2b94f9..97960448aaf 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts @@ -182,19 +182,26 @@ export class AgentFeedbackOverlayWidget extends Disposable { } } -class AgentFeedbackOverlayController { +export interface IAgentFeedbackOverlayEditorGroup extends IEditorGroup { + readonly editorPaneContainer: HTMLElement; +} + +export class AgentFeedbackOverlayController { private readonly _store = new DisposableStore(); private readonly _domNode = document.createElement('div'); constructor( - container: HTMLElement, - group: IEditorGroup, + group: IAgentFeedbackOverlayEditorGroup, @IAgentFeedbackService agentFeedbackService: IAgentFeedbackService, @IInstantiationService instaService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, @ICodeReviewService codeReviewService: ICodeReviewService, ) { + const container = group.editorPaneContainer; + container.classList.add('agent-feedback-editor-overlay-host'); + this._store.add(toDisposable(() => container.classList.remove('agent-feedback-editor-overlay-host'))); + this._domNode.classList.add('agent-feedback-editor-overlay'); this._domNode.style.position = 'absolute'; this._domNode.style.bottom = '24px'; @@ -305,7 +312,7 @@ export class AgentFeedbackEditorOverlay implements IWorkbenchContribution { new ServiceCollection([IContextKeyService, group.scopedContextKeyService]) ); - const ctrl = scopedInstaService.createInstance(AgentFeedbackOverlayController, group.element, group); + const ctrl = scopedInstaService.createInstance(AgentFeedbackOverlayController, group); overlayWidgets.set(group, combinedDisposable(ctrl, scopedInstaService)); } } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css index ff76c10c317..c13c09b21e0 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.agent-feedback-editor-overlay-host { + position: relative; +} + .agent-feedback-editor-overlay-widget { padding: 2px 4px; color: var(--vscode-foreground); diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorOverlay.test.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorOverlay.test.ts new file mode 100644 index 00000000000..17807640893 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorOverlay.test.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { EditorGroupView } from '../../../../../workbench/browser/parts/editor/editorGroupView.js'; +import { IEditorGroupsService } from '../../../../../workbench/services/editor/common/editorGroupsService.js'; +import { createEditorPart, workbenchInstantiationService } from '../../../../../workbench/test/browser/workbenchTestServices.js'; +import { ICodeReviewService } from '../../../codeReview/browser/codeReviewService.js'; +import { AgentFeedbackEditorOverlay } from '../../browser/agentFeedbackEditorOverlay.js'; +import { IAgentFeedbackService } from '../../browser/agentFeedbackService.js'; + +suite('AgentFeedbackEditorOverlay', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('anchors the overlay host to the editor pane container', async () => { + const disposables = store.add(new DisposableStore()); + const instantiationService = workbenchInstantiationService(undefined, disposables); + const editorPart = await createEditorPart(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, editorPart); + instantiationService.stub(IAgentFeedbackService, new class extends mock() { + override readonly onDidChangeFeedback = Event.None; + override readonly onDidChangeNavigation = Event.None; + override readonly onDidChangeFeedbackScope = Event.None; + }); + instantiationService.stub(ICodeReviewService, new class extends mock() { }); + + const group = editorPart.activeGroup; + assert.ok(group instanceof EditorGroupView); + const fullEditorWidth = Number.parseInt(group.editorPaneContainer.style.width, 10); + group.setContentRightInset(300); + + const contribution = instantiationService.createInstance(AgentFeedbackEditorOverlay); + assert.deepStrictEqual({ + editorPaneWidthReduction: fullEditorWidth - Number.parseInt(group.editorPaneContainer.style.width, 10), + editorPaneIsHost: group.editorPaneContainer.classList.contains('agent-feedback-editor-overlay-host'), + editorGroupIsHost: group.element.classList.contains('agent-feedback-editor-overlay-host'), + }, { + editorPaneWidthReduction: 300, + editorPaneIsHost: true, + editorGroupIsHost: false, + }); + + contribution.dispose(); + assert.strictEqual(group.editorPaneContainer.classList.contains('agent-feedback-editor-overlay-host'), false); + }); +}); diff --git a/src/vs/sessions/contrib/changes/test/browser/agentsDiffEditor.fixture.ts b/src/vs/sessions/contrib/changes/test/browser/agentsDiffEditor.fixture.ts index 357894aac08..8cfd20b525c 100644 --- a/src/vs/sessions/contrib/changes/test/browser/agentsDiffEditor.fixture.ts +++ b/src/vs/sessions/contrib/changes/test/browser/agentsDiffEditor.fixture.ts @@ -7,9 +7,11 @@ import '../../browser/media/multiFileDiffEditor.css'; import '../../../agentFeedback/browser/media/agentFeedbackEditorInput.css'; import '../../../../../base/browser/ui/codicons/codiconStyles.js'; import { $, Dimension, getWindow } from '../../../../../base/browser/dom.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; import { Event, ValueWithChangeEvent } from '../../../../../base/common/event.js'; import { DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; import { constObservable } from '../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { MultiDiffEditorWidget } from '../../../../../editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.js'; @@ -18,25 +20,84 @@ import { RefCounted } from '../../../../../editor/browser/widget/diffEditor/util import { IDocumentDiffItem } from '../../../../../editor/browser/widget/multiDiffEditor/model.js'; import { IResourceLabel, IWorkbenchUIElementFactory } from '../../../../../editor/browser/widget/multiDiffEditor/workbenchUIElementFactory.js'; import { TestDiffProviderFactoryService } from '../../../../../editor/test/browser/diff/testDiffProviderFactoryService.js'; +import { IMenu, IMenuActionOptions, IMenuService, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { IWorkspace, IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { ResourceLabel } from '../../../../../workbench/browser/labels.js'; +import { IVisibleEditorPane } from '../../../../../workbench/common/editor.js'; import { IDecorationsService } from '../../../../../workbench/services/decorations/common/decorations.js'; +import { IEditorGroup } from '../../../../../workbench/services/editor/common/editorGroupsService.js'; import { IEditorProgressService } from '../../../../../platform/progress/common/progress.js'; import { INotebookDocumentService } from '../../../../../workbench/services/notebook/common/notebookDocumentService.js'; import { ITextFileService } from '../../../../../workbench/services/textfile/common/textfiles.js'; import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; +import { TestEditorInput } from '../../../../../workbench/test/browser/workbenchTestServices.js'; import { AgentFeedbackEditorInputContribution } from '../../../agentFeedback/browser/agentFeedbackEditorInputContribution.js'; -import { IAgentFeedbackService } from '../../../agentFeedback/browser/agentFeedbackService.js'; +import { AgentFeedbackOverlayController, IAgentFeedbackOverlayEditorGroup } from '../../../agentFeedback/browser/agentFeedbackEditorOverlay.js'; +import { clearAllFeedbackActionId, navigateNextFeedbackActionId, navigatePreviousFeedbackActionId, navigationBearingFakeActionId, submitFeedbackActionId } from '../../../agentFeedback/browser/agentFeedbackEditorActions.js'; +import { AgentFeedbackKind, AgentFeedbackState, IAgentFeedback, IAgentFeedbackService } from '../../../agentFeedback/browser/agentFeedbackService.js'; +import { Menus } from '../../../../browser/menus.js'; import { ISession } from '../../../../services/sessions/common/session.js'; const SESSION_RESOURCE = URI.parse('fixture-session://agents-diff'); const MODIFIED_FIRST_RESOURCE = URI.file('/workspace/src/first.ts'); +const OVERLAY_RESOURCE = URI.file('/workspace/changes.diff'); +const FIXTURE_WIDTH = 860; +const FIXTURE_HEIGHT = 620; +const DETAIL_WIDTH = 280; const UNCHANGED_LINES = Array.from({ length: 18 }, (_, index) => `const unchanged${index} = ${index};`).join('\n'); +class FixtureAgentFeedbackMenuService implements IMenuService { + + declare readonly _serviceBrand: undefined; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + createMenu(id: MenuId): IMenu { + if (id !== Menus.AgentFeedbackEditorContent) { + return { + onDidChange: Event.None, + dispose: () => { }, + getActions: () => [], + }; + } + const createAction = (actionId: string, title: string, icon: ThemeIcon) => this.instantiationService.createInstance( + MenuItemAction, + { id: actionId, title, icon }, + undefined, + { renderShortTitle: true }, + undefined, + undefined, + ); + const navigateActions = [ + createAction(navigationBearingFakeActionId, 'Navigation Status', Codicon.commentDiscussion), + createAction(navigatePreviousFeedbackActionId, 'Previous', Codicon.arrowUp), + createAction(navigateNextFeedbackActionId, 'Next', Codicon.arrowDown), + ]; + const submitActions = [ + createAction(submitFeedbackActionId, 'Submit', Codicon.send), + createAction(clearAllFeedbackActionId, 'Clear', Codicon.clearAll), + ]; + return { + onDidChange: Event.None, + dispose: () => { }, + getActions: () => [ + ['navigate', navigateActions], + ['a_submit', submitActions], + ], + }; + } + + getMenuActions(_id: MenuId, _contextKeyService: IContextKeyService, _options?: IMenuActionOptions) { return []; } + getMenuContexts() { return new Set(); } + resetHiddenStates() { } +} + class AgentsDiffUIElementFactory implements IWorkbenchUIElementFactory { constructor( @@ -65,7 +126,7 @@ function createFixtureSession(): ISession { }(); } -function createAgentFeedbackService(): IAgentFeedbackService { +function createAgentFeedbackService(feedback: readonly IAgentFeedback[] = [], feedbackScopeResource: URI = MODIFIED_FIRST_RESOURCE): IAgentFeedbackService { const session = createFixtureSession(); return new class extends mock() { override readonly onDidChangeFeedback = Event.None; @@ -75,38 +136,97 @@ function createAgentFeedbackService(): IAgentFeedbackService { return resource.toString() === MODIFIED_FIRST_RESOURCE.toString() ? session : undefined; } override getFeedbackSessionResource(resource: URI): URI | undefined { - return resource.toString() === MODIFIED_FIRST_RESOURCE.toString() ? SESSION_RESOURCE : undefined; + return resource.toString() === feedbackScopeResource.toString() ? SESSION_RESOURCE : undefined; } override getFeedback() { - return []; + return feedback; } override getNavigationBearing() { - return { activeIdx: -1, totalCount: 0 }; + return { activeIdx: feedback.length > 0 ? 0 : -1, totalCount: feedback.length }; } }(); } +class FixtureOverlayEditorGroup extends mock() implements IAgentFeedbackOverlayEditorGroup { + + override readonly onDidActiveEditorChange = Event.None; + override readonly onDidModelChange = Event.None; + override readonly activeEditor: TestEditorInput; + override readonly activeEditorPane: IVisibleEditorPane; + + constructor( + readonly editorPaneContainer: HTMLElement, + input: TestEditorInput, + ) { + super(); + this.activeEditor = input; + this.activeEditorPane = new class extends mock() { + override readonly input = input; + }(); + } + + override async closeEditor(): Promise { + return true; + } +} + function createContextKeyService(): IContextKeyService { return new class extends MockContextKeyService { override contextMatchesRules(): boolean { return true; } }(); } -async function renderAgentsDiffEditor({ container, disposableStore, disposableStackStore, theme }: ComponentFixtureContext): Promise { - container.classList.add('agent-sessions-workbench'); - container.style.width = '520px'; - container.style.height = '620px'; +interface IAgentsDiffFixtureOptions { + readonly showSubmitOverlay?: boolean; +} + +async function renderAgentsDiffEditor({ container, disposableStore, disposableStackStore, theme }: ComponentFixtureContext, options: IAgentsDiffFixtureOptions = {}): Promise { + const editorWidth = options.showSubmitOverlay ? FIXTURE_WIDTH - DETAIL_WIDTH : 520; + const fixtureWidth = options.showSubmitOverlay ? FIXTURE_WIDTH : editorWidth; + container.classList.add('agent-sessions-workbench', 'dock-detail-panel'); + container.style.width = `${fixtureWidth}px`; + container.style.height = `${FIXTURE_HEIGHT}px`; container.style.background = 'var(--vscode-agentsPanel-background)'; const editorPart = container.appendChild($('.part.editor')); + editorPart.style.position = 'relative'; + editorPart.style.width = '100%'; editorPart.style.height = '100%'; - const agentFeedbackService = createAgentFeedbackService(); + const editorContent = editorPart.appendChild($('.content')); + editorContent.style.width = '100%'; + editorContent.style.height = '100%'; + + const editorGroup = editorContent.appendChild($('.editor-group-container')); + editorGroup.style.position = 'relative'; + editorGroup.style.width = '100%'; + editorGroup.style.height = '100%'; + + const editorPane = editorGroup.appendChild($('.editor-container')); + editorPane.style.width = `${editorWidth}px`; + editorPane.style.height = '100%'; + + const editorInstance = editorPane.appendChild($('.editor-instance')); + editorInstance.style.width = '100%'; + editorInstance.style.height = '100%'; + + const feedback: readonly IAgentFeedback[] = options.showSubmitOverlay ? [{ + id: 'feedback-1', + text: 'Keep the submit control with the diff.', + resourceUri: MODIFIED_FIRST_RESOURCE, + range: { startLineNumber: 19, startColumn: 1, endLineNumber: 19, endColumn: 1 }, + sessionResource: SESSION_RESOURCE, + kind: AgentFeedbackKind.UserReview, + state: AgentFeedbackState.Accepted, + }] : []; + const agentFeedbackService = createAgentFeedbackService(feedback, options.showSubmitOverlay ? OVERLAY_RESOURCE : MODIFIED_FIRST_RESOURCE); const instantiationService = createEditorServices(disposableStore, { colorTheme: theme, additionalServices: reg => { + registerWorkbenchServices(reg); reg.defineInstance(IAgentFeedbackService, agentFeedbackService); reg.defineInstance(IContextKeyService, createContextKeyService()); + reg.define(IMenuService, FixtureAgentFeedbackMenuService); reg.defineInstance(IDecorationsService, new class extends mock() { override onDidChangeDecorations = Event.None; }()); reg.defineInstance(ITextFileService, new class extends mock() { override readonly untitled = new class extends mock() { override readonly onDidChangeLabel = Event.None; }(); }()); reg.defineInstance(IWorkspaceContextService, new class extends mock() { override onDidChangeWorkspaceFolders = Event.None; override getWorkspace(): IWorkspace { return { id: '', folders: [], configuration: undefined }; } }()); @@ -115,7 +235,6 @@ async function renderAgentsDiffEditor({ container, disposableStore, disposableSt show: () => ({ total: () => { }, worked: () => { }, done: () => { } }), }); reg.defineInstance(IDiffProviderFactoryService, new TestDiffProviderFactoryService()); - registerWorkbenchServices(reg); }, }); @@ -129,7 +248,7 @@ async function renderAgentsDiffEditor({ container, disposableStore, disposableSt const second = RefCounted.createOfNonDisposable({ original: secondOriginal, modified: secondModified }, { dispose() { } }); const widget = disposableStackStore.add(instantiationService.createInstance( MultiDiffEditorWidget, - editorPart, + editorInstance, instantiationService.createInstance(AgentsDiffUIElementFactory), { hideOriginalLineNumbers: true, @@ -144,9 +263,17 @@ async function renderAgentsDiffEditor({ container, disposableStore, disposableSt documents: ValueWithChangeEvent.const([first, second]), })); widget.setViewModel(viewModel); - widget.layout(new Dimension(520, 620)); + widget.layout(new Dimension(editorWidth, FIXTURE_HEIGHT)); disposableStackStore.add(toDisposable(() => widget.setViewModel(undefined))); + if (options.showSubmitOverlay) { + renderDockedDetailPanel(editorPart); + const input = disposableStackStore.add(new TestEditorInput(OVERLAY_RESOURCE, 'fixture.agentsDiff')); + const group = new FixtureOverlayEditorGroup(editorPane, input); + disposableStackStore.add(instantiationService.createInstance(AgentFeedbackOverlayController, group)); + return; + } + const targetWindow = getWindow(container); await new Promise(resolve => targetWindow.requestAnimationFrame(() => targetWindow.requestAnimationFrame(() => resolve()))); @@ -156,13 +283,49 @@ async function renderAgentsDiffEditor({ container, disposableStore, disposableSt } const lineNumber = editor?.getDomNode()?.querySelector('.line-numbers'); lineNumber?.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: lineNumber.getBoundingClientRect().left + 1, clientY: lineNumber.getBoundingClientRect().top + 1 })); - await new Promise(resolve => targetWindow.requestAnimationFrame(() => resolve())); } +function renderDockedDetailPanel(editorPart: HTMLElement): void { + const detail = editorPart.appendChild($('.part.auxiliarybar.docked-auxiliarybar')); + detail.style.position = 'absolute'; + detail.style.top = '0'; + detail.style.right = '0'; + detail.style.width = `${DETAIL_WIDTH}px`; + detail.style.height = '100%'; + detail.style.boxSizing = 'border-box'; + detail.style.background = 'var(--vscode-sideBar-background)'; + detail.style.borderLeft = 'var(--vscode-strokeThickness) solid var(--vscode-sideBar-border)'; + + const title = detail.appendChild($('.fixture-docked-detail-title')); + title.textContent = 'Files'; + title.style.height = '35px'; + title.style.boxSizing = 'border-box'; + title.style.padding = '8px 12px'; + title.style.fontWeight = 'var(--vscode-fontWeight-semiBold)'; + title.style.borderBottom = 'var(--vscode-strokeThickness) solid var(--vscode-sideBar-border)'; + + const files = detail.appendChild($('.fixture-docked-detail-files')); + files.style.padding = '8px 12px'; + for (const [name, stats] of [['first.ts', '+2 -1'], ['second.ts', '+1 -1'], ['README.md', '+4 -0']]) { + const row = files.appendChild($('.fixture-docked-detail-file')); + row.style.display = 'flex'; + row.style.justifyContent = 'space-between'; + row.style.padding = '6px 0'; + row.appendChild(document.createTextNode(name)); + const count = row.appendChild($('span')); + count.textContent = stats; + count.style.color = 'var(--vscode-descriptionForeground)'; + } +} + export default defineThemedFixtureGroup({ path: 'sessions/changes/' }, { CompactDiffWithFeedback: defineComponentFixture({ labels: { kind: 'screenshot' }, render: renderAgentsDiffEditor, }), + CompactDiffWithSubmitOverlay: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderAgentsDiffEditor(context, { showSubmitOverlay: true }), + }), }); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 3ba2b186260..9be6fd142cf 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1021,6 +1021,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.stickyCount; } + /** The container that bounds the editor pane, excluding any docked content inset. */ + get editorPaneContainer(): HTMLElement { + return this.editorContainer; + } + get activeEditorPane(): IVisibleEditorPane | undefined { return this.editorPane ? this.editorPane.activeEditorPane ?? undefined : undefined; } From 874db427990a102ac4e33e8b68ad1ec02532ab8a Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Fri, 31 Jul 2026 01:41:25 -0700 Subject: [PATCH 51/86] Improve notification handling to reduce duplicates (#328304) * Agent Host changes for main * fix tests --- .../notifications/notificationsToasts.ts | 30 ++-- .../pendingNotificationToasts.ts | 79 +++++++++++ .../test/browser/notificationsToasts.test.ts | 134 ++++++++++++++++++ 3 files changed, 234 insertions(+), 9 deletions(-) create mode 100644 src/vs/workbench/browser/parts/notifications/pendingNotificationToasts.ts create mode 100644 src/vs/workbench/test/browser/notificationsToasts.test.ts diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index bd77caab75a..67913a163b6 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -28,6 +28,7 @@ import { mainWindow } from '../../../../base/browser/window.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { DEFAULT_CUSTOM_TITLEBAR_HEIGHT } from '../../../../platform/window/common/window.js'; +import { PendingNotificationToasts } from './pendingNotificationToasts.js'; interface INotificationToast { readonly item: INotificationViewItem; @@ -72,6 +73,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast private readonly mapNotificationToToast = new Map(); private readonly mapNotificationToDisposable = new Map(); + private readonly pendingToasts: PendingNotificationToasts; private readonly notificationsToastsVisibleContextKey: IContextKey; @@ -93,6 +95,12 @@ export class NotificationsToasts extends Themable implements INotificationsToast super(themeService); this.notificationsToastsVisibleContextKey = NotificationsToastsVisibleContext.bindTo(contextKeyService); + this.pendingToasts = this._register(new PendingNotificationToasts( + item => this.model.notifications.includes(item), + (item, other) => item.equals(other), + callback => scheduleAtNextAnimationFrame(getWindow(this.container), callback) + )); + this._register(toDisposable(() => this.removeToasts())); this.registerListeners(); } @@ -192,6 +200,10 @@ export class NotificationsToasts extends Themable implements INotificationsToast } } + if (this.pendingToasts.tryReplace(item)) { + return; + } + // Optimization: it is possible that a lot of notifications are being // added in a very short time. To prevent this kind of spam, we protect // against showing too many notifications at once. Since they can always @@ -202,15 +214,10 @@ export class NotificationsToasts extends Themable implements INotificationsToast return; } - // Optimization: showing a notification toast can be expensive - // because of the associated animation. If the renderer is busy - // doing actual work, the animation can cause a lot of slowdown - // As such we use `scheduleAtNextAnimationFrame` to push out - // the toast until the renderer has time to process it. - // (see also https://github.com/microsoft/vscode/issues/107935) - const itemDisposables = new DisposableStore(); - this.mapNotificationToDisposable.set(item, itemDisposables); - itemDisposables.add(scheduleAtNextAnimationFrame(getWindow(this.container), () => this.doAddToast(item, itemDisposables))); + this.pendingToasts.add(item, (pendingItem, itemDisposables) => { + this.mapNotificationToDisposable.set(pendingItem, itemDisposables); + this.doAddToast(pendingItem, itemDisposables); + }); } private isElementInNotificationQuarter(element: HTMLElement): boolean { @@ -395,6 +402,8 @@ export class NotificationsToasts extends Themable implements INotificationsToast private removeToast(item: INotificationViewItem): void { let focusEditor = false; + this.pendingToasts.remove(item); + // UI const notificationToast = this.mapNotificationToToast.get(item); if (notificationToast) { @@ -432,6 +441,9 @@ export class NotificationsToasts extends Themable implements INotificationsToast private removeToasts(): void { + // Pending + this.pendingToasts.clear(); + // Toast this.mapNotificationToToast.clear(); diff --git a/src/vs/workbench/browser/parts/notifications/pendingNotificationToasts.ts b/src/vs/workbench/browser/parts/notifications/pendingNotificationToasts.ts new file mode 100644 index 00000000000..cbbbdb5e858 --- /dev/null +++ b/src/vs/workbench/browser/parts/notifications/pendingNotificationToasts.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * 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, toDisposable } from '../../../../base/common/lifecycle.js'; + +interface IPendingNotificationToast { + item: T; + readonly disposables: DisposableStore; + cleanupScheduled: boolean; +} + +export class PendingNotificationToasts implements IDisposable { + + private readonly pendingToasts = new Set>(); + + constructor( + private readonly isCurrent: (item: T) => boolean, + private readonly equals: (item: T, other: T) => boolean, + private readonly scheduler: (callback: () => void) => IDisposable + ) { } + + tryReplace(item: T): boolean { + for (const pendingToast of this.pendingToasts) { + if (this.equals(pendingToast.item, item)) { + pendingToast.item = item; + return true; + } + } + + return false; + } + + add(item: T, render: (item: T, disposables: DisposableStore) => void): void { + const disposables = new DisposableStore(); + const pendingToast: IPendingNotificationToast = { item, disposables, cleanupScheduled: false }; + this.pendingToasts.add(pendingToast); + disposables.add(toDisposable(() => this.pendingToasts.delete(pendingToast))); + // Defer toast creation to avoid animation work while the renderer is busy (#107935). + disposables.add(this.scheduler(() => { + const pendingItem = pendingToast.item; + if (!this.isCurrent(pendingItem)) { + disposables.dispose(); + return; + } + + this.pendingToasts.delete(pendingToast); + render(pendingItem, disposables); + })); + } + + remove(item: T): void { + for (const pendingToast of this.pendingToasts) { + if (pendingToast.item === item && !pendingToast.cleanupScheduled) { + pendingToast.cleanupScheduled = true; + // Allow a synchronous duplicate ADD to retarget the pending toast before cleanup. + queueMicrotask(() => { + pendingToast.cleanupScheduled = false; + if (this.pendingToasts.has(pendingToast) && !this.isCurrent(pendingToast.item)) { + pendingToast.disposables.dispose(); + } + }); + break; + } + } + } + + clear(): void { + for (const pendingToast of this.pendingToasts) { + pendingToast.disposables.dispose(); + } + this.pendingToasts.clear(); + } + + dispose(): void { + this.clear(); + } +} diff --git a/src/vs/workbench/test/browser/notificationsToasts.test.ts b/src/vs/workbench/test/browser/notificationsToasts.test.ts new file mode 100644 index 00000000000..7d7768b0e2e --- /dev/null +++ b/src/vs/workbench/test/browser/notificationsToasts.test.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Dimension, getWindow } from '../../../base/browser/dom.js'; +import { Event } from '../../../base/common/event.js'; +import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; +import { Severity } from '../../../platform/notification/common/notification.js'; +import { NotificationsToasts } from '../../browser/parts/notifications/notificationsToasts.js'; +import { NotificationsModel } from '../../common/notifications.js'; +import { workbenchInstantiationService } from './workbenchTestServices.js'; + +suite('NotificationsToasts', () => { + + suiteSetup(async () => { + const warmupDisposables = new DisposableStore(); + try { + const { model, toasts } = await createToasts(warmupDisposables); + const toastVisible = Event.toPromise(toasts.onDidChangeVisibility); + model.addNotification({ severity: Severity.Error, message: 'Warmup' }); + await toastVisible; + } finally { + warmupDisposables.dispose(); + } + }); + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + async function createToasts(testDisposables: Pick = disposables): Promise<{ + readonly container: HTMLElement; + readonly model: NotificationsModel; + readonly toasts: NotificationsToasts; + readonly flushAnimationFrame: () => Promise; + }> { + const container = document.createElement('div'); + const targetWindow = getWindow(container); + targetWindow.document.body.appendChild(container); + testDisposables.add(toDisposable(() => container.remove())); + + const instantiationService = workbenchInstantiationService(undefined, testDisposables); + const model = testDisposables.add(new NotificationsModel()); + testDisposables.add(toDisposable(() => { + for (const notification of [...model.notifications]) { + notification.close(); + } + })); + + const toasts = testDisposables.add(instantiationService.createInstance(NotificationsToasts, container, model)); + // Avoid viewport-dependent hiding because these tests assert scheduled toast counts. + toasts.layout(new Dimension(1024, Number.MAX_SAFE_INTEGER)); + await Promise.resolve(); + + return { + container, + model, + toasts, + flushAnimationFrame: () => new Promise(resolve => targetWindow.requestAnimationFrame(() => resolve())) + }; + } + + test('shows one toast for rapidly added duplicate notifications', async () => { + const { container, model, toasts } = await createToasts(); + const toastVisible = Event.toPromise(toasts.onDidChangeVisibility); + + for (let i = 0; i < 15; i++) { + model.addNotification({ severity: Severity.Error, message: 'Hello!' }); + } + await Promise.resolve(); + + const beforeAnimationFrame = { + notifications: model.notifications.length, + toasts: container.querySelectorAll('.notification-toast-container').length + }; + + await toastVisible; + assert.deepStrictEqual({ + beforeAnimationFrame, + notifications: model.notifications.length, + toasts: container.querySelectorAll('.notification-toast-container').length, + visible: toasts.isVisible + }, { + beforeAnimationFrame: { + notifications: 1, + toasts: 0 + }, + notifications: 1, + toasts: 1, + visible: true + }); + }); + + test('limits rapidly added distinct notification toasts', async () => { + const { container, model, toasts } = await createToasts(); + const toastVisible = Event.toPromise(toasts.onDidChangeVisibility); + + for (let i = 0; i < 15; i++) { + model.addNotification({ severity: Severity.Error, message: `Message ${i}` }); + } + + await toastVisible; + + assert.deepStrictEqual({ + notifications: model.notifications.length, + toasts: container.querySelectorAll('.notification-toast-container').length, + visible: toasts.isVisible + }, { + notifications: 15, + toasts: 3, + visible: true + }); + }); + + test('does not show a pending notification removed before rendering', async () => { + const { container, model, toasts, flushAnimationFrame } = await createToasts(); + const handle = model.addNotification({ severity: Severity.Error, message: 'Hello!' }); + + handle.close(); + await Promise.resolve(); + await flushAnimationFrame(); + + assert.deepStrictEqual({ + notifications: model.notifications.length, + toasts: container.querySelectorAll('.notification-toast-container').length, + visible: toasts.isVisible + }, { + notifications: 0, + toasts: 0, + visible: false + }); + }); +}); From 35341ebe12c424bf316fcfbcd7991f9030180c6e Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:48:50 +0200 Subject: [PATCH 52/86] Add /vscode-pet command reference to Pet menu label (#328361) The Pet context-menu item in the new-session view now shows "Pet (/vscode-pet)" to help users discover the command that toggles the feature. This aligns the menu label with the accessibility help documentation and the /vscode-pet slash command, making the interface more discoverable. Also updates the SESSIONS.md specification to document the updated menu label. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/SESSIONS.md | 21 ++++++++++--------- .../contrib/chat/browser/newChatWidget.ts | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/vs/sessions/SESSIONS.md b/src/vs/sessions/SESSIONS.md index 8540a1a035c..205004e9739 100644 --- a/src/vs/sessions/SESSIONS.md +++ b/src/vs/sessions/SESSIONS.md @@ -332,16 +332,17 @@ replacement. The new-session view mounts the aquarium action outside `.new-chat-widget-content`. Its surrounding surface has checked **Aquarium** and -**Pet** context-menu items. `AquariumService` owns the application-scoped action -visibility preference; `IChatPetService` owns the same persisted pet state used -by `/vscode-pet`. Context-menu events from inside `.new-chat-widget-content` are -left untouched so the composer retains its own context-menu behavior. The -aquarium preference is also keyboard-accessible through the **Developer: Toggle -Aquarium Action Visibility** command. `NewChatView` forwards its effective grid -visibility to the aquarium mount so a hidden composer cannot leave the aquarium -rendering behind the visible chat surface. Since `NewChatView` also hosts the -peer-chat composer, aquarium-specific lifecycle calls must first narrow the -wrapped widget to `NewChatWidget`. +**Pet (/vscode-pet)** context-menu items. `AquariumService` owns the +application-scoped action visibility preference; `IChatPetService` owns the same +persisted pet state used by `/vscode-pet`. Context-menu events from inside +`.new-chat-widget-content` are left untouched so the composer retains its own +context-menu behavior. The aquarium preference is also keyboard-accessible +through the **Developer: Toggle Aquarium Action Visibility** command. +`NewChatView` forwards its effective grid visibility to the aquarium mount so a +hidden composer cannot leave the aquarium rendering behind the visible chat +surface. Since `NewChatView` also hosts the peer-chat composer, +aquarium-specific lifecycle calls must first narrow the wrapped widget to +`NewChatWidget`. Agent feedback created while the active session is undefined or uncreated uses one shared new-session feedback scope, so it follows every undefined/uncreated diff --git a/src/vs/sessions/contrib/chat/browser/newChatWidget.ts b/src/vs/sessions/contrib/chat/browser/newChatWidget.ts index 7fee4d666cf..9d68f990c8e 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatWidget.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatWidget.ts @@ -287,7 +287,7 @@ export class NewChatWidget extends Disposable { )); const petAction = this._register(new Action( 'sessions.chatPet.toggle', - localize('petAction', "Pet"), + localize('petAction', "Pet (/vscode-pet)"), undefined, true, () => this.chatPetService.toggle() From 0fd71aca652f44c613a1ba07f66d67b39fa4f7e5 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 31 Jul 2026 02:23:13 -0700 Subject: [PATCH 53/86] Preserve non-pty shell output across runtime truncation snapshots (#328351) * preserve non-pty shell output across runtime truncation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fb7ed72c-b94f-4e59-bbf7-7797fcf9e69e * fix non-pty shell stream stitching edge cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f0076ad4-9973-4167-86e4-dd3b0fc9c84d --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fb7ed72c-b94f-4e59-bbf7-7797fcf9e69e Copilot-Session: f0076ad4-9973-4167-86e4-dd3b0fc9c84d --- .../copilot/copilotNonPtyShellTerminals.ts | 106 +++++-- .../test/node/copilotAgentSession.test.ts | 65 +++-- .../node/copilotNonPtyShellTerminals.test.ts | 265 ++++++++++++++++++ 3 files changed, 401 insertions(+), 35 deletions(-) create mode 100644 src/vs/platform/agentHost/test/node/copilotNonPtyShellTerminals.test.ts diff --git a/src/vs/platform/agentHost/node/copilot/copilotNonPtyShellTerminals.ts b/src/vs/platform/agentHost/node/copilot/copilotNonPtyShellTerminals.ts index 76aa44634c7..1d56eedb343 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotNonPtyShellTerminals.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotNonPtyShellTerminals.ts @@ -23,8 +23,8 @@ interface INonPtyShellStream { readonly uri: string; readonly title: string; created: boolean; - /** The last cumulative snapshot written to the channel. */ - lastEmitted: string; + lastSnapshot: string; + sourceTruncated: boolean; finalized: boolean; } @@ -44,6 +44,38 @@ function parseCompletedShell(text: string | undefined): TerminalCommandResult | }; } +const enum StitchConstants { + /** Minimum characters of overlap required to treat a rewritten snapshot as a rolling tail. */ + MinimumOverlapLength = 8 +} + +const partialOutputTruncationMarker = /\n?$/; + +function getTruncatedOutputPrefix(output: string): string | undefined { + const match = partialOutputTruncationMarker.exec(output); + return match ? output.slice(0, match.index) : undefined; +} + +/** + * Finds where `next` overlaps the end of `previous` when the runtime rewrote + * its cumulative snapshot as a rolling tail. + */ +function findStitchOverlap(previous: string, next: string): number | undefined { + const probe = next.slice(0, StitchConstants.MinimumOverlapLength); + if (probe.length < StitchConstants.MinimumOverlapLength) { + return undefined; + } + let index = previous.indexOf(probe); + while (index !== -1) { + const overlapLength = previous.length - index; + if (overlapLength <= next.length && next.startsWith(previous.slice(index))) { + return overlapLength; + } + index = previous.indexOf(probe, index + 1); + } + return undefined; +} + export interface INonPtyShellToolCompletion { readonly uri: string; readonly result?: TerminalCommandResult; @@ -56,10 +88,7 @@ export interface INonPtyShellToolCompletion { * via `tool.execution_partial_result` as throttled cumulative snapshots that * may be rewritten once output is truncated (a trailing truncation marker * under the emit cap, a rolling tail past the large-output threshold); this - * class emits only the unseen suffix as `terminal/data` while the snapshot - * grows in place, and resets the channel when the snapshot was rewritten, so - * subscribed clients receive live plain-text output (`isPty: false` — no VT - * parsing needed). + * class preserves the streamed transcript across those lossy rewrites. * * Created once per session and disposed with it, matching the pty-backed * `ShellManager` lifecycle. @@ -95,7 +124,8 @@ export class NonPtyShellTerminalStreams extends Disposable { this._streams.set(toolCallId, { uri: buildNonPtyShellTerminalUri(this._sessionUri, toolCallId), title, - lastEmitted: '', + lastSnapshot: '', + sourceTruncated: false, finalized: false, created: false, }); @@ -111,19 +141,39 @@ export class NonPtyShellTerminalStreams extends Disposable { if (created) { this._createTerminal(toolCallId, stream); } - if (stream.finalized || cumulativeOutput === stream.lastEmitted) { + if (stream.finalized || cumulativeOutput === stream.lastSnapshot) { return { uri: stream.uri, created }; } - if (cumulativeOutput.startsWith(stream.lastEmitted)) { - this._terminalManager.appendOutputTerminalData(stream.uri, cumulativeOutput.slice(stream.lastEmitted.length)); + const truncatedPrefix = getTruncatedOutputPrefix(cumulativeOutput); + if (truncatedPrefix !== undefined) { + if (!stream.sourceTruncated) { + if (cumulativeOutput.startsWith(stream.lastSnapshot)) { + this._terminalManager.appendOutputTerminalData(stream.uri, cumulativeOutput.slice(stream.lastSnapshot.length)); + } else { + const overlap = findStitchOverlap(stream.lastSnapshot, cumulativeOutput); + this._terminalManager.appendOutputTerminalData(stream.uri, overlap === undefined ? cumulativeOutput.slice(truncatedPrefix.length) : cumulativeOutput.slice(overlap)); + } + stream.sourceTruncated = true; + } + } else if (cumulativeOutput.startsWith(stream.lastSnapshot)) { + this._terminalManager.appendOutputTerminalData(stream.uri, cumulativeOutput.slice(stream.lastSnapshot.length)); } else { - // The snapshot no longer extends what we emitted — the runtime - // rewrote it after truncation (marker under the emit cap, rolling - // tail past the large-output threshold). Start the channel over. - this._terminalManager.resetOutputTerminal(stream.uri); - this._terminalManager.appendOutputTerminalData(stream.uri, cumulativeOutput); + const previousSnapshot = getTruncatedOutputPrefix(stream.lastSnapshot) ?? stream.lastSnapshot; + const overlap = findStitchOverlap(previousSnapshot, cumulativeOutput); + if (overlap !== undefined) { + const unseen = cumulativeOutput.slice(overlap); + if (unseen) { + this._terminalManager.appendOutputTerminalData(stream.uri, unseen); + } + } else if (stream.sourceTruncated || cumulativeOutput.length < stream.lastSnapshot.length) { + this._terminalManager.appendOutputTerminalData(stream.uri, cumulativeOutput); + stream.sourceTruncated = true; + } else { + this._terminalManager.resetOutputTerminal(stream.uri); + this._terminalManager.appendOutputTerminalData(stream.uri, cumulativeOutput); + } } - stream.lastEmitted = cumulativeOutput; + stream.lastSnapshot = cumulativeOutput; return { uri: stream.uri, created }; } @@ -145,11 +195,20 @@ export class NonPtyShellTerminalStreams extends Disposable { } return { uri: stream.uri, shouldRetire: false }; } - if (!stream.created) { + const created = !stream.created; + if (created) { this._createTerminal(toolCallId, stream); } - if (result.preview !== undefined) { - this.append(toolCallId, result.preview); + if (!stream.finalized && result.preview !== undefined) { + if (created) { + this.append(toolCallId, result.preview); + } else if (!result.truncated) { + if (stream.sourceTruncated || !result.preview.startsWith(stream.lastSnapshot)) { + this._replaceOutput(stream, result.preview); + } else { + this.append(toolCallId, result.preview); + } + } } if (result.exitCode !== undefined) { this._finalize(stream, result.exitCode); @@ -184,6 +243,15 @@ export class NonPtyShellTerminalStreams extends Disposable { this._terminalManager.finalizeOutputTerminal(stream.uri, exitCode); } + private _replaceOutput(stream: INonPtyShellStream, output: string): void { + this._terminalManager.resetOutputTerminal(stream.uri); + if (output) { + this._terminalManager.appendOutputTerminalData(stream.uri, output); + } + stream.lastSnapshot = output; + stream.sourceTruncated = false; + } + private _createTerminal(toolCallId: string, stream: INonPtyShellStream): void { const claim: TerminalSessionClaim = { kind: TerminalClaimKind.Session, diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index d9963270253..8eb7710d096 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -4266,8 +4266,9 @@ suite('CopilotAgentSession', () => { ]); }); - test('tool partial results reset the channel when the runtime rewrites its snapshot', async () => { - const { mockSession, terminalManager } = await createAgentSession(disposables); + test('truncated shell output streams through marker, rolling-tail, and completion transitions', async () => { + const { session, mockSession, signals, waitForSignal, terminalManager } = await createAgentSession(disposables); + session.resetTurnState('turn-truncated-stream'); const terminalUri = 'agenthost-terminal://shell/test-session-1/tc-rewrite'; mockSession.fire('tool.execution_start', { @@ -4277,27 +4278,59 @@ suite('CopilotAgentSession', () => { } as SessionEventPayload<'tool.execution_start'>['data']); mockSession.fire('tool.execution_partial_result', { toolCallId: 'tc-rewrite', - partialOutput: 'tick 1\n', - } as SessionEventPayload<'tool.execution_partial_result'>['data']); - // Once output is truncated the runtime rewrites its snapshot (a - // truncation marker under the emit cap, a rolling tail past the - // large-output threshold), so it stops being prefix-stable. - mockSession.fire('tool.execution_partial_result', { - toolCallId: 'tc-rewrite', - partialOutput: 'tick 1\n[...truncated 42 lines...]\n', + partialOutput: 'line 1\nline 498\nline 499\n', } as SessionEventPayload<'tool.execution_partial_result'>['data']); mockSession.fire('tool.execution_partial_result', { toolCallId: 'tc-rewrite', - partialOutput: 'tick 1\n[...truncated 99 lines...]\n', + partialOutput: 'line 1\nline 498\nline 499\n\n', } as SessionEventPayload<'tool.execution_partial_result'>['data']); + mockSession.fire('tool.execution_partial_result', { + toolCallId: 'tc-rewrite', + partialOutput: 'line 1\nline 498\nline 499\n\n', + } as SessionEventPayload<'tool.execution_partial_result'>['data']); + mockSession.fire('tool.execution_partial_result', { + toolCallId: 'tc-rewrite', + partialOutput: 'line 498\nline 499\nline 500\n', + } as SessionEventPayload<'tool.execution_partial_result'>['data']); + mockSession.fire('tool.execution_partial_result', { + toolCallId: 'tc-rewrite', + partialOutput: 'line 499\nline 500\nline 501\n', + } as SessionEventPayload<'tool.execution_partial_result'>['data']); + mockSession.fire('tool.execution_complete', { + toolCallId: 'tc-rewrite', + success: true, + result: { + content: 'Output too large', + contents: [{ + type: 'shell_exit', + shellId: '0', + exitCode: 0, + outputPreview: 'line 1\nline 2\n', + outputTruncated: true, + }], + }, + } as SessionEventPayload<'tool.execution_complete'>['data']); + await waitForSignal(signal => isAction(signal, ActionType.ChatToolCallComplete)); - assert.deepStrictEqual({ data: terminalManager.outputTerminalData, resets: terminalManager.outputTerminalResets }, { + const completed = getActions(signals).find(action => action.type === ActionType.ChatToolCallComplete) as ChatToolCallCompleteAction; + const terminalResult = completed.result.content?.find(content => content.type === ToolResultContentType.Terminal) as ToolResultTerminalContent | undefined; + assert.deepStrictEqual({ + data: terminalManager.outputTerminalData, + resets: terminalManager.outputTerminalResets, + finalized: terminalManager.outputTerminalsFinalized, + disposed: terminalManager.disposedTerminals, + result: terminalResult?.result, + }, { data: [ - { uri: terminalUri, data: 'tick 1\n' }, - { uri: terminalUri, data: '[...truncated 42 lines...]\n' }, - { uri: terminalUri, data: 'tick 1\n[...truncated 99 lines...]\n' }, + { uri: terminalUri, data: 'line 1\nline 498\nline 499\n' }, + { uri: terminalUri, data: '\n' }, + { uri: terminalUri, data: 'line 500\n' }, + { uri: terminalUri, data: 'line 501\n' }, ], - resets: [terminalUri], + resets: [], + finalized: [{ uri: terminalUri, exitCode: 0 }], + disposed: [terminalUri], + result: { exitCode: 0, preview: 'line 1\nline 2\n', truncated: true }, }); }); diff --git a/src/vs/platform/agentHost/test/node/copilotNonPtyShellTerminals.test.ts b/src/vs/platform/agentHost/test/node/copilotNonPtyShellTerminals.test.ts new file mode 100644 index 00000000000..37f1723c3b7 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/copilotNonPtyShellTerminals.test.ts @@ -0,0 +1,265 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, ok, strictEqual } from 'assert'; +import { URI } from '../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { NonPtyShellTerminalStreams } from '../../node/copilot/copilotNonPtyShellTerminals.js'; +import { TestAgentHostTerminalManager } from './testAgentHostTerminalManager.js'; + +suite('NonPtyShellTerminalStreams', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let manager: TestAgentHostTerminalManager; + let streams: NonPtyShellTerminalStreams; + + setup(() => { + manager = store.add(new TestAgentHostTerminalManager()); + streams = store.add(new NonPtyShellTerminalStreams(URI.parse('agenthost-session://test/session-1'), manager)); + }); + + function channelContent(): string { + return manager.outputTerminalData.map(d => d.data).join(''); + } + + suite('rolling-tail snapshot stitching', () => { + test('appends only the unseen suffix when the snapshot is a rolling tail, without resetting', () => { + streams.track('call-1', 'shell'); + streams.append('call-1', 'line 1\r\nline 2\r\nline 3\r\n'); + streams.append('call-1', 'line 2\r\nline 3\r\nline 4\r\n'); + streams.append('call-1', 'line 4\r\nline 5\r\nline 6\r\n'); + + deepStrictEqual(manager.outputTerminalResets, [], 'rolling tails must not reset the channel'); + strictEqual(channelContent(), 'line 1\r\nline 2\r\nline 3\r\nline 4\r\nline 5\r\nline 6\r\n'); + }); + + test('truncated completion preview does not discard the streamed transcript', () => { + streams.track('call-2', 'shell'); + streams.append('call-2', 'line 1\r\nline 2\r\nline 3\r\n'); + streams.append('call-2', 'line 3\r\nline 4\r\nline 5\r\n'); + + const completion = streams.completeToolCall('call-2', undefined, { + shellId: 'shell-1', + result: { exitCode: 0, preview: 'line 4\r\nline 5\r\n', truncated: true } + }); + + ok(completion); + deepStrictEqual(manager.outputTerminalResets, []); + strictEqual(channelContent(), 'line 1\r\nline 2\r\nline 3\r\nline 4\r\nline 5\r\n'); + deepStrictEqual(manager.outputTerminalsFinalized, [{ uri: completion.uri, exitCode: 0 }]); + }); + + test('preserves the transcript across truncation marker rewrites and disjoint rolling tails', () => { + streams.track('call-3', 'shell'); + streams.append('call-3', 'line 1\r\nline 498\r\nline 499\r\n'); + streams.append('call-3', 'line 1\r\nline 498\r\nline 499\r\n\n'); + streams.append('call-3', 'line 1\r\nline 498\r\nline 499\r\n\n'); + streams.append('call-3', 'line 498\r\nline 499\r\nline 500\r\n'); + streams.append('call-3', 'line 499\r\nline 500\r\nline 501\r\n'); + streams.append('call-3', 'line 700\r\nline 701\r\nline 702\r\n'); + + deepStrictEqual({ + resets: manager.outputTerminalResets, + content: channelContent(), + }, { + resets: [], + content: [ + 'line 1\r\nline 498\r\nline 499\r\n\n', + 'line 500\r\n', + 'line 501\r\n', + 'line 700\r\nline 701\r\nline 702\r\n', + ].join(''), + }); + }); + + test('recognizes the single-line character truncation marker', () => { + streams.track('call-4', 'shell'); + streams.append('call-4', 'abcdefghij'); + streams.append('call-4', 'abcdefghij'); + streams.append('call-4', 'abcdefghij'); + + deepStrictEqual({ + resets: manager.outputTerminalResets, + content: channelContent(), + }, { + resets: [], + content: 'abcdefghij', + }); + }); + + test('preserves a direct transition to disjoint shorter tails', () => { + streams.track('call-5', 'shell'); + streams.append('call-5', 'alpha beta gamma\r\n'); + streams.append('call-5', 'tail one\r\n'); + streams.append('call-5', 'tail two\r\n'); + + deepStrictEqual({ + resets: manager.outputTerminalResets, + content: channelContent(), + }, { + resets: [], + content: 'alpha beta gamma\r\ntail one\r\ntail two\r\n', + }); + }); + + test('does not append a truncated completion preview after streamed output', () => { + streams.track('call-6', 'shell'); + streams.append('call-6', 'line 1\r\nline 2\r\n\n'); + streams.append('call-6', 'line 498\r\nline 499\r\nline 500\r\n'); + + streams.completeToolCall('call-6', undefined, { + shellId: 'shell-1', + result: { exitCode: 0, preview: 'line 1\r\nline 2\r\n', truncated: true } + }); + + strictEqual(channelContent(), [ + 'line 1\r\nline 2\r\n\n', + 'line 498\r\nline 499\r\nline 500\r\n', + ].join('')); + }); + + test('seeds a zero-partial terminal from its truncated completion preview', () => { + streams.track('call-7', 'shell'); + + streams.completeToolCall('call-7', undefined, { + shellId: 'shell-1', + result: { exitCode: 0, preview: 'line 1\r\nline 2\r\n', truncated: true } + }); + + strictEqual(channelContent(), 'line 1\r\nline 2\r\n'); + }); + + test('replaces a truncated stream with an authoritative non-truncated completion preview', () => { + streams.track('call-8', 'shell'); + const appended = streams.append('call-8', 'head\r\n\n'); + ok(appended); + + streams.completeToolCall('call-8', undefined, { + shellId: 'shell-1', + result: { exitCode: 0, preview: 'complete output\r\n', truncated: false } + }); + + deepStrictEqual({ + resets: manager.outputTerminalResets, + data: manager.outputTerminalData, + }, { + resets: [appended.uri], + data: [ + { uri: appended.uri, data: 'head\r\n\n' }, + { uri: appended.uri, data: 'complete output\r\n' }, + ], + }); + }); + + test('clears stale streamed output when the authoritative completion preview is empty', () => { + streams.track('call-9', 'shell'); + const appended = streams.append('call-9', 'stale output\r\n'); + ok(appended); + + streams.completeToolCall('call-9', undefined, { + shellId: 'shell-1', + result: { exitCode: 0, preview: '', truncated: false } + }); + + deepStrictEqual({ + resets: manager.outputTerminalResets, + data: manager.outputTerminalData, + }, { + resets: [appended.uri], + data: [{ uri: appended.uri, data: 'stale output\r\n' }], + }); + }); + + test('appends a prefix-stable authoritative completion preview', () => { + streams.track('call-10', 'shell'); + const appended = streams.append('call-10', 'line 1\r\n'); + ok(appended); + + streams.completeToolCall('call-10', undefined, { + shellId: 'shell-1', + result: { exitCode: 0, preview: 'line 1\r\nline 2\r\n', truncated: false } + }); + + deepStrictEqual({ + resets: manager.outputTerminalResets, + data: manager.outputTerminalData, + }, { + resets: [], + data: [ + { uri: appended.uri, data: 'line 1\r\n' }, + { uri: appended.uri, data: 'line 2\r\n' }, + ], + }); + }); + + test('an unrelated rewrite still resets the channel', () => { + streams.track('call-11', 'shell'); + streams.append('call-11', 'alpha beta gamma\r\n'); + streams.append('call-11', 'completely different content\r\n'); + + strictEqual(manager.outputTerminalResets.length, 1); + deepStrictEqual(manager.outputTerminalData.map(d => d.data), ['alpha beta gamma\r\n', 'completely different content\r\n']); + }); + }); + + suite('completion and lifecycle', () => { + test('parses fallback completion, finalizes once, and ignores later output', () => { + streams.track('call-12', 'shell'); + + const completion = streams.completeToolCall('call-12', 'fallback output\r\n', undefined); + streams.completeToolCall('call-12', 'different output\r\n', undefined); + streams.append('call-12', 'late output\r\n'); + + deepStrictEqual({ + completion, + content: channelContent(), + finalized: manager.outputTerminalsFinalized, + }, { + completion: { + uri: 'agenthost-terminal://shell/session-1/call-12', + result: { exitCode: -1, preview: 'fallback output\r\n' }, + shouldRetire: true, + }, + content: 'fallback output\r\n', + finalized: [{ uri: 'agenthost-terminal://shell/session-1/call-12', exitCode: -1 }], + }); + }); + + test('drops an unstarted stream without completion data', () => { + streams.track('call-13', 'shell'); + + strictEqual(streams.completeToolCall('call-13', undefined, undefined), undefined); + strictEqual(streams.append('call-13', 'late output'), undefined); + }); + + test('keeps a started stream alive without completion data', () => { + streams.track('call-14', 'shell'); + const appended = streams.append('call-14', 'partial output'); + ok(appended); + + deepStrictEqual(streams.completeToolCall('call-14', undefined, undefined), { + uri: appended.uri, + shouldRetire: false, + }); + }); + + test('retires a stream exactly once', () => { + streams.track('call-15', 'shell'); + const appended = streams.append('call-15', 'partial output'); + ok(appended); + + streams.retire('call-15'); + streams.retire('call-15'); + + deepStrictEqual(manager.disposedTerminals, [appended.uri]); + strictEqual(streams.append('call-15', 'late output'), undefined); + }); + + test('ignores append and completion for an untracked tool call', () => { + strictEqual(streams.append('missing', 'output'), undefined); + strictEqual(streams.completeToolCall('missing', undefined, undefined), undefined); + }); + }); +}); From e43a72941acc2ac4012ff06b59ada8d17be5a6cf Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 31 Jul 2026 10:46:24 +0200 Subject: [PATCH 54/86] Fixes component explorer artifact codicons --- build/rspack/rspack.serve-out.config.mts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/rspack/rspack.serve-out.config.mts b/build/rspack/rspack.serve-out.config.mts index d533ce29a29..760c9880d12 100644 --- a/build/rspack/rspack.serve-out.config.mts +++ b/build/rspack/rspack.serve-out.config.mts @@ -98,6 +98,9 @@ export default { { test: /\.ttf$/, type: 'asset/resource', + generator: { + publicPath: isStaticComponentExplorerBuild ? '../' : '/', + }, }, { // Built-in theme JSON files use JSONC (comments / trailing From 8a0c23856e8d0feb64a007b4e9f1164b1efbb8c3 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 30 Jul 2026 15:00:12 -0700 Subject: [PATCH 55/86] Dispose partial multi-diff model references on failure Wait for both model reference acquisitions and dispose any successful references when the other side fails or the cached item is removed.\n\n(Written by Copilot)\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/multiDiffEditorInput.ts | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts index 7cb8aff0c84..2584796279e 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts @@ -191,17 +191,31 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor const multiDiffItemStore = new DisposableStore(); - try { - [original, modified] = await Promise.all([ - r.originalUri ? this._textModelService.createModelReference(r.originalUri) : undefined, - r.modifiedUri ? this._textModelService.createModelReference(r.modifiedUri) : undefined, - ]); + const [originalResult, modifiedResult] = await Promise.allSettled([ + r.originalUri ? this._textModelService.createModelReference(r.originalUri) : undefined, + r.modifiedUri ? this._textModelService.createModelReference(r.modifiedUri) : undefined, + ]); + + if (originalResult.status === 'fulfilled') { + original = originalResult.value; if (original) { multiDiffItemStore.add(original); } + } + if (modifiedResult.status === 'fulfilled') { + modified = modifiedResult.value; if (modified) { multiDiffItemStore.add(modified); } - } catch (e) { + } + + if (store.isDisposed) { + multiDiffItemStore.dispose(); + return undefined; + } + + const errorResult = originalResult.status === 'rejected' ? originalResult : modifiedResult.status === 'rejected' ? modifiedResult : undefined; + if (errorResult) { + multiDiffItemStore.dispose(); // e.g. "File seems to be binary and cannot be opened as text" - console.error(e); - onUnexpectedError(e); + console.error(errorResult.reason); + onUnexpectedError(errorResult.reason); return undefined; } From 92abb765af777baa2a424025882a32f52408a21d Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 30 Jul 2026 16:42:10 -0700 Subject: [PATCH 56/86] Address multi-diff cleanup review feedback Preserve handling for synchronous model-reference failures and make rejection selection easier to follow.\n\n(Written by Copilot)\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../multiDiffEditor/browser/multiDiffEditorInput.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts index 2584796279e..424b9236081 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts @@ -190,10 +190,11 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor let modified: IReference | undefined; const multiDiffItemStore = new DisposableStore(); + const createModelReference = async (resource: URI | undefined) => resource ? this._textModelService.createModelReference(resource) : undefined; const [originalResult, modifiedResult] = await Promise.allSettled([ - r.originalUri ? this._textModelService.createModelReference(r.originalUri) : undefined, - r.modifiedUri ? this._textModelService.createModelReference(r.modifiedUri) : undefined, + createModelReference(r.originalUri), + createModelReference(r.modifiedUri), ]); if (originalResult.status === 'fulfilled') { @@ -210,7 +211,12 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor return undefined; } - const errorResult = originalResult.status === 'rejected' ? originalResult : modifiedResult.status === 'rejected' ? modifiedResult : undefined; + let errorResult: PromiseRejectedResult | undefined; + if (originalResult.status === 'rejected') { + errorResult = originalResult; + } else if (modifiedResult.status === 'rejected') { + errorResult = modifiedResult; + } if (errorResult) { multiDiffItemStore.dispose(); // e.g. "File seems to be binary and cannot be opened as text" From 2f52910ea783bd06100d57611c044eeffd660a33 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:53:34 +0200 Subject: [PATCH 57/86] Fix double tooltip on the chat turn preview pill (#328368) * Use hover infrastructure for the turn preview pill (#328076) The preview pill in the turn status pills showed two tooltips: a custom hover from the resource label with the file path, plus a native `title` attribute on the surrounding button with "Open Preview: ". Now the resource label owns the single hover for the whole button (via `hoverTargetOverride`) and its title carries both the file path and the "Open Preview" hint, so only one custom hover shows. The native `title` is gone and the button keeps a dedicated aria-label. Also drop the fixed 200px cap on the pill so the file name is only ellipsized when the header actually runs out of horizontal space. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove narrating comment on preview label creation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../widget/chatContentParts/chatTurnPillsPart.ts | 16 ++++++++++------ .../contrib/chat/browser/widget/media/chat.css | 9 ++++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTurnPillsPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTurnPillsPart.ts index 38b99ae83bb..fdb91d334e2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTurnPillsPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTurnPillsPart.ts @@ -18,6 +18,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { FileKind } from '../../../../../../platform/files/common/files.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; @@ -61,6 +62,7 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent @IConfigurationService configurationService: IConfigurationService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILabelService private readonly _labelService: ILabelService, ) { super(); @@ -200,7 +202,7 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent const button = container.appendChild(document.createElement('button')); button.classList.add('chat-turn-preview-action'); button.type = 'button'; - const label = this._register(resourceLabels.create(button)); + const label = this._register(resourceLabels.create(button, { hoverTargetOverride: button })); const clickDisposable = dom.addDisposableListener(button, 'click', (e) => { this._openPrimaryPreview(previewFiles.get()); @@ -211,13 +213,15 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent const files = previewFiles.read(reader); const primaryFile = files.at(0); if (primaryFile) { + const name = basename(primaryFile.uri); label.setResource( - { resource: primaryFile.uri, name: basename(primaryFile.uri) }, - { fileKind: FileKind.FILE }, + { resource: primaryFile.uri, name }, + { + fileKind: FileKind.FILE, + title: localize('chat.turnPreview.tooltip', "{0} • Open Preview", this._labelService.getUriLabel(primaryFile.uri)), + }, ); - const tooltip = localize('chat.turnPreview.tooltip', 'Open Preview: {0}', basename(primaryFile.uri)); - button.setAttribute('aria-label', tooltip); - button.title = tooltip; + button.setAttribute('aria-label', localize('chat.turnPreview.ariaLabel', "Open Preview: {0}", name)); } container.classList.toggle('hidden', !showPreview.read(reader)); })); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index dc2784364bd..f14393b8cee 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -3113,15 +3113,17 @@ have to be updated for changes to the rules above, or to support more deeply nes /* The pill header already spaces its children with `gap`, so the label's own margin-right (needed in the standalone checkpoint summary) is redundant here - and leaves unwanted trailing space. */ + and leaves unwanted trailing space. The counts label also keeps its intrinsic + width so the preview action is the part that shrinks when space runs out. */ .interactive-session .chat-turn-pills-part .checkpoint-file-changes-summary-header .chat-file-changes-label { margin-right: 0; + flex: none; } .interactive-session .chat-turn-pills-part .chat-turn-preview { display: flex; align-items: center; - flex: none; + flex: 0 1 auto; gap: var(--vscode-spacing-size40); min-width: 0; overflow: hidden; @@ -3147,12 +3149,13 @@ have to be updated for changes to the rules above, or to support more deeply nes display: none; } +/* The file name is only ellipsized when the header actually runs out of room, so + the action shrinks with the available width instead of at a fixed cap. */ .interactive-session .chat-turn-pills-part .chat-turn-preview-action { display: inline-flex; align-items: center; gap: var(--vscode-spacing-size40); min-width: 0; - max-width: 200px; padding: var(--vscode-spacing-sizeNone) var(--vscode-spacing-size40) var(--vscode-spacing-sizeNone) var(--vscode-spacing-size20); border: none; border-radius: var(--vscode-cornerRadius-small); From 50821935e3502d0f0089c84bab6ba829bdcc15b4 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:02:45 +0200 Subject: [PATCH 58/86] chat: preserve symbol references when editing input (#328369) Recover dynamic variable ranges when an editor replacement retains the original reference text, while preserving atomic removal when the reference itself changes. Fixes #328324. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../attachments/chatDynamicVariables.ts | 63 ++++++++++-- .../browser/attachments/chatVariables.test.ts | 98 ++++++++++++++++++- 2 files changed, 153 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatDynamicVariables.ts index d56e9f82c38..78675ef803e 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatDynamicVariables.ts @@ -11,6 +11,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; import { Command, isLocation } from '../../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { IModelContentChange } from '../../../../../editor/common/model/mirrorTextModel.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -48,7 +50,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC return ChatDynamicVariableModel.ID; } - private decorationData: { id: string; text: string }[] = []; + private decorationData: { id: string; text: string; rangeOffset: number }[] = []; private readonly _editorListener = this._register(new MutableDisposable()); @@ -96,12 +98,23 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC const newText = model.getValueInRange(newRange); if (newText !== data.text) { + const replacement = e.changes.find(change => + change.rangeOffset <= data.rangeOffset + && change.rangeOffset + change.rangeLength >= data.rangeOffset + data.text.length + ); + const preservedRange = replacement && this.findReferenceRangeInReplacement(model, e.changes, replacement, data); + if (preservedRange) { + didChange = true; + return { ...ref, range: preservedRange }; + } - this.widget.inputEditor.executeEdits(this.id, [{ - range: newRange, - text: '', - }]); - this.widget.refreshParsedInput(); + if (!replacement) { + this.widget.inputEditor.executeEdits(this.id, [{ + range: newRange, + text: '', + }]); + this.widget.refreshParsedInput(); + } removed.push(ref); return null; @@ -129,6 +142,40 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC }); } + private findReferenceRangeInReplacement( + model: ITextModel, + changes: readonly IModelContentChange[], + replacement: IModelContentChange, + data: { text: string; rangeOffset: number } + ): Range | undefined { + if (!data.text) { + return undefined; + } + + const previousRelativeOffset = data.rangeOffset - replacement.rangeOffset; + let matchOffset = replacement.text.indexOf(data.text); + let closestMatchOffset = matchOffset; + while (matchOffset !== -1) { + if (Math.abs(matchOffset - previousRelativeOffset) < Math.abs(closestMatchOffset - previousRelativeOffset)) { + closestMatchOffset = matchOffset; + } + matchOffset = replacement.text.indexOf(data.text, matchOffset + data.text.length); + } + + if (closestMatchOffset === -1) { + return undefined; + } + + const precedingChangesDelta = changes.reduce((delta, change) => + change.rangeOffset < replacement.rangeOffset ? delta + change.text.length - change.rangeLength : delta, 0); + const startOffset = replacement.rangeOffset + precedingChangesDelta + closestMatchOffset; + const range = Range.fromPositions( + model.getPositionAt(startOffset), + model.getPositionAt(startOffset + data.text.length) + ); + return model.getValueInRange(range) === data.text ? range : undefined; + } + getInputState(contrib: Record): void { contrib[ChatDynamicVariableModel.ID] = [...this._variables]; } @@ -183,9 +230,11 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC this._variables = validVariables.slice(0, decorationIds.length); this.decorationData = []; for (let i = 0; i < decorationIds.length; i++) { + const range = this._variables[i].range; this.decorationData.push({ id: decorationIds[i], - text: model.getValueInRange(this._variables[i].range) + text: model.getValueInRange(range), + rangeOffset: model.getOffsetAt({ lineNumber: range.startLineNumber, column: range.startColumn }) }); } } diff --git a/src/vs/workbench/contrib/chat/test/browser/attachments/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/browser/attachments/chatVariables.test.ts index 24063cd6081..145335ae2af 100644 --- a/src/vs/workbench/contrib/chat/test/browser/attachments/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/attachments/chatVariables.test.ts @@ -7,12 +7,19 @@ import assert from 'assert'; import { Emitter } from '../../../../../../base/common/event.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { ICodeEditorService } from '../../../../../../editor/browser/services/codeEditorService.js'; import { Range } from '../../../../../../editor/common/core/range.js'; +import { TrackedRangeStickiness } from '../../../../../../editor/common/model.js'; +import { TestCodeEditorService } from '../../../../../../editor/test/browser/editorTestServices.js'; +import { createTestCodeEditor } from '../../../../../../editor/test/browser/testCodeEditor.js'; +import { createTextModel } from '../../../../../../editor/test/common/testTextModel.js'; +import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { TestThemeService } from '../../../../../../platform/theme/test/common/testThemeService.js'; import { IDynamicVariable, toAttachedContextDynamicVariable } from '../../../common/attachments/chatVariables.js'; import { IChatWidget } from '../../../browser/chat.js'; import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../../../browser/attachments/chatVariables.js'; -import { ChatDynamicVariableModel } from '../../../browser/attachments/chatDynamicVariables.js'; +import { ChatDynamicVariableModel, dynamicVariableDecorationType } from '../../../browser/attachments/chatDynamicVariables.js'; import { IChatRequestVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { IToolData, ToolDataSource, ToolAndToolSetEnablementMap } from '../../../common/tools/languageModelToolsService.js'; import { observableValue } from '../../../../../../base/common/observable.js'; @@ -220,6 +227,94 @@ suite('inline attachment references', () => { suite('ChatDynamicVariableModel', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); + function createDynamicVariableModel(text: string): { editor: ReturnType; model: ChatDynamicVariableModel } { + const textModel = store.add(createTextModel(text)); + const codeEditorService = store.add(new TestCodeEditorService(new TestThemeService())); + store.add(codeEditorService.registerDecorationType('test', dynamicVariableDecorationType, { + rangeBehavior: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + })); + const editor = store.add(createTestCodeEditor(textModel, { + serviceCollection: new ServiceCollection([ICodeEditorService, codeEditorService]), + })); + const onDidChangeActiveInputEditor = store.add(new Emitter()); + const onDidChangeAttachments = store.add(new Emitter<{ deleted: readonly string[]; added: readonly IChatRequestVariableEntry[]; updated: readonly IChatRequestVariableEntry[] }>()); + const widget = { + input: { + attachmentModel: { + attachments: [], + onDidChange: onDidChangeAttachments.event, + }, + }, + inputEditor: editor, + onDidChangeActiveInputEditor: onDidChangeActiveInputEditor.event, + refreshParsedInput: () => { }, + } as unknown as IChatWidget; + const model = store.add(new ChatDynamicVariableModel(widget, { + getUriLabel: () => '', + } as unknown as ILabelService)); + return { editor, model }; + } + + test('keeps a reference when editing text before it', () => { + const { editor, model } = createDynamicVariableModel('explain #sym:example '); + model.addReference(createMockVariable({ + range: new Range(1, 9, 1, 21), + })); + + editor.executeEdits('test', [{ + range: new Range(1, 1, 1, 21), + text: 'describe #sym:example', + }]); + + assert.deepStrictEqual({ + text: editor.getValue(), + variables: model.variables.map(variable => variable.range), + }, { + text: 'describe #sym:example ', + variables: [new Range(1, 10, 1, 22)], + }); + }); + + test('removes a reference without deleting replacement text', () => { + const { editor, model } = createDynamicVariableModel('explain #sym:example '); + model.addReference(createMockVariable({ + range: new Range(1, 9, 1, 21), + })); + + editor.executeEdits('test', [{ + range: new Range(1, 1, 1, 21), + text: 'describe', + }]); + + assert.deepStrictEqual({ + text: editor.getValue(), + variables: model.variables, + }, { + text: 'describe ', + variables: [], + }); + }); + + test('removes the whole reference when editing inside it', () => { + const { editor, model } = createDynamicVariableModel('explain #sym:example '); + model.addReference(createMockVariable({ + range: new Range(1, 9, 1, 21), + })); + + editor.executeEdits('test', [{ + range: new Range(1, 14, 1, 15), + text: 'X', + }]); + + assert.deepStrictEqual({ + text: editor.getValue(), + variables: model.variables, + }, { + text: 'explain ', + variables: [], + }); + }); + test('does not retain attachment payload after the backing attachment is removed', () => { const attachment = createMockAttachment({ kind: 'image', @@ -303,6 +398,7 @@ suite('ChatDynamicVariableModel', () => { getModel: () => ({ getValueInRange: () => '#attachment', getDecorationRange: () => new Range(1, 1, 1, 20), + getOffsetAt: (position: { column: number }) => position.column - 1, }), setDecorationsByType: (_owner: string, _type: string, decorations: Array<{ hoverMessage?: { value: string } }>) => { for (const decoration of decorations) { From 25b78bb3d67d3d87f4063b452ef4a695e85a90f6 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Fri, 31 Jul 2026 15:04:04 +0500 Subject: [PATCH 59/86] chat: fix: avoid per-subagent menu listeners (#328201) Use a one-shot menu snapshot for the Open Subagent toolbar so retained transcript parts do not each listen to the shared context-key event. Fixes #328199 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 95fe0ef5-fb4d-49bb-8047-6322510ee909 --- src/vs/sessions/SESSIONS.md | 2 +- .../chatSubagentContentPart.ts | 79 ++++++++++---- .../chatSubagentContentPart.test.ts | 103 ++++++++++++++++-- 3 files changed, 153 insertions(+), 31 deletions(-) diff --git a/src/vs/sessions/SESSIONS.md b/src/vs/sessions/SESSIONS.md index 205004e9739..84540432467 100644 --- a/src/vs/sessions/SESSIONS.md +++ b/src/vs/sessions/SESSIONS.md @@ -167,7 +167,7 @@ In the agent host, the real producer of read-only chats is **subagent (worker) c Subagent chats **persist** in the session catalog after the subagent completes (completion only marks the chat's turn complete; the chat is removed only when the whole session is disposed), so the read-only tab stays reviewable for the lifetime of the session. -**Opening a subagent chat from the transcript.** The inline subagent block (`ChatSubagentContentPart`) renders a small pill (`OpenSubagentChatActionViewItem`) that reveals the subagent's read-only tab. The provider action stays disabled and out of visual and accessibility layout until its resource resolves to a surfaced peer chat, preventing a transient generic action while the chat catalog hydrates. `ChatSubagentContentPart` switches to `chat-subagent-open-chat-only` mode only while `MenuId.ChatSubagentContent` contains an enabled action; an unresolved or stale action therefore restores the normal collapsible subagent surface rather than leaving a blank row. It re-tracks the toolbar action when either the menu or its custom action-view registration changes, because late registration replaces the action instance whose enabled state drives this mode. The custom action view mirrors its resolved enabled state onto both its rendering proxy and the original menu action, which is the typed signal the shared subagent part observes to suppress the legacy surface. The pill is a control-tier chip, not a fully rounded capsule, and is a sibling of the shared collapse button at the start of the header row. Do not rely on Agents-window CSS ancestry for this switch: the shared chat widget can be hosted through different DOM roots. The wider pill gives the subagent chat's own title priority as the leading label, with quiet, width-capped inline model metadata that is shown by default and hidden only when the child turn's model matches the parent chat's selected model; canonical ids and registered display names are treated as equivalent, and an unresolved parent model still shows the metadata (a match cannot be established, so the model is surfaced). No duplicate agent-name phrase appears beside it. While the subagent is active, one single-line row attached below the pill shows the newest child tool with the same registered or inferred compact icon used by shared thinking-tool rendering. The row subtracts its left inset from its available width so its margin box remains inside the pill. Terminal tools prefer the protocol's dedicated intention over their raw command invocation message; other tools use the SDK/provider-authored invocation message (falling back to the display name in the Agent Host adapter). The row uses shared chat markdown and file-widget rendering, so file references and inline commands retain the same rich tool presentation as editor chat; it reserves a fixed minimum line slot so swapping among text, code, and file chips does not shift surrounding content. Newer tool intents replace it with the rotating-placeholder wipe/shimmer transition whose phases follow the actual CSS animation lifecycle rather than duplicated delays; changes that cancel the animation or environments without animation support settle immediately. The effective `workbench.reduceMotion` preference controls both this transition and the pending-confirmation pulse, so forced motion overrides are honored and a preference change during a transition swaps immediately. Status uses the shared pixel spinner: the grid/dropper variant for `InProgress`, the ring variant for `NeedsInput`, and the conversation icon when complete. Normal progress remains neutral—the spinner is sufficient. A subagent with a queued confirmation gets the subtle sessions-list warning background pulse; the subagent whose confirmation is currently active above the input gets the stronger warning border/background. A numeric warning badge is shown only for two or more pending confirmations; one confirmation keeps the warning state without a redundant `1`. The carousel publishes its active subagent id for this presentation state only; confirmation ownership/routing is unchanged. The carousel's subagent reference remains scroll-to-context in regular/editor chat, but in the Agents window it invokes `workbench.action.chat.openAgentHostChat` to open the related read-only chat. A quiet italic duration sits outside the border: `Working for 10s` while active and `Worked for 10s` after completion. It uses tabular figures so once-per-second digit changes do not shift the surrounding label. Timing starts from the child chat's actual first `activeTurn.startedAt`, updates once per second while active, and freezes from the completed turn duration. Those values are copied onto serialized subagent tool data so stopping or reloading cannot reset the display. `ChatSubagentContentPart` publishes timing, confirmation count, active-confirmation state, model name, and latest active tool label/icon through the toolbar action context. The subagent chat resource is carried to the widget on `IChatSubagentToolInvocationData.chatResource` (populated in `stateToProgressAdapter` from `ToolResultSubagentContent.resource`). Because the chat widget is provider-agnostic and lower-layer, the link invokes `workbench.action.chat.openAgentHostChat` with the subagent chat URI; the sessions layer handler derives the chat id, finds the matching surfaced peer across visible sessions, and calls `sessionsService.openChat` to activate the existing tab. +**Opening a subagent chat from the transcript.** The inline subagent block (`ChatSubagentContentPart`) renders a small pill (`OpenSubagentChatActionViewItem`) that reveals the subagent's read-only tab. The provider action stays disabled and out of visual and accessibility layout until its resource resolves to a surfaced peer chat, preventing a transient generic action while the chat catalog hydrates. `ChatSubagentContentPart` switches to `chat-subagent-open-chat-only` mode only while `MenuId.ChatSubagentContent` contains an enabled action; an unresolved or stale action therefore restores the normal collapsible subagent surface rather than leaving a blank row. Each subagent resolves the static menu action with a one-shot menu snapshot and creates a plain toolbar only after the custom action-view factory is available; if registration is late, it waits with a one-time listener rather than keeping a menu/context-key listener per transcript item. The custom action view mirrors its resolved enabled state onto both its rendering proxy and the original menu action, which is the typed signal the shared subagent part observes to suppress the legacy surface. The pill is a control-tier chip, not a fully rounded capsule, and is a sibling of the shared collapse button at the start of the header row. Do not rely on Agents-window CSS ancestry for this switch: the shared chat widget can be hosted through different DOM roots. The wider pill gives the subagent chat's own title priority as the leading label, with quiet, width-capped inline model metadata that is shown by default and hidden only when the child turn's model matches the parent chat's selected model; canonical ids and registered display names are treated as equivalent, and an unresolved parent model still shows the metadata (a match cannot be established, so the model is surfaced). No duplicate agent-name phrase appears beside it. While the subagent is active, one single-line row attached below the pill shows the newest child tool with the same registered or inferred compact icon used by shared thinking-tool rendering. The row subtracts its left inset from its available width so its margin box remains inside the pill. Terminal tools prefer the protocol's dedicated intention over their raw command invocation message; other tools use the SDK/provider-authored invocation message (falling back to the display name in the Agent Host adapter). The row uses shared chat markdown and file-widget rendering, so file references and inline commands retain the same rich tool presentation as editor chat; it reserves a fixed minimum line slot so swapping among text, code, and file chips does not shift surrounding content. Newer tool intents replace it with the rotating-placeholder wipe/shimmer transition whose phases follow the actual CSS animation lifecycle rather than duplicated delays; changes that cancel the animation or environments without animation support settle immediately. The effective `workbench.reduceMotion` preference controls both this transition and the pending-confirmation pulse, so forced motion overrides are honored and a preference change during a transition swaps immediately. Status uses the shared pixel spinner: the grid/dropper variant for `InProgress`, the ring variant for `NeedsInput`, and the conversation icon when complete. Normal progress remains neutral—the spinner is sufficient. A subagent with a queued confirmation gets the subtle sessions-list warning background pulse; the subagent whose confirmation is currently active above the input gets the stronger warning border/background. A numeric warning badge is shown only for two or more pending confirmations; one confirmation keeps the warning state without a redundant `1`. The carousel publishes its active subagent id for this presentation state only; confirmation ownership/routing is unchanged. The carousel's subagent reference remains scroll-to-context in regular/editor chat, but in the Agents window it invokes `workbench.action.chat.openAgentHostChat` to open the related read-only chat. A quiet italic duration sits outside the border: `Working for 10s` while active and `Worked for 10s` after completion. It uses tabular figures so once-per-second digit changes do not shift the surrounding label. Timing starts from the child chat's actual first `activeTurn.startedAt`, updates once per second while active, and freezes from the completed turn duration. Those values are copied onto serialized subagent tool data so stopping or reloading cannot reset the display. `ChatSubagentContentPart` publishes timing, confirmation count, active-confirmation state, model name, and latest active tool label/icon through the toolbar action context. The subagent chat resource is carried to the widget on `IChatSubagentToolInvocationData.chatResource` (populated in `stateToProgressAdapter` from `ToolResultSubagentContent.resource`). Because the chat widget is provider-agnostic and lower-layer, the link invokes `workbench.action.chat.openAgentHostChat` with the subagent chat URI; the sessions layer handler derives the chat id, finds the matching surfaced peer across visible sessions, and calls `sessionsService.openChat` to activate the existing tab. **Confirmations in read-only subagent chats.** Read-only hides the composer, but the tool-confirmation carousel remains visible and keeps the input part in layout. This lets multi-chat/side-chat subagent views resolve their own confirmations without making the chat message composer interactive. diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index 1f33bf28e54..1d744b86899 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -17,13 +17,15 @@ import { rcut } from '../../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { localize } from '../../../../../../nls.js'; import { IActionViewItemService } from '../../../../../../platform/actions/browser/actionViewItemService.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; -import { MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { HiddenItemStrategy, WorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; +import { IMenuService, MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID } from '../../../common/constants.js'; import { formatCopilotCredits, IChatHookPart, IChatMarkdownContent, IChatToolInvocation, IChatToolInvocationSerialized, isLegacyChatTerminalToolInvocationData } from '../../../common/chatService/chatService.js'; import { IChatRendererContent, isResponseVM } from '../../../common/model/chatViewModel.js'; import { IRunSubagentToolInputParams } from '../../../common/tools/builtinTools/runSubagentTool.js'; @@ -128,9 +130,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen * header. The Agents window contributes an "Open Subagent" action (rendered * as a pill) into this menu; elsewhere the menu is empty and nothing shows. */ - private _openChatToolbar: MenuWorkbenchToolBar | undefined; + private _openChatToolbar: WorkbenchToolBar | undefined; private _openChatToolbarContainer: HTMLElement | undefined; private readonly _openChatActionListeners = this._register(new MutableDisposable()); + private readonly _openChatActionViewRegistration = this._register(new MutableDisposable()); // Confirmation auto-expand tracking private toolsWaitingForConfirmation: number = 0; @@ -242,27 +245,63 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this._openChatToolbarContainer?.classList.add('hidden'); return; } - if (!this._openChatToolbar) { - const container = $('.chat-subagent-open-chat-toolbar'); - this._collapseButton.element.parentElement?.insertBefore(container, this._collapseButton.element); - this._openChatToolbarContainer = container; - this._openChatToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, container, MenuId.ChatSubagentContent, { - hiddenItemStrategy: HiddenItemStrategy.Ignore, - menuOptions: { shouldForwardArgs: true }, - toolbarOptions: { primaryGroup: () => true }, - })); - this._register(this._openChatToolbar.onDidChangeMenuItems(() => this._trackOpenChatActions())); - this._register(this.actionViewItemService.onDidChange(menuId => { - if (menuId === MenuId.ChatSubagentContent) { - this._trackOpenChatActions(); - } - })); - this._trackOpenChatActions(); + if (!this._ensureOpenChatToolbar()) { + return; } this._updateOpenChatToolbarContext(); this._openChatToolbarContainer!.classList.remove('hidden'); } + private _ensureOpenChatToolbar(): boolean { + if (this._openChatToolbar) { + return true; + } + const menuAction = this._getOpenChatMenuAction(); + if (!menuAction) { + return false; + } + const actionViewItemProvider = this.actionViewItemService.lookUp(MenuId.ChatSubagentContent, CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID); + if (!actionViewItemProvider) { + if (!this._openChatActionViewRegistration.value) { + this._openChatActionViewRegistration.value = Event.once(Event.filter( + this.actionViewItemService.onDidChange, + menuId => menuId === MenuId.ChatSubagentContent + ))(() => { + this._openChatActionViewRegistration.clear(); + this._updateOpenChatLink(); + }); + } + return false; + } + + this._openChatActionViewRegistration.clear(); + const container = $('.chat-subagent-open-chat-toolbar'); + this._collapseButton?.element.parentElement?.insertBefore(container, this._collapseButton.element); + this._openChatToolbarContainer = container; + this._openChatToolbar = this._register(this.instantiationService.createInstance(WorkbenchToolBar, container, { + hiddenItemStrategy: HiddenItemStrategy.Ignore, + actionViewItemProvider: (action, options) => actionViewItemProvider( + action, + options, + this.instantiationService, + dom.getWindow(container).vscodeWindowId + ), + })); + this._openChatToolbar.setActions([menuAction]); + this._trackOpenChatActions(); + return true; + } + + private _getOpenChatMenuAction(): MenuItemAction | undefined { + for (const [, actions] of this.menuService.getMenuActions(MenuId.ChatSubagentContent, this.contextKeyService, { shouldForwardArgs: true })) { + const action = actions.find(action => action.id === CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID); + if (action instanceof MenuItemAction) { + return action; + } + } + return undefined; + } + private _trackOpenChatActions(): void { const store = new DisposableStore(); const itemCount = this._openChatToolbar?.getItemsLength() ?? 0; @@ -335,6 +374,8 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen @IConfigurationService private readonly configurationService: IConfigurationService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IActionViewItemService private readonly actionViewItemService: IActionViewItemService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { // Extract description, agentName, and prompt from toolInvocation const { description, isDefaultDescription, agentName, prompt, modelName, credits } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts index 580b393d37b..45ce74f850b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts @@ -5,7 +5,8 @@ import assert from 'assert'; import { isHTMLElement } from '../../../../../../../base/browser/dom.js'; -import { Action } from '../../../../../../../base/common/actions.js'; +import { ActionViewItem, IActionViewItemOptions } from '../../../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { Action, IAction } from '../../../../../../../base/common/actions.js'; import { Emitter, Event } from '../../../../../../../base/common/event.js'; import { DisposableStore } from '../../../../../../../base/common/lifecycle.js'; import { observableValue } from '../../../../../../../base/common/observable.js'; @@ -14,7 +15,7 @@ import { ThemeIcon } from '../../../../../../../base/common/themables.js'; import { BaseObservable } from '../../../../../../../base/common/observableInternal/observables/baseObservable.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { mainWindow } from '../../../../../../../base/browser/window.js'; -import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { TestMenuService, workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; import { ChatCollapsibleContentPart } from '../../../../browser/widget/chatContentParts/chatCollapsibleContentPart.js'; import { ChatSubagentContentPart } from '../../../../browser/widget/chatContentParts/chatSubagentContentPart.js'; import { IChatMarkdownContent, IChatSubagentToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; @@ -37,12 +38,33 @@ import { ToolDataSource } from '../../../../common/tools/languageModelToolsServi import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'; import { TestAccessibilityService } from '../../../../../../../platform/accessibility/test/common/testAccessibilityService.js'; import { IActionViewItemFactory, IActionViewItemService } from '../../../../../../../platform/actions/browser/actionViewItemService.js'; -import { MenuId } from '../../../../../../../platform/actions/common/actions.js'; +import { IMenuActionOptions, IMenuService, MenuId, MenuItemAction } from '../../../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'; +import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; +import { CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID } from '../../../../common/constants.js'; + +class TestOpenChatActionViewItem extends ActionViewItem { + constructor(sourceAction: IAction, options: IActionViewItemOptions) { + super(undefined, new Action(sourceAction.id, sourceAction.label, sourceAction.class, true, context => sourceAction.run(context)), options); + if (this.action instanceof Action) { + this._register(this.action); + } + } +} class TestActionViewItemService implements IActionViewItemService { declare _serviceBrand: undefined; private readonly _onDidChange = new Emitter(); readonly onDidChange = this._onDidChange.event; + private _providerAvailable = true; + + get hasChangeListeners(): boolean { + return this._onDidChange.hasListeners(); + } + + setProviderAvailable(available: boolean): void { + this._providerAvailable = available; + } fireDidChange(menuId: MenuId): void { this._onDidChange.fire(menuId); @@ -52,8 +74,33 @@ class TestActionViewItemService implements IActionViewItemService { return { dispose: () => { } }; } - lookUp(_menu: MenuId, _commandId: string | MenuId): IActionViewItemFactory | undefined { - return undefined; + lookUp(menu: MenuId, commandId: string | MenuId): IActionViewItemFactory | undefined { + if (!this._providerAvailable || menu !== MenuId.ChatSubagentContent || commandId !== CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID) { + return undefined; + } + return (action, options) => new TestOpenChatActionViewItem(action, options); + } +} + +class TestSubagentMenuService extends TestMenuService { + createMenuCalls = 0; + getMenuActionsCalls = 0; + + constructor(private readonly openChatAction: MenuItemAction) { + super(); + } + + override createMenu(id: MenuId, contextKeyService: IContextKeyService) { + this.createMenuCalls++; + return super.createMenu(id, contextKeyService); + } + + override getMenuActions(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuActionOptions): ReturnType { + this.getMenuActionsCalls++; + if (id === MenuId.ChatSubagentContent) { + return [['navigation', [this.openChatAction]]]; + } + return super.getMenuActions(id, contextKeyService, options); } } @@ -71,6 +118,7 @@ suite('ChatSubagentContentPart', () => { let mockEditorPool: EditorPool; let announcedToolProgressKeys: Set; let actionViewItemService: TestActionViewItemService; + let menuService: TestSubagentMenuService; function createMockRenderContext(isComplete: boolean = false): IChatContentPartRenderContext { const mockElement: Partial = { @@ -275,6 +323,16 @@ suite('ChatSubagentContentPart', () => { }()); actionViewItemService = new TestActionViewItemService(); instantiationService.stub(IActionViewItemService, actionViewItemService); + menuService = new TestSubagentMenuService(new MenuItemAction( + { id: CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID, title: 'Open Subagent' }, + undefined, + { shouldForwardArgs: true }, + undefined, + undefined, + instantiationService.get(IContextKeyService), + instantiationService.get(ICommandService), + )); + instantiationService.stub(IMenuService, menuService); // Mock list pool and editor pool mockListPool = {} as CollapsibleListPool; @@ -380,6 +438,28 @@ suite('ChatSubagentContentPart', () => { }); }); + test('should use a menu snapshot without persistent menu or action-view listeners', () => { + const part = createPart(createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Test subagent description', + chatResource: 'ahp-chat://subagent/test/tool-call', + } + }), createMockRenderContext(false)); + + assert.deepStrictEqual({ + hasToolbar: !!(part as unknown as { _openChatToolbar?: object })._openChatToolbar, + createMenuCalls: menuService.createMenuCalls, + getMenuActionsCalls: menuService.getMenuActionsCalls, + hasActionViewListeners: actionViewItemService.hasChangeListeners, + }, { + hasToolbar: true, + createMenuCalls: 0, + getMenuActionsCalls: 1, + hasActionViewListeners: false, + }); + }); + test('should hide the complete collapsible surface when the open-chat action is available', () => { const part = createPart(createMockToolInvocation({ toolSpecificData: { @@ -406,6 +486,7 @@ suite('ChatSubagentContentPart', () => { }); test('should hydrate open-chat-only mode when the action view registers after rendering', () => { + actionViewItemService.setProviderAvailable(false); const part = createPart(createMockToolInvocation({ toolSpecificData: { kind: 'subagent', @@ -413,22 +494,22 @@ suite('ChatSubagentContentPart', () => { chatResource: 'ahp-chat://subagent/test/tool-call', } }), createMockRenderContext(false)); - setOpenChatOnlyMode(part, false); + const listeningBeforeRegistration = actionViewItemService.hasChangeListeners; - const toolbar = (part as unknown as { _openChatToolbar?: { getItemsLength(): number; getItemAction(index: number): Action | undefined } })._openChatToolbar; - assert.ok(toolbar); - const hydratedAction = store.add(new Action('openSubagent', 'Open Subagent', '', true)); - toolbar.getItemsLength = () => 1; - toolbar.getItemAction = () => hydratedAction; + actionViewItemService.setProviderAvailable(true); actionViewItemService.fireDidChange(MenuId.ChatSubagentContent); const collapseButton = getCollapseButton(part); const animationContainer = part.domNode.querySelector('.chat-collapsible-content-animation'); assert.deepStrictEqual({ + listeningBeforeRegistration, + listeningAfterRegistration: actionViewItemService.hasChangeListeners, openChatOnlyClass: part.domNode.classList.contains('chat-subagent-open-chat-only'), collapseButtonDisplay: collapseButton?.style.display, animationDisplay: animationContainer?.style.display, }, { + listeningBeforeRegistration: true, + listeningAfterRegistration: false, openChatOnlyClass: true, collapseButtonDisplay: 'none', animationDisplay: 'none', From 70c3c3b4eeae4d27ee95465b3ea59d7dfe2cf5af Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Fri, 31 Jul 2026 12:58:22 +0200 Subject: [PATCH 60/86] Github issue tracking for agent host sessions --- .../common/agentHostGitStateService.ts | 9 + .../agentHost/common/githubIssueReferences.ts | 74 +++++ .../agentHost/common/state/sessionState.ts | 7 + .../node/agentHostGitStateService.ts | 31 ++ .../platform/agentHost/node/agentService.ts | 4 + .../agentHost/node/agentSideEffects.ts | 7 + .../test/common/githubIssueReferences.test.ts | 32 ++ .../agentHostChangesetCoordinator.test.ts | 1 + ...agentHostChangesetOperationService.test.ts | 1 + .../node/agentHostGitStateService.test.ts | 29 +- ...ntHostPullRequestOperationProvider.test.ts | 1 + src/vs/sessions/LAYOUT.md | 2 +- .../parts/sessionHeaderMetaActionViewItem.ts | 11 +- src/vs/sessions/common/contextkeys.ts | 1 + .../browser/fetchers/githubIssueFetcher.ts | 66 ++++ .../github/browser/github.contribution.ts | 1 + .../contrib/github/browser/githubService.ts | 12 + .../contrib/github/browser/issueActions.ts | 292 ++++++++++++++++++ .../contrib/github/browser/issueHover.ts | 107 +++++++ .../github/browser/media/issueHover.css | 113 +++++++ .../github/browser/models/githubIssueModel.ts | 194 ++++++++++++ .../sessions/contrib/github/common/types.ts | 60 ++++ .../github/test/browser/githubModels.test.ts | 105 ++++++- .../browser/baseAgentHostSessionsProvider.ts | 19 +- .../services/sessions/common/session.ts | 17 + .../sessions/common/sessionContextKeys.ts | 6 + .../sessions/githubFixtureUtils.ts | 61 +++- .../sessions/openIssue.fixture.ts | 230 ++++++++++++++ 28 files changed, 1484 insertions(+), 9 deletions(-) create mode 100644 src/vs/platform/agentHost/common/githubIssueReferences.ts create mode 100644 src/vs/platform/agentHost/test/common/githubIssueReferences.test.ts create mode 100644 src/vs/sessions/contrib/github/browser/fetchers/githubIssueFetcher.ts create mode 100644 src/vs/sessions/contrib/github/browser/issueActions.ts create mode 100644 src/vs/sessions/contrib/github/browser/issueHover.ts create mode 100644 src/vs/sessions/contrib/github/browser/media/issueHover.css create mode 100644 src/vs/sessions/contrib/github/browser/models/githubIssueModel.ts create mode 100644 src/vs/workbench/test/browser/componentFixtures/sessions/openIssue.fixture.ts diff --git a/src/vs/platform/agentHost/common/agentHostGitStateService.ts b/src/vs/platform/agentHost/common/agentHostGitStateService.ts index 034cb1aa08e..87a33c6eeb8 100644 --- a/src/vs/platform/agentHost/common/agentHostGitStateService.ts +++ b/src/vs/platform/agentHost/common/agentHostGitStateService.ts @@ -45,4 +45,13 @@ export interface IAgentHostGitStateService { * @param sessionKey The key of the session for which to check the GitHub pull request. */ attachSessionGitHubPullRequest(sessionKey: string): Promise; + + /** + * Detect GitHub issues referenced in a user message and add them to the + * session's GitHub state. Already-known issues are kept, so the session + * accumulates every issue referenced over its lifetime. + * @param sessionKey The key of the session the message was sent to. + * @param text The user message to scan for issue references. + */ + attachSessionGitHubIssues(sessionKey: string, text: string): Promise; } diff --git a/src/vs/platform/agentHost/common/githubIssueReferences.ts b/src/vs/platform/agentHost/common/githubIssueReferences.ts new file mode 100644 index 00000000000..c2980802738 --- /dev/null +++ b/src/vs/platform/agentHost/common/githubIssueReferences.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** A GitHub issue referenced from a user message. */ +export interface IGitHubIssueReference { + readonly owner: string; + readonly repo: string; + readonly number: number; +} + +/** + * Matches `https://github.com/{owner}/{repo}/issues/{number}`, optionally with a + * `www.` host, a trailing slash, a query string or a fragment (e.g. the + * `#issuecomment-123` anchor GitHub appends when copying a comment link). + */ +const ISSUE_URL_PATTERN = /\bhttps?:\/\/(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/issues\/(\d+)\b/gi; + +/** + * Matches the cross-repository shorthand `{owner}/{repo}#{number}`. The leading + * boundary check rejects references that are part of a longer path (e.g. the + * `microsoft/vscode#1` inside a URL, which the URL pattern already covers). + */ +const ISSUE_SHORTHAND_PATTERN = /(?(); + + const add = (owner: string, repo: string, rawNumber: string): void => { + const number = Number(rawNumber); + if (!Number.isSafeInteger(number) || number <= 0) { + return; + } + const url = toGitHubIssueUrl({ owner, repo, number }); + if (seen.has(url)) { + return; + } + seen.add(url); + references.push({ owner, repo, number }); + }; + + for (const match of text.matchAll(ISSUE_URL_PATTERN)) { + add(match[1], match[2], match[3]); + } + for (const match of text.matchAll(ISSUE_SHORTHAND_PATTERN)) { + add(match[1], match[2], match[3]); + } + + return references; +} + +/** Builds the canonical `github.com` URL for an issue reference. */ +export function toGitHubIssueUrl(reference: IGitHubIssueReference): string { + return `https://github.com/${reference.owner}/${reference.repo}/issues/${reference.number}`; +} + +/** Parses a canonical GitHub issue URL back into its parts, or `undefined`. */ +export function parseGitHubIssueUrl(url: string): IGitHubIssueReference | undefined { + return parseGitHubIssueReferences(url)[0]; +} diff --git a/src/vs/platform/agentHost/common/state/sessionState.ts b/src/vs/platform/agentHost/common/state/sessionState.ts index d0be6ee48bb..ed84dd9a735 100644 --- a/src/vs/platform/agentHost/common/state/sessionState.ts +++ b/src/vs/platform/agentHost/common/state/sessionState.ts @@ -1166,6 +1166,11 @@ export interface ISessionGitHubState { readonly repo?: string; /** The URL of the GitHub pull request. */ readonly pullRequestUrl?: string; + /** + * URLs of the GitHub issues referenced by the session's user messages, in + * order of first appearance. + */ + readonly issueUrls?: readonly string[]; } /** @@ -1244,11 +1249,13 @@ export function readSessionGitHubState(meta: SessionSummaryMeta | undefined): IS owner?: string; repo?: string; pullRequestUrl?: string; + issueUrls?: readonly string[]; } = {}; if (typeof raw['owner'] === 'string') { result.owner = raw['owner']; } if (typeof raw['repo'] === 'string') { result.repo = raw['repo']; } if (typeof raw['pullRequestUrl'] === 'string') { result.pullRequestUrl = raw['pullRequestUrl']; } + if (Array.isArray(raw['issueUrls'])) { result.issueUrls = raw['issueUrls'].filter((url): url is string => typeof url === 'string'); } return result; } diff --git a/src/vs/platform/agentHost/node/agentHostGitStateService.ts b/src/vs/platform/agentHost/node/agentHostGitStateService.ts index 0c8e47a2154..6619f3b2a7a 100644 --- a/src/vs/platform/agentHost/node/agentHostGitStateService.ts +++ b/src/vs/platform/agentHost/node/agentHostGitStateService.ts @@ -9,6 +9,7 @@ import { Emitter } from '../../../base/common/event.js'; import { ILogService } from '../../log/common/log.js'; import { IAgentHostGitStateService, META_GIT_STATE, META_GITHUB_STATE } from '../common/agentHostGitStateService.js'; import { ISessionGitHubState, readSessionGitHubState, readSessionGitState, SessionLifecycle, withSessionGitHubState, withSessionGitState, type ISessionGitState } from '../common/state/sessionState.js'; +import { MAX_SESSION_ISSUE_REFERENCES, parseGitHubIssueReferences, toGitHubIssueUrl } from '../common/githubIssueReferences.js'; import { IAgentHostGitService } from '../common/agentHostGitService.js'; import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js'; import { ISessionDataService } from '../common/sessionDataService.js'; @@ -93,6 +94,36 @@ export class AgentHostGitStateService extends Disposable implements IAgentHostGi } } + /** + * Scans a user message for GitHub issue references and merges them into the + * session's GitHub state. References already recorded are preserved and keep + * their position, so the list reflects the order in which the session first + * mentioned each issue. + */ + async attachSessionGitHubIssues(sessionKey: string, text: string): Promise { + const references = parseGitHubIssueReferences(text); + if (references.length === 0) { + return; + } + + const currentUrls = readSessionGitHubState(this._stateManager.getSessionState(sessionKey)?._meta)?.issueUrls ?? []; + const nextUrls = [...currentUrls]; + for (const reference of references) { + const url = toGitHubIssueUrl(reference); + if (!nextUrls.includes(url)) { + nextUrls.push(url); + } + } + + if (nextUrls.length === currentUrls.length) { + return; + } + + await this.setSessionGitHubState(sessionKey, { + issueUrls: nextUrls.slice(0, MAX_SESSION_ISSUE_REFERENCES) + } satisfies ISessionGitHubState); + } + async refreshSessionGitState(sessionKey: string, workingDirectory: URI | undefined): Promise { const sessionState = this._stateManager.getSessionState(sessionKey); if (sessionState?.lifecycle === SessionLifecycle.CreationFailed) { diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index e851c6b0ed8..4f2d2492a42 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -535,6 +535,10 @@ export class AgentService extends Disposable implements IAgentService { // Check for a GitHub pull request associated with the session's branch. void this._gitStateService.attachSessionGitHubPullRequest(session.toString()); }, + onUserMessage: (session, text) => { + // Record the GitHub issues the message references on the session. + void this._gitStateService.attachSessionGitHubIssues(session.toString(), text); + }, })); // Server-side tools, executed in-process against each session's own diff --git a/src/vs/platform/agentHost/node/agentSideEffects.ts b/src/vs/platform/agentHost/node/agentSideEffects.ts index 22970e8114c..8f4ccd682a6 100644 --- a/src/vs/platform/agentHost/node/agentSideEffects.ts +++ b/src/vs/platform/agentHost/node/agentSideEffects.ts @@ -108,6 +108,12 @@ export interface IAgentSideEffectsOptions { * excluded — only the parent session URI is passed. */ readonly onTurnComplete: (session: ProtocolURI) => void; + /** + * Called with the text of every user message that is forwarded to an agent, + * so the host can derive session state from what the user wrote (e.g. the + * GitHub issues the message references). + */ + readonly onUserMessage?: (session: ProtocolURI, text: string) => void; } interface IQueuedMessageSender { @@ -1209,6 +1215,7 @@ export class AgentSideEffects extends Disposable { this._logService.info(`[AgentSideEffects] Turn started for session not in state manager: ${channel}, turnId=${action.turnId} - status/summary updates may be dropped unless the session is restored`); } this._titleController.seedTitleFromFirstMessage(sessionChannel, action.message.text, chatChannel); + this._options.onUserMessage?.(sessionChannel, action.message.text); const agent = this._options.getAgent(sessionChannel); if (!agent) { diff --git a/src/vs/platform/agentHost/test/common/githubIssueReferences.test.ts b/src/vs/platform/agentHost/test/common/githubIssueReferences.test.ts new file mode 100644 index 00000000000..2022520cafb --- /dev/null +++ b/src/vs/platform/agentHost/test/common/githubIssueReferences.test.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { parseGitHubIssueReferences } from '../../common/githubIssueReferences.js'; + +suite('parseGitHubIssueReferences', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('detects issue URLs and owner/repo shorthand, ignores everything else', () => { + const text = [ + 'Fix https://github.com/microsoft/vscode/issues/123 first.', + 'Related: microsoft/vscode#456 and octo-org/my.repo#7.', + 'Also see https://www.github.com/microsoft/vscode/issues/123#issuecomment-99 (dupe).', + 'Not an issue: #789, https://github.com/microsoft/vscode/pull/321, https://gitlab.com/o/r/issues/5.', + ].join('\n'); + + assert.deepStrictEqual(parseGitHubIssueReferences(text), [ + { owner: 'microsoft', repo: 'vscode', number: 123 }, + { owner: 'microsoft', repo: 'vscode', number: 456 }, + { owner: 'octo-org', repo: 'my.repo', number: 7 }, + ]); + }); + + test('returns nothing for text without references', () => { + assert.deepStrictEqual(parseGitHubIssueReferences('Please refactor the parser and add tests.'), []); + }); +}); diff --git a/src/vs/platform/agentHost/test/node/agentHostChangesetCoordinator.test.ts b/src/vs/platform/agentHost/test/node/agentHostChangesetCoordinator.test.ts index d7c6899ba62..d011b0259ee 100644 --- a/src/vs/platform/agentHost/test/node/agentHostChangesetCoordinator.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostChangesetCoordinator.test.ts @@ -375,6 +375,7 @@ class TestGitStateService extends Disposable implements IAgentHostGitStateServic } async setSessionGitHubState(_sessionKey: string, _state: ISessionGitHubState): Promise { } async attachSessionGitHubPullRequest(_sessionKey: string): Promise { } + async attachSessionGitHubIssues(_sessionKey: string, _text: string): Promise { } } class TestFileMonitorService extends Disposable implements IAgentHostFileMonitorService { diff --git a/src/vs/platform/agentHost/test/node/agentHostChangesetOperationService.test.ts b/src/vs/platform/agentHost/test/node/agentHostChangesetOperationService.test.ts index 2d44169b9bd..e342d99d93a 100644 --- a/src/vs/platform/agentHost/test/node/agentHostChangesetOperationService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostChangesetOperationService.test.ts @@ -75,6 +75,7 @@ class TestGitStateService implements IAgentHostGitStateService { async setSessionGitHubState(_sessionKey: string, _state: ISessionGitHubState): Promise { } async attachSessionGitHubPullRequest(_sessionKey: string): Promise { } + async attachSessionGitHubIssues(_sessionKey: string, _text: string): Promise { } } suite('AgentHostChangesetOperationService', () => { diff --git a/src/vs/platform/agentHost/test/node/agentHostGitStateService.test.ts b/src/vs/platform/agentHost/test/node/agentHostGitStateService.test.ts index 2eef875b681..7680c020366 100644 --- a/src/vs/platform/agentHost/test/node/agentHostGitStateService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostGitStateService.test.ts @@ -11,7 +11,7 @@ import { NullLogService } from '../../../log/common/log.js'; import { IAgentHostGitService } from '../../common/agentHostGitService.js'; import type { IAgentService } from '../../common/agentService.js'; import { readSessionGitHubState, readSessionGitState, withSessionGitState, SessionStatus, type ISessionGitState, type SessionSummary } from '../../common/state/sessionState.js'; -import { META_GIT_STATE } from '../../common/agentHostGitStateService.js'; +import { META_GIT_STATE, META_GITHUB_STATE } from '../../common/agentHostGitStateService.js'; import { AgentHostGitStateService } from '../../node/agentHostGitStateService.js'; import { createTestGitHubEndpointService } from './testGitHubEndpointService.js'; import { AgentHostStateManager } from '../../node/agentHostStateManager.js'; @@ -209,6 +209,33 @@ suite('AgentHostGitStateService', () => { }); }); + test('accumulates the GitHub issues referenced across user messages', async () => { + const h = createHarness(); + seedSession(h.stateManager, { workingDirectory: WORKING_DIRECTORY }); + + await h.service.attachSessionGitHubIssues(SESSION, 'Fix https://github.com/microsoft/vscode/issues/1 please'); + await h.service.attachSessionGitHubIssues(SESSION, 'Also microsoft/vscode#1 and octo/repo#2, but not #3'); + await h.service.attachSessionGitHubIssues(SESSION, 'Nothing to see here'); + + assert.deepStrictEqual({ + github: readSessionGitHubState(h.stateManager.getSessionState(SESSION)?._meta), + persistedGitHub: await h.db.getMetadata(META_GITHUB_STATE), + }, { + github: { + issueUrls: [ + 'https://github.com/microsoft/vscode/issues/1', + 'https://github.com/octo/repo/issues/2', + ] + }, + persistedGitHub: JSON.stringify({ + issueUrls: [ + 'https://github.com/microsoft/vscode/issues/1', + 'https://github.com/octo/repo/issues/2', + ] + }), + }); + }); + test('swallows git errors and fires no events', async () => { const h = createHarness(); seedSession(h.stateManager, { workingDirectory: WORKING_DIRECTORY }); diff --git a/src/vs/platform/agentHost/test/node/agentHostPullRequestOperationProvider.test.ts b/src/vs/platform/agentHost/test/node/agentHostPullRequestOperationProvider.test.ts index 43853936954..4aaf86da92f 100644 --- a/src/vs/platform/agentHost/test/node/agentHostPullRequestOperationProvider.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostPullRequestOperationProvider.test.ts @@ -21,6 +21,7 @@ const nullGitStateService = new class implements IAgentHostGitStateService { async getSessionGitHubState(): Promise { return undefined; } async setSessionGitHubState(): Promise { } async attachSessionGitHubPullRequest(): Promise { } + async attachSessionGitHubIssues(): Promise { } }; const githubBranchWithUncommittedChanges: ISessionGitState = { diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index 73941d8015a..ca8a3614b8c 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -165,7 +165,7 @@ The Sessions Part (`SessionsPart` in [browser/parts/sessionsPart.ts](src/vs/sess A `SessionView` ([browser/parts/sessionView.ts](src/vs/sessions/browser/parts/sessionView.ts)) is a single leaf in the Sessions Part's internal grid. It hosts: -- A **session header** at the top ([browser/parts/sessionHeader.ts](src/vs/sessions/browser/parts/sessionHeader.ts)) — the session status icon + title, a meta row (the contributed workspace folder / changes / pull request buttons), and the session toolbars (Run, Open in VS Code, New Chat). The status icon ([browser/sessionStatusIcon.ts](src/vs/sessions/browser/sessionStatusIcon.ts)) shows the live spinner/status glyph for in-progress / needs-input / error states; in terminal/default states the title shows the read/unread **dot indicator** (filled link-colored dot when unread, small muted dot when read) — neither the session type icon nor the PR icon is shown in the title, since the pull request is surfaced in the meta row instead. (The status icon's `completedStateIcon` argument is generic: the header passes nothing so it falls back to the dot indicator, while the sessions list still passes the PR icon.) The meta row hosts a generic `Menus.SessionHeaderMeta` toolbar that any feature can contribute actions into; by default each contributed action renders as a consistent compact secondary `Button` with an inline `icon title` label via `SessionHeaderMetaActionViewItem` ([browser/parts/sessionHeaderMetaActionViewItem.ts](src/vs/sessions/browser/parts/sessionHeaderMetaActionViewItem.ts)) unless it registers its own action view item (spacing between the pills comes from the meta row's `gap`, no separator dot). The files view contributes the workspace folder pill (order -10, so it leads the row, gated by the per-view `SessionHasWorkspaceContext` key which `SessionView` sets when the session has a workspace label, with a custom action view item that extends `SessionHeaderMetaActionViewItem` to render the workspace icon — cloud / folder / worktree per workspace kind, where a session whose isolated worktree is still being created (`ISession.worktreePending`) already shows the worktree icon — plus the workspace label, and a hover showing the working-directory path and git branch (replaced by a "Creating worktree…" note while the worktree is pending, since the reported folder and branch are still those of the checkout the session was started from), registered from `contrib/files/browser/workspaceFolderActions.ts`) that, when activated, opens the Files view. The changes view contributes the diff stats as a clickable menu item (order 0, gated by the per-view `SessionHasChangesContext` key, which `SessionView` sets from the session's **Branch Changes** changeset, with a custom action view item that extends `SessionHeaderMetaActionViewItem` to render the diff-multiple icon, a `{n} files` label, and the live `+insertions -deletions` counts, registered via `IActionViewItemService` from `contrib/changes/browser/changesActions.ts`) that, when activated, opens the multi-file diff editor for the session. The pill always reflects the **Branch Changes** changeset (the branch-vs-base diff) — located in `IActiveSession.changesets` by the shared `BRANCH_CHANGES_CHANGESET_ID` (`services/sessions/common/session.ts`), falling back to `IActiveSession.changes` when absent — so it is independent of whichever changeset the Changes view currently has selected. While a session's isolated worktree is still being created (`ISession.worktreePending`) the key stays `false`, so the checkout's own changes are never attributed to the session. The GitHub contribution similarly contributes a pull request button (order 1, so it follows the changes button) showing the PR icon + `#` (gated by the per-view `SessionHasPullRequestContext` key, which `SessionView` sets from the session's GitHub info, with a custom action view item that extends `SessionHeaderMetaActionViewItem` to render the live `#` as its label, registered from `contrib/github/browser/pullRequestActions.ts`) that, when activated, opens the pull request on GitHub; its leading icon reads `gitHubInfo.pullRequest.icon` and renders its themed color (set as an inline `color` with `!important` priority) so the glyph reflects the live PR state; its hover is owned by the GitHub contribution and shows the repository link/date, PR title, up to three lines of description, and target/source branch pills. Visible once the bound session is created. It is also the drag handle for the session. Right-clicking the header opens `Menus.SessionHeaderContext`, which surfaces pin view / close (`1_view`), rename (`2_edit`), and mark read / unread (`3_read`). The built-in rename action is registered from `contrib/sessions/browser/sessionsActions.ts` and uses `ISessionsPartService` to find the matching `SessionView`, which delegates to the header's inline rename control. +- A **session header** at the top ([browser/parts/sessionHeader.ts](src/vs/sessions/browser/parts/sessionHeader.ts)) — the session status icon + title, a meta row (the contributed workspace folder / changes / pull request buttons), and the session toolbars (Run, Open in VS Code, New Chat). The status icon ([browser/sessionStatusIcon.ts](src/vs/sessions/browser/sessionStatusIcon.ts)) shows the live spinner/status glyph for in-progress / needs-input / error states; in terminal/default states the title shows the read/unread **dot indicator** (filled link-colored dot when unread, small muted dot when read) — neither the session type icon nor the PR icon is shown in the title, since the pull request is surfaced in the meta row instead. (The status icon's `completedStateIcon` argument is generic: the header passes nothing so it falls back to the dot indicator, while the sessions list still passes the PR icon.) The meta row hosts a generic `Menus.SessionHeaderMeta` toolbar that any feature can contribute actions into; by default each contributed action renders as a consistent compact secondary `Button` with an inline `icon title` label via `SessionHeaderMetaActionViewItem` ([browser/parts/sessionHeaderMetaActionViewItem.ts](src/vs/sessions/browser/parts/sessionHeaderMetaActionViewItem.ts)) unless it registers its own action view item (spacing between the pills comes from the meta row's `gap`, no separator dot). The files view contributes the workspace folder pill (order -10, so it leads the row, gated by the per-view `SessionHasWorkspaceContext` key which `SessionView` sets when the session has a workspace label, with a custom action view item that extends `SessionHeaderMetaActionViewItem` to render the workspace icon — cloud / folder / worktree per workspace kind, where a session whose isolated worktree is still being created (`ISession.worktreePending`) already shows the worktree icon — plus the workspace label, and a hover showing the working-directory path and git branch (replaced by a "Creating worktree…" note while the worktree is pending, since the reported folder and branch are still those of the checkout the session was started from), registered from `contrib/files/browser/workspaceFolderActions.ts`) that, when activated, opens the Files view. The changes view contributes the diff stats as a clickable menu item (order 0, gated by the per-view `SessionHasChangesContext` key, which `SessionView` sets from the session's **Branch Changes** changeset, with a custom action view item that extends `SessionHeaderMetaActionViewItem` to render the diff-multiple icon, a `{n} files` label, and the live `+insertions -deletions` counts, registered via `IActionViewItemService` from `contrib/changes/browser/changesActions.ts`) that, when activated, opens the multi-file diff editor for the session. The pill always reflects the **Branch Changes** changeset (the branch-vs-base diff) — located in `IActiveSession.changesets` by the shared `BRANCH_CHANGES_CHANGESET_ID` (`services/sessions/common/session.ts`), falling back to `IActiveSession.changes` when absent — so it is independent of whichever changeset the Changes view currently has selected. While a session's isolated worktree is still being created (`ISession.worktreePending`) the key stays `false`, so the checkout's own changes are never attributed to the session. The GitHub contribution similarly contributes a pull request button (order 1, so it follows the changes button) showing the PR icon + `#` (gated by the per-view `SessionHasPullRequestContext` key, which `SessionView` sets from the session's GitHub info, with a custom action view item that extends `SessionHeaderMetaActionViewItem` to render the live `#` as its label, registered from `contrib/github/browser/pullRequestActions.ts`) that, when activated, opens the pull request on GitHub; its leading icon reads `gitHubInfo.pullRequest.icon` and renders its themed color (set as an inline `color` with `!important` priority) so the glyph reflects the live PR state; its hover is owned by the GitHub contribution and shows the repository link/date, PR title, up to three lines of description, and target/source branch pills. The same contribution adds an issue button (order 2, so it follows the pull request button) for the GitHub issues the session's user messages referenced (gated by the per-view `SessionHasIssuesContext` key, registered from `contrib/github/browser/issueActions.ts`): a single issue renders as `#` and hovers to the issue title/description, while several render as ` issues` and open a sticky picker listing each issue on click; the leading icon reflects the aggregate live issue state (open green, closed-as-completed purple, closed as not planned/duplicate muted). Visible once the bound session is created. It is also the drag handle for the session. Right-clicking the header opens `Menus.SessionHeaderContext`, which surfaces pin view / close (`1_view`), rename (`2_edit`), and mark read / unread (`3_read`). The built-in rename action is registered from `contrib/sessions/browser/sessionsActions.ts` and uses `ISessionsPartService` to find the matching `SessionView`, which delegates to the header's inline rename control. - A **chat composite bar** below the header ([browser/parts/chatCompositeBar.ts](src/vs/sessions/browser/parts/chatCompositeBar.ts)) — the chat tab strip. Visibility tracks the number of **visible tabs** (`IActiveSession.visibleChatTabs`): it is shown only when the session has **more than one chat actually showing as a tab**, and always hidden when there is just one visible tab — even if other chats are **closed**, the single chat's **title diverged** from the session title, or the session has unopened subagents. User-created peer chats, including `/btw` side chats, participate in this ordinary tab model; tool-origin subagents stay hidden until explicitly opened. This rule is a single shared observable `IActiveSession.shouldShowChatTabs` ([services/sessions/browser/visibleSessions.ts](src/vs/sessions/services/sessions/browser/visibleSessions.ts)), read by both the composite bar and the `SessionShouldShowChatTabsContext` context key. The strip's own trailing **New Chat** action follows this visibility. The header's **New Chat** action is shown while the tab strip is hidden (a single visible tab); once the strip is shown the strip's trailing **New Chat** action offers it instead. The **Chats** (Conversations) menu is always rendered in the session header **meta row**, at the end of the pills (`Menus.SessionHeaderMeta`, order 100), independent of the tab strip's visibility — it appears once the session has more than one **committed (non-draft)** chat, or when the active chat has subagents. It renders as the meta toolbar's default submenu **icon** (the comment-discussion glyph), and clicking it opens the submenu as a dropdown. While the tab strip is shown the chat tabs are keyboard-navigable from the active session: `Ctrl/Cmd+Shift+]` / `Ctrl/Cmd+Shift+[` go to the next / previous chat (wrapping), `Ctrl/Cmd+W` closes the active chat tab (deleting an in-composer draft, hiding a committed chat) instead of the session — the same command (`sessions.chatCompositeBar.closeChat`) is contributed to the per-tab `Menus.SessionChatTab`, which the chat tab strip renders as each non-main tab's close button (forwarding the tab's chat as the action argument), and `Ctrl+Tab` / `Ctrl+Shift+Tab` open a **chat switcher** — a no-input, editor-switcher (MRU) quick pick over the session's **open** chats (skipping in-composer drafts), each shown with a chat icon (hold the modifier, press `Tab` to cycle, release to select), winning over the session-history secondary on that chord while the session has multiple open chats and falling back to session navigation otherwise (and to the editor's own `Ctrl+Tab` switcher while a quick pick is already open, since the open chords are gated on `inQuickOpen` negated); the **Go to Chat in Session** palette command (`sessions.showChatsPicker`, `Ctrl/Cmd+Shift+O`, gated on more than one committed chat) opens a **searchable** variant that additionally lists **Closed** chats in a separate group (selecting one reopens it) — these commands (`sessions.chatCompositeBar.navigateNextChat` / `navigatePreviousChat` / `closeChat` and `sessions.showChatsPicker` in `contrib/sessions/browser/sessionsActions.ts`) outrank the session-level navigation/close chords via a higher keybinding weight. Chat-to-chat navigation (next/previous chat and the `Ctrl+Tab` switcher) is gated on `SessionHasMultipleOpenChatsContext` (more than one **open** tab) — distinct from the broader `SessionShouldShowChatTabsContext` that drives strip visibility — so it stays a no-op when only a single open chat remains (e.g. one open + one closed chat); `closeChat` is gated on `SessionActiveChatIsClosableContext`, and the searchable palette command on `SessionHasMultipleCommittedChatsContext`. - A **chat view** below the bars, swapped in/out based on session state. - A floating toolbar overlay ([browser/parts/sessionHeader.ts](src/vs/sessions/browser/parts/sessionHeader.ts), `SessionViewFloatingToolbar`) shown for not-yet-created sessions in place of the header. diff --git a/src/vs/sessions/browser/parts/sessionHeaderMetaActionViewItem.ts b/src/vs/sessions/browser/parts/sessionHeaderMetaActionViewItem.ts index 42d43a2fb1e..3e3333742ce 100644 --- a/src/vs/sessions/browser/parts/sessionHeaderMetaActionViewItem.ts +++ b/src/vs/sessions/browser/parts/sessionHeaderMetaActionViewItem.ts @@ -35,7 +35,7 @@ export class SessionHeaderMetaActionViewItem extends BaseActionViewItem { button.element.classList.add('monaco-text-button', 'chat-composite-bar-meta-item-button'); this._register(button.onDidClick(() => { if (this._action.enabled) { - this.actionRunner.run(this._action, this._context); + this.onDidClickButton(); } })); @@ -44,6 +44,15 @@ export class SessionHeaderMetaActionViewItem extends BaseActionViewItem { this.updateTooltip(); } + /** + * Invoked when the pill is activated. Runs the action by default; subclasses can + * override to present their own affordance (e.g. a picker when the pill stands + * for several items). + */ + protected onDidClickButton(): void { + this.actionRunner.run(this._action, this._context); + } + override focus(): void { this.button?.focus(); } diff --git a/src/vs/sessions/common/contextkeys.ts b/src/vs/sessions/common/contextkeys.ts index 92c76ff30f2..25faa5fb5d8 100644 --- a/src/vs/sessions/common/contextkeys.ts +++ b/src/vs/sessions/common/contextkeys.ts @@ -39,6 +39,7 @@ export const SessionIsReadContext = new RawContextKey('sessionIsRead', export const SessionIsArchivedContext = new RawContextKey('sessionIsArchived', false, localize('sessionIsArchived', "Whether the session in scope is archived/marked as done (the active session globally, or a specific session within an isolated component such as the session view or a context menu overlay)")); export const SessionHasChangesContext = new RawContextKey('sessionHasChanges', false, localize('sessionHasChanges', "Whether the session view's session has pending changes (insertions or deletions)")); export const SessionHasPullRequestContext = new RawContextKey('sessionHasPullRequest', false, localize('sessionHasPullRequest', "Whether the session view's session is associated with a GitHub pull request")); +export const SessionHasIssuesContext = new RawContextKey('sessionHasIssues', false, localize('sessionHasIssues', "Whether the session view's session references at least one GitHub issue")); export const SessionHasWorkspaceContext = new RawContextKey('sessionHasWorkspace', false, localize('sessionHasWorkspace', "Whether the session view's session has an associated workspace folder")); export const IsQuickChatSessionContext = new RawContextKey('isQuickChatSession', false, localize('isQuickChatSession', "Whether the session in scope is a workspace-less quick chat")); diff --git a/src/vs/sessions/contrib/github/browser/fetchers/githubIssueFetcher.ts b/src/vs/sessions/contrib/github/browser/fetchers/githubIssueFetcher.ts new file mode 100644 index 00000000000..c852d833c38 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/fetchers/githubIssueFetcher.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 { GitHubIssueState, GitHubIssueStateReason, IGitHubIssue } from '../../common/types.js'; +import { GitHubApiClient, IGitHubApiResponse } from '../githubApiClient.js'; + +interface IGitHubIssueResponse { + readonly number: number; + readonly title: string; + readonly body: string | null; + readonly state: 'open' | 'closed'; + readonly state_reason: string | null; + readonly user: { readonly login: string; readonly avatar_url: string }; + readonly created_at: string; + readonly updated_at: string; + readonly closed_at: string | null; + /** Only set when the "issue" is actually a pull request. */ + readonly pull_request?: unknown; +} + +export class GitHubIssueFetcher { + + constructor( + private readonly _apiClient: GitHubApiClient, + ) { } + + async getIssue(owner: string, repo: string, issueNumber: number, etag?: string): Promise> { + const response = await this._apiClient.request( + 'GET', + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`, + 'githubApi.getIssue', + { etag } + ); + + return { + ...response, + data: response.data ? mapIssue(response.data) : undefined + }; + } +} + +function mapIssue(data: IGitHubIssueResponse): IGitHubIssue { + return { + number: data.number, + title: data.title, + body: data.body ?? '', + state: data.state === 'closed' ? GitHubIssueState.Closed : GitHubIssueState.Open, + stateReason: mapStateReason(data.state_reason), + author: { login: data.user.login, avatarUrl: data.user.avatar_url }, + createdAt: data.created_at, + updatedAt: data.updated_at, + closedAt: data.closed_at ?? undefined, + }; +} + +function mapStateReason(value: string | null): GitHubIssueStateReason | undefined { + switch (value) { + case 'completed': return GitHubIssueStateReason.Completed; + case 'not_planned': return GitHubIssueStateReason.NotPlanned; + case 'duplicate': return GitHubIssueStateReason.Duplicate; + case 'reopened': return GitHubIssueStateReason.Reopened; + default: return undefined; + } +} diff --git a/src/vs/sessions/contrib/github/browser/github.contribution.ts b/src/vs/sessions/contrib/github/browser/github.contribution.ts index a5a7609d27f..8c5db02634e 100644 --- a/src/vs/sessions/contrib/github/browser/github.contribution.ts +++ b/src/vs/sessions/contrib/github/browser/github.contribution.ts @@ -19,6 +19,7 @@ import { GitHubService, IGitHubService } from './githubService.js'; import { IPullRequestIconCache, PullRequestIconCache } from './pullRequestIconCache.js'; import './pullRequestActions.js'; +import './issueActions.js'; const TRACE_PREFIX = '[PR-ICON-TRACE]'; diff --git a/src/vs/sessions/contrib/github/browser/githubService.ts b/src/vs/sessions/contrib/github/browser/githubService.ts index c3694684d17..9c772f87631 100644 --- a/src/vs/sessions/contrib/github/browser/githubService.ts +++ b/src/vs/sessions/contrib/github/browser/githubService.ts @@ -12,6 +12,7 @@ import { GitHubRepositoryModel, GitHubRepositoryModelReferenceCollection } from import { GitHubPullRequestModel, GitHubPullRequestModelReferenceCollection } from './models/githubPullRequestModel.js'; import { GitHubPullRequestReviewThreadsModel, GitHubPullRequestReviewThreadsModelReferenceCollection } from './models/githubPullRequestReviewThreadsModel.js'; import { GitHubPullRequestCIModel, GitHubPullRequestCIModelReferenceCollection } from './models/githubPullRequestCIModel.js'; +import { GitHubIssueModel, GitHubIssueModelReferenceCollection } from './models/githubIssueModel.js'; import { GitHubChangesFetcher } from './fetchers/githubChangesFetcher.js'; import { getPullRequestKey } from '../common/utils.js'; import { derived, derivedOpts, IObservable } from '../../../../base/common/observable.js'; @@ -52,6 +53,11 @@ export interface IGitHubService { */ createPullRequestCIModelReference(owner: string, repo: string, prNumber: number, headSha: string): IReference; + /** + * Get a reference to a reactive model for a GitHub issue. + */ + createIssueModelReference(owner: string, repo: string, issueNumber: number): IReference; + /** * List files changed between two refs using the GitHub compare API. */ @@ -84,6 +90,7 @@ export class GitHubService extends Disposable implements IGitHubService { private readonly _pullRequestReferences: GitHubPullRequestModelReferenceCollection; private readonly _pullRequestReviewThreadsReferences: GitHubPullRequestReviewThreadsModelReferenceCollection; private readonly _pullRequestCIReferences: GitHubPullRequestCIModelReferenceCollection; + private readonly _issueReferences: GitHubIssueModelReferenceCollection; private readonly _apiClient: GitHubApiClient; /** @@ -111,6 +118,7 @@ export class GitHubService extends Disposable implements IGitHubService { this._pullRequestReferences = instantiationService.createInstance(GitHubPullRequestModelReferenceCollection, apiClient); this._pullRequestReviewThreadsReferences = instantiationService.createInstance(GitHubPullRequestReviewThreadsModelReferenceCollection, apiClient); this._pullRequestCIReferences = instantiationService.createInstance(GitHubPullRequestCIModelReferenceCollection, apiClient); + this._issueReferences = instantiationService.createInstance(GitHubIssueModelReferenceCollection, apiClient); const gitHubInfoObs = derivedOpts<{ owner: string; repo: string; pullRequestNumber: number } | undefined>({ equalsFn: structuralEquals }, reader => { @@ -197,6 +205,10 @@ export class GitHubService extends Disposable implements IGitHubService { return this._pullRequestCIReferences.acquire(`${getPullRequestKey(owner, repo, prNumber)}/${headSha}`, owner, repo, prNumber, headSha); } + createIssueModelReference(owner: string, repo: string, issueNumber: number): IReference { + return this._issueReferences.acquire(`${owner}/${repo}/issues/${issueNumber}`, owner, repo, issueNumber); + } + getChangedFiles(owner: string, repo: string, base: string, head: string): Promise { return this._changesFetcher.getChangedFiles(owner, repo, base, head); } diff --git a/src/vs/sessions/contrib/github/browser/issueActions.ts b/src/vs/sessions/contrib/github/browser/issueActions.ts new file mode 100644 index 00000000000..05e73bf64a9 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/issueActions.ts @@ -0,0 +1,292 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IManagedHoverContent } from '../../../../base/browser/ui/hover/hover.js'; +import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; +import { $ } from '../../../../base/browser/dom.js'; +import { arrayEquals } from '../../../../base/common/equals.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, derived, derivedOpts, IObservable } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize, localize2 } from '../../../../nls.js'; +import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; +import { Action2, MenuItemAction, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; +import { Menus } from '../../../browser/menus.js'; +import { SessionHeaderMetaActionViewItem } from '../../../browser/parts/sessionHeaderMetaActionViewItem.js'; +import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { SessionHasIssuesContext } from '../../../common/contextkeys.js'; +import { ISessionContext } from '../../../services/sessions/browser/sessionContext.js'; +import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; +import { IActiveSession } from '../../../services/sessions/common/sessionsManagement.js'; +import { IGitHubIssueRef, ISession } from '../../../services/sessions/common/session.js'; +import { computeAggregateIssueIcon, computeIssueIcon, GitHubIssueState, IGitHubIssue } from '../common/types.js'; +import { IGitHubService } from './githubService.js'; +import { createIssueHoverElement, createIssueListElement } from './issueHover.js'; + +/** A session issue paired with its live details, once they have been fetched. */ +interface IResolvedSessionIssue { + readonly ref: IGitHubIssueRef; + readonly issue: IGitHubIssue | undefined; +} + +// --- Open Issue action + +class OpenIssueAction extends Action2 { + static readonly ID = 'workbench.agentSessions.action.openIssue'; + + constructor() { + super({ + id: OpenIssueAction.ID, + title: localize2('agentSessions.openIssue', 'Open Issue'), + icon: Codicon.issues, + f1: false, + // Issue pill shown in the session header meta row + // (vs/sessions/browser/parts/sessionHeader.ts), right after the pull + // request pill. Rendered with a custom action view item that shows the + // aggregate issue icon plus either `#` or ` issues`. + menu: [{ + id: Menus.SessionHeaderMeta, + group: 'navigation', + order: 2, + when: SessionHasIssuesContext + }], + }); + } + + override async run(accessor: ServicesAccessor, session?: IActiveSession | ISession | ISession[]): Promise { + const openerService = accessor.get(IOpenerService); + const sessionsService = accessor.get(ISessionsService); + + const targetSession = (Array.isArray(session) ? session[0] : session) ?? sessionsService.activeSession.get(); + const issue = getSessionIssues(targetSession)[0]; + if (!issue) { + return; + } + + await openerService.open(issue.uri, { openExternal: true }); + } +} +registerAction2(OpenIssueAction); + +function getSessionIssues(session: ISession | undefined): readonly IGitHubIssueRef[] { + return session?.workspace.get()?.folders[0]?.gitRepository?.gitHubInfo.get()?.issues ?? []; +} + +// --- Open Issue action view item (session header issue pill) + +/** + * Renders the GitHub issues a session references as a single pill, the {@link OpenIssueAction} + * menu item contributed into {@link Menus.SessionHeaderMeta} (the session header meta row). + * + * A session that references one issue shows `#` and hovers to the issue's details. + * A session that references several shows ` issues` and opens a picker on click, since the + * pill then stands for a set rather than a single target. Either way the icon reflects the + * aggregate live state: open wins over closed, and closed-as-completed wins over + * closed as not planned. + * + * The issues are read from the {@link ISessionContext} so the correct per-session issues are + * shown even when several session views are visible at once. + */ +export class OpenIssueActionViewItem extends SessionHeaderMetaActionViewItem { + + private readonly _issueRefsObs: IObservable; + private readonly _issuesObs: IObservable; + + constructor( + action: MenuItemAction, + options: IActionViewItemOptions, + @ISessionContext sessionContext: ISessionContext, + @IGitHubService private readonly _gitHubService: IGitHubService, + @IOpenerService private readonly _openerService: IOpenerService, + @IHoverService private readonly _hoverService: IHoverService, + ) { + super(undefined, action, options); + + this._issueRefsObs = derivedOpts({ + owner: this, + equalsFn: (a, b) => arrayEquals(a, b, (x, y) => x.owner === y.owner && x.repo === y.repo && x.number === y.number) + }, reader => { + const session = sessionContext.session.read(reader); + const workspace = session?.workspace.read(reader); + return workspace?.folders[0]?.gitRepository?.gitHubInfo.read(reader)?.issues ?? []; + }); + + this._issuesObs = derived(reader => this._issueRefsObs.read(reader).map(ref => { + const reference = reader.store.add(this._gitHubService.createIssueModelReference(ref.owner, ref.repo, ref.number)); + return { ref, issue: reference.object.issue.read(reader) }; + })); + + // Keep the issue models warm for as long as the pill is rendered so the icon + // reflects the live state. This autorun depends only on the issue *identities*, + // so a state change does not release and re-acquire every model. + this._register(autorun(reader => { + for (const ref of this._issueRefsObs.read(reader)) { + const reference = reader.store.add(this._gitHubService.createIssueModelReference(ref.owner, ref.repo, ref.number)); + const model = reference.object; + model.refresh(); + + // A closed issue is effectively final, so it is only fetched once. Gate the + // repeating loop on a stable boolean so poll results don't toggle it. + const shouldPoll = derived(this, pollReader => model.issue.read(pollReader)?.state !== GitHubIssueState.Closed); + reader.store.add(autorun(pollReader => { + if (shouldPoll.read(pollReader)) { + pollReader.store.add(model.startPolling()); + } + })); + } + })); + + this._register(autorun(reader => { + this._issuesObs.read(reader); + this.updateLabel(); + this.updateTooltip(); + })); + } + + protected override onDidClickButton(): void { + const issues = this._issuesObs.get(); + if (issues.length > 1) { + this._showIssuePicker(issues); + return; + } + + super.onDidClickButton(); + } + + protected override getIconElement(): HTMLElement | undefined { + const icon = this._computeIcon(); + const iconElement = $(`span.chat-composite-bar-meta-item-icon${ThemeIcon.asCSSSelector(icon)}`); + if (icon.color) { + // Inline `!important` wins over `button.css`'s `.monaco-text-button .codicon + // { color: inherit !important }`, so the glyph reflects the live issue state color. + iconElement.style.setProperty('color', asCssVariable(icon.color.id), 'important'); + } + return iconElement; + } + + protected override getLabelText(): string { + const issues = this._issuesObs.get(); + if (issues.length === 0) { + return ''; + } + return issues.length === 1 + ? `#${issues[0].ref.number}` + : localize('agentSessions.openIssue.count', "{0} issues", issues.length); + } + + protected override getHoverContents(): IManagedHoverContent | undefined { + const issues = this._issuesObs.get(); + if (issues.length !== 1) { + return this.getTooltip(); + } + + const { ref, issue } = issues[0]; + return { + element: () => createIssueHoverElement({ + owner: ref.owner, + repo: ref.repo, + number: ref.number, + repositoryHref: this._getRepositoryUri(ref).toString(true), + issue, + onDidClickRepository: () => this._openerService.open(this._getRepositoryUri(ref), { openExternal: true }), + }), + }; + } + + protected override getTooltip(): string { + const issues = this._issuesObs.get(); + if (issues.length > 1) { + return localize('agentSessions.openIssue.tooltipMany', "Show the {0} Issues Referenced by This Session", issues.length); + } + const number = issues[0]?.ref.number; + return number !== undefined + ? localize('agentSessions.openIssue.tooltipWithNumber', "Open Issue #{0}", number) + : localize('agentSessions.openIssue.tooltip', "Open Issue"); + } + + private _computeIcon(): ThemeIcon { + const issues = this._issuesObs.get(); + if (issues.length === 1) { + const issue = issues[0].issue; + return issue ? computeIssueIcon(issue.state, issue.stateReason) : computeIssueIcon(GitHubIssueState.Open, undefined); + } + return computeAggregateIssueIcon(issues.map(({ issue }) => issue)); + } + + /** + * Shows the referenced issues below the pill. A sticky hover is used rather than a + * context menu because menu items render their icon on the label element, which would + * lose the per-issue state color. + */ + private _showIssuePicker(issues: readonly IResolvedSessionIssue[]): void { + const target = this.button?.element; + if (!target) { + return; + } + + const entries = issues.map(({ ref, issue }) => ({ + number: ref.number, + title: issue?.title, + icon: issue ? computeIssueIcon(issue.state, issue.stateReason) : computeIssueIcon(GitHubIssueState.Open, undefined), + uri: ref.uri, + })); + + this._hoverService.showInstantHover({ + content: createIssueListElement(entries, entry => { + this._hoverService.hideHover(); + this._openerService.open(entry.uri, { openExternal: true }); + }), + target, + position: { hoverPosition: HoverPosition.BELOW }, + persistence: { sticky: true, hideOnKeyDown: true }, + appearance: { showPointer: false, skipFadeInAnimation: true }, + trapFocus: true, + }, true); + } + + private _getRepositoryUri(ref: IGitHubIssueRef): URI { + return URI.parse(`https://github.com/${ref.owner}/${ref.repo}`); + } +} + +/** + * Registers the {@link OpenIssueActionViewItem} for the open-issue action in the session + * header meta toolbar. Registering it here (rather than in the core session header) keeps + * the rendering of the GitHub-owned action co-located with the action itself. + */ +class OpenIssueActionViewItemContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.openIssueActionViewItem'; + + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + ) { + super(); + + // The action view item service only notifies toolbars of a factory via the event + // passed to register(), not on registration itself. A session header restored with + // existing issues may create its meta toolbar before this contribution runs, so + // announce the factory once right after registering to make those toolbars + // re-render and pick it up. + const onDidRegister = this._register(new Emitter()); + this._register(actionViewItemService.register(Menus.SessionHeaderMeta, OpenIssueAction.ID, (action, options, instantiationService) => { + if (!(action instanceof MenuItemAction)) { + return undefined; + } + return instantiationService.createInstance(OpenIssueActionViewItem, action, options); + }, onDidRegister.event)); + onDidRegister.fire(); + } +} + +registerWorkbenchContribution2(OpenIssueActionViewItemContribution.ID, OpenIssueActionViewItemContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/sessions/contrib/github/browser/issueHover.ts b/src/vs/sessions/contrib/github/browser/issueHover.ts new file mode 100644 index 00000000000..73dfc0c0fd2 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/issueHover.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/issueHover.css'; + +import { $, append } from '../../../../base/browser/dom.js'; +import { safeIntl } from '../../../../base/common/date.js'; +import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { IGitHubIssue } from '../common/types.js'; + +const issueDateFormatter = safeIntl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' }); + +export interface IIssueHoverData { + readonly owner: string; + readonly repo: string; + readonly number: number; + readonly repositoryHref: string; + readonly issue: IGitHubIssue | undefined; + readonly onDidClickRepository?: () => void; +} + +export function createIssueHoverElement(data: IIssueHoverData): HTMLElement { + const hoverElement = $('.sessions-issue-hover'); + + const header = append(hoverElement, $('.sessions-issue-hover-header')); + const repositoryLink = document.createElement('a'); + repositoryLink.className = 'sessions-issue-hover-repository'; + append(header, repositoryLink); + repositoryLink.href = data.repositoryHref; + repositoryLink.textContent = `${data.owner}/${data.repo}#${data.number}`; + repositoryLink.title = repositoryLink.textContent; + if (data.onDidClickRepository) { + repositoryLink.onclick = event => { + event.preventDefault(); + event.stopPropagation(); + data.onDidClickRepository?.(); + }; + } + + const date = formatIssueDate(data.issue?.createdAt); + if (date) { + append(header, $('span.sessions-issue-hover-date', undefined, localize('agentSessions.issueHover.onDate', "on {0}", date))); + } + + append(hoverElement, $('.sessions-issue-hover-title', undefined, data.issue?.title || localize('agentSessions.issueHover.titleFallback', "Issue #{0}", data.number))); + + const body = data.issue?.body.trim() || localize('agentSessions.issueHover.bodyFallback', "No description provided."); + append(hoverElement, $('.sessions-issue-hover-description', undefined, body)); + + return hoverElement; +} + +function formatIssueDate(value: string | undefined): string | undefined { + if (!value) { + return undefined; + } + + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return undefined; + } + + return issueDateFormatter.value.format(date); +} + +/** One row of the multi-issue list shown when a session references several issues. */ +export interface IIssueListEntry { + readonly number: number; + readonly title: string | undefined; + readonly icon: ThemeIcon; +} + +/** + * Renders the session's issues as a list of ` # ` rows. Each row + * is a button so it is reachable by keyboard when the containing popup traps focus. + */ +export function createIssueListElement<T extends IIssueListEntry>(entries: readonly T[], onDidSelect: (entry: T) => void): HTMLElement { + const listElement = $('.sessions-issue-list', { role: 'list' }); + + for (const entry of entries) { + const row = append(listElement, $('button.sessions-issue-list-entry', { role: 'listitem', type: 'button' })); + row.onclick = event => { + event.preventDefault(); + event.stopPropagation(); + onDidSelect(entry); + }; + + const icon = append(row, $(`span.sessions-issue-list-entry-icon${ThemeIcon.asCSSSelector(entry.icon)}`)); + if (entry.icon.color) { + icon.style.color = asCssVariable(entry.icon.color.id); + } + + append(row, $('span.sessions-issue-list-entry-number', undefined, `#${entry.number}`)); + + const title = entry.title; + if (title) { + const titleElement = append(row, $('span.sessions-issue-list-entry-title', undefined, title)); + titleElement.title = title; + } + } + + return listElement; +} diff --git a/src/vs/sessions/contrib/github/browser/media/issueHover.css b/src/vs/sessions/contrib/github/browser/media/issueHover.css new file mode 100644 index 00000000000..0443d0fec91 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/media/issueHover.css @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.sessions-issue-hover { + box-sizing: border-box; + display: flex; + flex-direction: column; + width: 520px; + max-width: 100%; + color: var(--vscode-editorHoverWidget-foreground); +} + +.sessions-issue-hover-header { + display: flex; + align-items: baseline; + gap: var(--vscode-spacing-size40); + min-width: 0; + padding: var(--vscode-spacing-size120) var(--vscode-spacing-size160) 0; + font-size: var(--vscode-agents-fontSize-body1); + line-height: 1.4; +} + +.sessions-issue-hover-repository { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.sessions-issue-hover-date { + flex-shrink: 0; + color: var(--vscode-descriptionForeground); +} + +.sessions-issue-hover-title { + padding: var(--vscode-spacing-size40) var(--vscode-spacing-size160) var(--vscode-spacing-size120); + font-size: var(--vscode-agents-fontSize-heading2, 18px); + font-weight: var(--vscode-agents-fontWeight-semiBold, 600); + line-height: 1.25; + overflow-wrap: anywhere; +} + +.sessions-issue-hover-description { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + line-clamp: 3; + overflow: hidden; + padding: var(--vscode-spacing-size120) 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; +} + +/* --- Issue list (shown when a session references more than one issue) --- */ + +.sessions-issue-list { + box-sizing: border-box; + display: flex; + flex-direction: column; + min-width: 260px; + max-width: 420px; + padding: var(--vscode-spacing-size40) 0; +} + +.sessions-issue-list-entry { + display: flex; + align-items: center; + gap: var(--vscode-spacing-size80); + min-width: 0; + padding: var(--vscode-spacing-size40) var(--vscode-spacing-size120); + border: none; + border-radius: var(--vscode-cornerRadius-small); + background: none; + color: var(--vscode-editorHoverWidget-foreground); + font-family: inherit; + font-size: var(--vscode-agents-fontSize-body1); + line-height: 1.4; + text-align: left; + cursor: pointer; +} + +.sessions-issue-list-entry:hover, +.sessions-issue-list-entry:focus { + background-color: var(--vscode-list-hoverBackground); + outline: none; +} + +.sessions-issue-list-entry:focus-visible { + outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.sessions-issue-list-entry-icon { + flex-shrink: 0; +} + +.sessions-issue-list-entry-number { + flex-shrink: 0; + color: var(--vscode-descriptionForeground); + font-variant-numeric: tabular-nums; +} + +.sessions-issue-list-entry-title { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/src/vs/sessions/contrib/github/browser/models/githubIssueModel.ts b/src/vs/sessions/contrib/github/browser/models/githubIssueModel.ts new file mode 100644 index 00000000000..72839d2f596 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/models/githubIssueModel.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { Disposable, DisposableSet, IDisposable, ReferenceCollection, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { LRUCache } from '../../../../../base/common/map.js'; +import { IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IGitHubIssue } from '../../common/types.js'; +import { GitHubIssueFetcher } from '../fetchers/githubIssueFetcher.js'; +import { GitHubApiClient } from '../githubApiClient.js'; + +const LOG_PREFIX = '[GitHubIssueModel]'; + +/** + * How long a model waits before it revalidates on demand. Issues move far more slowly + * than pull requests, so repeated {@link GitHubIssueModel.refresh} calls — several + * session views showing the same issue, a header re-created on a session switch — + * collapse into a single request instead of producing one each. + */ +export const MIN_REFRESH_INTERVAL_MS = 60_000; + +/** How often an issue is revalidated while something keeps its model warm. */ +const DEFAULT_POLL_INTERVAL_MS = 900_000; + +/** How many disposed issues keep their revalidation state. */ +const MAX_CACHED_SNAPSHOTS = 100; + +/** + * The revalidation state of a disposed issue model: the last payload and the ETag that + * produced it. Restoring it into a freshly created model lets that model render the + * last-known state right away and revalidate with `If-None-Match`, which GitHub answers + * with a `304 Not Modified` that does not count against the API rate limit. + */ +interface IGitHubIssueSnapshot { + readonly etag: string | undefined; + readonly issue: IGitHubIssue | undefined; + readonly refreshedAt: number; +} + +export class GitHubIssueModelReferenceCollection extends ReferenceCollection<GitHubIssueModel> { + private readonly _fetcher: GitHubIssueFetcher; + + /** + * Revalidation state of issues whose model has been disposed, keyed like the + * collection itself. Session switches and list re-renders release the last reference + * to an issue model routinely; without this the next model would start cold and spend + * a full, rate-limited request re-fetching a payload that almost never changed. + */ + private readonly _snapshots = new LRUCache<string, IGitHubIssueSnapshot>(MAX_CACHED_SNAPSHOTS); + + constructor( + apiClient: GitHubApiClient, + @ILogService private readonly _logService: ILogService + ) { + super(); + this._fetcher = new GitHubIssueFetcher(apiClient); + } + + protected override createReferencedObject(key: string, owner: string, repo: string, issueNumber: number): GitHubIssueModel { + const model = new GitHubIssueModel(owner, repo, issueNumber, this._fetcher, this._logService); + const snapshot = this._snapshots.get(key); + if (snapshot) { + model.restore(snapshot); + } + return model; + } + + protected override destroyReferencedObject(key: string, object: GitHubIssueModel): void { + const snapshot = object.snapshot(); + if (snapshot) { + this._snapshots.set(key, snapshot); + } + object.dispose(); + } +} + +/** + * Reactive model for a GitHub issue. Wraps fetcher data in an observable, supports + * on-demand refresh, and can poll periodically. + * + * Every request after the first is conditional on the last ETag, so an unchanged issue + * costs a `304` that GitHub does not charge against the rate limit. On-demand refreshes + * are additionally debounced by {@link MIN_REFRESH_INTERVAL_MS} so redundant callers do + * not each produce a request. + */ +export class GitHubIssueModel extends Disposable { + + private _etag: string | undefined = undefined; + private readonly _issue = observableValue<IGitHubIssue | undefined>(this, undefined); + readonly issue: IObservable<IGitHubIssue | undefined> = this._issue; + + private _refreshPromise: Promise<void> | undefined = undefined; + /** When the last request completed (whether it returned `200` or `304`). */ + private _refreshedAt: number | undefined = undefined; + + private readonly _pollScheduler: RunOnceScheduler; + private readonly _pollingDisposables = this._register(new DisposableSet()); + + constructor( + readonly owner: string, + readonly repo: string, + readonly issueNumber: number, + private readonly _fetcher: GitHubIssueFetcher, + private readonly _logService: ILogService, + ) { + super(); + + this._pollScheduler = this._register(new RunOnceScheduler(() => this._poll(), DEFAULT_POLL_INTERVAL_MS)); + } + + /** Adopts the revalidation state of an earlier model for the same issue. */ + restore(snapshot: IGitHubIssueSnapshot): void { + this._etag = snapshot.etag; + this._refreshedAt = snapshot.refreshedAt; + if (snapshot.issue) { + this._issue.set(snapshot.issue, undefined); + } + } + + /** The revalidation state to hand to the next model for this issue, if any. */ + snapshot(): IGitHubIssueSnapshot | undefined { + return this._refreshedAt !== undefined + ? { etag: this._etag, issue: this._issue.get(), refreshedAt: this._refreshedAt } + : undefined; + } + + /** + * Revalidates the issue, unless the last request completed less than + * {@link MIN_REFRESH_INTERVAL_MS} ago. + */ + refresh(): Promise<void> { + if (this._refreshedAt !== undefined && Date.now() - this._refreshedAt < MIN_REFRESH_INTERVAL_MS) { + return Promise.resolve(); + } + + return this._refreshNow(); + } + + startPolling(intervalMs: number = DEFAULT_POLL_INTERVAL_MS): IDisposable { + const disposable = toDisposable(() => { + this._pollingDisposables.deleteAndDispose(disposable); + + if (this._pollingDisposables.size === 0) { + this._pollScheduler.cancel(); + } + }); + this._pollingDisposables.add(disposable); + + if (this._pollingDisposables.size === 1) { + this._pollScheduler.schedule(intervalMs); + } + + return disposable; + } + + private _refreshNow(): Promise<void> { + if (!this._refreshPromise) { + this._refreshPromise = this._refresh() + .finally(() => { + this._refreshPromise = undefined; + }); + } + + return this._refreshPromise; + } + + private async _poll(): Promise<void> { + // Poll ticks always revalidate; the on-demand debounce would otherwise + // swallow a tick that lands inside the debounce window. + await this._refreshNow(); + // Re-schedule for the next poll cycle (RunOnceScheduler is one-shot). + if (!this._store.isDisposed && this._pollingDisposables.size > 0) { + this._pollScheduler.schedule(); + } + } + + private async _refresh(): Promise<void> { + try { + const response = await this._fetcher.getIssue(this.owner, this.repo, this.issueNumber, this._etag); + this._refreshedAt = Date.now(); + if (response.statusCode === 200 && response.data) { + this._etag = response.etag; + this._issue.set(response.data, undefined); + } + } catch (err) { + // Leave `_refreshedAt` untouched so the next caller retries instead of being + // debounced against a request that never produced data. + this._logService.error(`${LOG_PREFIX} Failed to refresh issue ${this.owner}/${this.repo}#${this.issueNumber}:`, err); + } + } +} diff --git a/src/vs/sessions/contrib/github/common/types.ts b/src/vs/sessions/contrib/github/common/types.ts index ee5b3ffa00f..1251b21b461 100644 --- a/src/vs/sessions/contrib/github/common/types.ts +++ b/src/vs/sessions/contrib/github/common/types.ts @@ -137,6 +137,66 @@ export function computePullRequestIcon(state: GitHubPullRequestState | 'draft', //#endregion +//#region Issues + +export const enum GitHubIssueState { + Open = 'open', + Closed = 'closed', +} + +/** Why an issue was closed (GitHub's `state_reason` on the REST issue payload). */ +export const enum GitHubIssueStateReason { + Completed = 'completed', + NotPlanned = 'not_planned', + Duplicate = 'duplicate', + Reopened = 'reopened', +} + +export interface IGitHubIssue { + readonly number: number; + readonly title: string; + readonly body: string; + readonly state: GitHubIssueState; + readonly stateReason: GitHubIssueStateReason | undefined; + readonly author: IGitHubUser; + readonly createdAt: string; + readonly updatedAt: string; + readonly closedAt: string | undefined; +} + +/** + * Compute the issue status icon, mirroring how github.com colors issues: open is + * green, closed-as-completed is purple, and closed as not planned or duplicate is + * muted (the work was never done). + */ +export function computeIssueIcon(state: GitHubIssueState, stateReason: GitHubIssueStateReason | undefined): ThemeIcon { + if (state === GitHubIssueState.Open) { + return { ...Codicon.issueOpened, color: themeColorFromId('charts.green') }; + } + if (stateReason === GitHubIssueStateReason.NotPlanned || stateReason === GitHubIssueStateReason.Duplicate) { + return { ...Codicon.issueClosed, color: themeColorFromId('descriptionForeground') }; + } + return { ...Codicon.issueClosed, color: themeColorFromId('charts.purple') }; +} + +/** + * Compute a single icon summarizing a set of issues: open wins over closed, and + * closed-as-completed wins over closed as not planned or duplicate. Issues whose + * live state is not loaded yet count as open, so the icon starts optimistic and + * only settles once every issue is known to be closed. + */ +export function computeAggregateIssueIcon(issues: readonly (IGitHubIssue | undefined)[]): ThemeIcon { + if (issues.length === 0 || issues.some(issue => !issue || issue.state === GitHubIssueState.Open)) { + return computeIssueIcon(GitHubIssueState.Open, undefined); + } + + const allDiscarded = issues.every(issue => + issue!.stateReason === GitHubIssueStateReason.NotPlanned || issue!.stateReason === GitHubIssueStateReason.Duplicate); + return computeIssueIcon(GitHubIssueState.Closed, allDiscarded ? GitHubIssueStateReason.NotPlanned : GitHubIssueStateReason.Completed); +} + +//#endregion + //#region Review Comments & Threads export interface IGitHubPRComment { diff --git a/src/vs/sessions/contrib/github/test/browser/githubModels.test.ts b/src/vs/sessions/contrib/github/test/browser/githubModels.test.ts index bfbbcdcfc59..c11acf58a18 100644 --- a/src/vs/sessions/contrib/github/test/browser/githubModels.test.ts +++ b/src/vs/sessions/contrib/github/test/browser/githubModels.test.ts @@ -14,11 +14,13 @@ import { TestStorageService } from '../../../../../workbench/test/common/workben import { GitHubPullRequestModel } from '../../browser/models/githubPullRequestModel.js'; import { GitHubPullRequestReviewThreadsModel } from '../../browser/models/githubPullRequestReviewThreadsModel.js'; import { GitHubPullRequestCIModel, GitHubPullRequestCIModelReferenceCollection, parseWorkflowRunId } from '../../browser/models/githubPullRequestCIModel.js'; +import { GitHubIssueModelReferenceCollection, MIN_REFRESH_INTERVAL_MS } from '../../browser/models/githubIssueModel.js'; import { GitHubRepositoryModel } from '../../browser/models/githubRepositoryModel.js'; +import { GitHubApiClient } from '../../browser/githubApiClient.js'; import { GitHubPRFetcher } from '../../browser/fetchers/githubPRFetcher.js'; import { GitHubPRCIFetcher } from '../../browser/fetchers/githubPRCIFetcher.js'; import { GitHubRepositoryFetcher } from '../../browser/fetchers/githubRepositoryFetcher.js'; -import { GitHubCIOverallStatus, GitHubCheckConclusion, GitHubCheckStatus, GitHubPullRequestState, IGitHubCICheck, IGitHubPRComment, IGitHubPullRequestReview, IGitHubPullRequest, IGitHubRepository, IGitHubPullRequestReviewThread } from '../../common/types.js'; +import { GitHubCIOverallStatus, GitHubCheckConclusion, GitHubCheckStatus, GitHubIssueState, GitHubPullRequestState, IGitHubCICheck, IGitHubPRComment, IGitHubPullRequestReview, IGitHubPullRequest, IGitHubRepository, IGitHubPullRequestReviewThread } from '../../common/types.js'; //#region Mock Fetchers @@ -653,6 +655,107 @@ suite('GitHubPullRequestCIModel', () => { })); }); +suite('GitHubIssueModel', () => { + + const store = new DisposableStore(); + const logService = new NullLogService(); + + /** + * Stands in for the low-level API client so the tests can observe the exact + * `If-None-Match` value each request carries and replay `304` responses. + */ + class MockGitHubApiClient { + readonly sentETags: (string | undefined)[] = []; + readonly responses: { data?: unknown; statusCode: number; etag?: string }[] = []; + + async request(_method: string, _path: string, _callSite: string, options?: { etag?: string }) { + this.sentETags.push(options?.etag); + return this.responses.shift() ?? { data: undefined, statusCode: 304 }; + } + } + + function issueResponse(state: 'open' | 'closed', title: string) { + return { + number: 7, + title, + body: 'body', + state, + state_reason: state === 'closed' ? 'completed' : null, + user: { login: 'octocat', avatar_url: '' }, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-02T00:00:00Z', + closed_at: null, + }; + } + + function createCollection(client: MockGitHubApiClient) { + return new GitHubIssueModelReferenceCollection(client as unknown as GitHubApiClient, logService); + } + + teardown(() => store.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('revalidates with the stored ETag and keeps the last payload on 304', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => { + const client = new MockGitHubApiClient(); + client.responses.push({ data: issueResponse('open', 'Original'), statusCode: 200, etag: 'W/"v1"' }); + client.responses.push({ data: undefined, statusCode: 304, etag: 'W/"v1"' }); + const collection = createCollection(client); + const reference = store.add(collection.acquire('owner/repo/issues/7', 'owner', 'repo', 7)); + + await reference.object.refresh(); + await timeout(MIN_REFRESH_INTERVAL_MS); + await reference.object.refresh(); + + assert.deepStrictEqual({ + sentETags: client.sentETags, + title: reference.object.issue.get()?.title, + }, { + sentETags: [undefined, 'W/"v1"'], + title: 'Original', + }); + })); + + test('on-demand refreshes inside the debounce window collapse into one request', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => { + const client = new MockGitHubApiClient(); + client.responses.push({ data: issueResponse('open', 'Original'), statusCode: 200, etag: 'W/"v1"' }); + const collection = createCollection(client); + const reference = store.add(collection.acquire('owner/repo/issues/7', 'owner', 'repo', 7)); + + await reference.object.refresh(); + await timeout(MIN_REFRESH_INTERVAL_MS - 1); + await reference.object.refresh(); + + assert.strictEqual(client.sentETags.length, 1); + })); + + test('a re-created model starts from the previous one\'s payload and ETag', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => { + const client = new MockGitHubApiClient(); + client.responses.push({ data: issueResponse('open', 'Original'), statusCode: 200, etag: 'W/"v1"' }); + client.responses.push({ data: issueResponse('closed', 'Original'), statusCode: 200, etag: 'W/"v2"' }); + const collection = createCollection(client); + + const first = collection.acquire('owner/repo/issues/7', 'owner', 'repo', 7); + await first.object.refresh(); + first.dispose(); + + const second = store.add(collection.acquire('owner/repo/issues/7', 'owner', 'repo', 7)); + const restoredState = second.object.issue.get()?.state; + await timeout(MIN_REFRESH_INTERVAL_MS); + await second.object.refresh(); + + assert.deepStrictEqual({ + restoredState, + sentETags: client.sentETags, + state: second.object.issue.get()?.state, + }, { + restoredState: GitHubIssueState.Open, + sentETags: [undefined, 'W/"v1"'], + state: GitHubIssueState.Closed, + }); + })); +}); + suite('parseWorkflowRunId', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts index 7be4b3f3cc7..b2edd275664 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -19,6 +19,7 @@ import { generateUuid } from '../../../../../base/common/uuid.js'; import { localize } from '../../../../../nls.js'; import { AgentSession, AuthenticateParams, AuthenticateResult, IAgentConnection, IAgentSessionMetadata } from '../../../../../platform/agentHost/common/agentService.js'; import { buildAnnotationsUri } from '../../../../../platform/agentHost/common/annotationsUri.js'; +import { parseGitHubIssueUrl } from '../../../../../platform/agentHost/common/githubIssueReferences.js'; import { getEffectiveAgents } from '../../../../../platform/agentHost/common/customAgents.js'; import { KNOWN_MODE_VALUES, SessionConfigKey } from '../../../../../platform/agentHost/common/sessionConfigKeys.js'; import { migrateLegacyAutopilotConfig } from '../../../../../platform/agentHost/common/agentHostSchema.js'; @@ -46,7 +47,7 @@ import { getRegisteredLanguageModels, resolveConfiguredModel, resolveModelIdenti import { buildMutableConfigSchema, IAgentHostMcpServer, IAgentHostSessionsProvider, resolvedConfigsEqual } from '../../../../common/agentHostSessionsProvider.js'; import { agentHostSessionWorkspaceKey } from '../../../../common/agentHostSessionWorkspace.js'; import { isSessionConfigComplete } from '../../../../common/sessionConfig.js'; -import { ChatInteractivity, ChatOriginKind, DEFAULT_CHAT_CAPABILITIES, effectiveChatInteractivity, IChat, IChatCapabilities, IGitHubInfo, ISession, ISessionAgentRef, ISessionCapabilities, ISessionChangeset, ISessionChangesSummary, ISessionFile, ISessionFileChange, ISessionType, ISessionWorkspace, ISessionWorkspaceBrowseAction, ISideChatSelection, sessionFileChangesEqual, SessionStatus, toSessionId } from '../../../../services/sessions/common/session.js'; +import { ChatInteractivity, ChatOriginKind, DEFAULT_CHAT_CAPABILITIES, effectiveChatInteractivity, IChat, IChatCapabilities, IGitHubInfo, IGitHubIssueRef, ISession, ISessionAgentRef, ISessionCapabilities, ISessionChangeset, ISessionChangesSummary, ISessionFile, ISessionFileChange, ISessionType, ISessionWorkspace, ISessionWorkspaceBrowseAction, ISideChatSelection, sessionFileChangesEqual, SessionStatus, toSessionId } from '../../../../services/sessions/common/session.js'; import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; import { IDeleteChatOptions, ISendRequestOptions, ISessionChangeEvent, ISessionModelPickerOptions, ISessionModelsSnapshot } from '../../../../services/sessions/common/sessionsProvider.js'; import { IGitHubService } from '../../../github/browser/githubService.js'; @@ -195,7 +196,20 @@ function isGitHubInfoEqual(a: IGitHubInfo | undefined, b: IGitHubInfo | undefine a.pullRequest?.number === b.pullRequest?.number && a.pullRequest?.icon?.id === b.pullRequest?.icon?.id && a.pullRequest?.baseRefOid === b.pullRequest?.baseRefOid && - a.pullRequest?.headRefOid === b.pullRequest?.headRefOid; + a.pullRequest?.headRefOid === b.pullRequest?.headRefOid && + arrayEquals(a.issues ?? [], b.issues ?? [], (x, y) => x.owner === y.owner && x.repo === y.repo && x.number === y.number); +} + +/** Maps the GitHub issue URLs recorded on the session's metadata to issue references. */ +function toGitHubIssueRefs(issueUrls: readonly string[] | undefined): readonly IGitHubIssueRef[] | undefined { + const refs: IGitHubIssueRef[] = []; + for (const url of issueUrls ?? []) { + const reference = parseGitHubIssueUrl(url); + if (reference) { + refs.push({ ...reference, uri: URI.parse(url) }); + } + } + return refs.length > 0 ? refs : undefined; } // ============================================================================ @@ -664,6 +678,7 @@ export class AgentHostSessionAdapter extends Disposable implements ISession { number: pullRequestNumber, uri: URI.parse(state.pullRequestUrl!), } : undefined, + issues: toGitHubIssueRefs(state.issueUrls), }; }); diff --git a/src/vs/sessions/services/sessions/common/session.ts b/src/vs/sessions/services/sessions/common/session.ts index 698e06ad787..e08ae5d5af9 100644 --- a/src/vs/sessions/services/sessions/common/session.ts +++ b/src/vs/sessions/services/sessions/common/session.ts @@ -205,6 +205,23 @@ export interface IGitHubInfo { /** Object ID of the head ref (PR branch) commit. */ readonly headRefOid?: string; }; + /** + * GitHub issues referenced by this session, in the order they were first + * mentioned. Issues may live in a different repository than {@link owner}/{@link repo}. + */ + readonly issues?: readonly IGitHubIssueRef[]; +} + +/** A GitHub issue referenced by a session. */ +export interface IGitHubIssueRef { + /** GitHub repository owner of the issue. */ + readonly owner: string; + /** GitHub repository name of the issue. */ + readonly repo: string; + /** Issue number. */ + readonly number: number; + /** URI of the issue. */ + readonly uri: URI; } export interface ISessionChangesSummary { diff --git a/src/vs/sessions/services/sessions/common/sessionContextKeys.ts b/src/vs/sessions/services/sessions/common/sessionContextKeys.ts index 914673326d9..862a862d0c6 100644 --- a/src/vs/sessions/services/sessions/common/sessionContextKeys.ts +++ b/src/vs/sessions/services/sessions/common/sessionContextKeys.ts @@ -9,6 +9,7 @@ import { IContextKey, IContextKeyService } from '../../../../platform/contextkey import { SessionHasChangesContext, SessionHasPullRequestContext, + SessionHasIssuesContext, SessionHasWorkspaceContext, IsQuickChatSessionContext, SessionIsArchivedContext, @@ -53,6 +54,7 @@ interface ISessionContextKeys { readonly hasGitRepository: IContextKey<boolean>; readonly hasChanges: IContextKey<boolean>; readonly hasPullRequest: IContextKey<boolean>; + readonly hasIssues: IContextKey<boolean>; readonly hasWorkspace: IContextKey<boolean>; readonly isQuickChat: IContextKey<boolean>; readonly isCreated: IContextKey<boolean>; @@ -93,6 +95,7 @@ function getBoundKeys(contextKeyService: IContextKeyService): ISessionContextKey hasGitRepository: SessionHasGitRepositoryContext.bindTo(contextKeyService), hasChanges: SessionHasChangesContext.bindTo(contextKeyService), hasPullRequest: SessionHasPullRequestContext.bindTo(contextKeyService), + hasIssues: SessionHasIssuesContext.bindTo(contextKeyService), hasWorkspace: SessionHasWorkspaceContext.bindTo(contextKeyService), isQuickChat: IsQuickChatSessionContext.bindTo(contextKeyService), isCreated: SessionIsCreatedContext.bindTo(contextKeyService), @@ -154,6 +157,9 @@ export function setSessionContextKeys(session: ISession | undefined, contextKeyS const pullRequest = session?.workspace.read(reader)?.folders[0]?.gitRepository?.gitHubInfo.read(reader)?.pullRequest; keys.hasPullRequest.set(!!pullRequest); + const issues = session?.workspace.read(reader)?.folders[0]?.gitRepository?.gitHubInfo.read(reader)?.issues; + keys.hasIssues.set(!!issues?.length); + keys.hasWorkspace.set(!!session?.workspace.read(reader)?.label); // Sourced from the session's `isQuickChat` tag — never inferred from diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/githubFixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/githubFixtureUtils.ts index 537f0fb2984..cc600799697 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/githubFixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/githubFixtureUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IReference, ReferenceCollection } from '../../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable, IReference, ReferenceCollection } from '../../../../../base/common/lifecycle.js'; import { constObservable, IObservable } from '../../../../../base/common/observable.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; @@ -16,9 +16,13 @@ import { GitHubPullRequestCIModel } from '../../../../../sessions/contrib/github // eslint-disable-next-line local/code-import-patterns import { GitHubPullRequestReviewThreadsModel } from '../../../../../sessions/contrib/github/browser/models/githubPullRequestReviewThreadsModel.js'; // eslint-disable-next-line local/code-import-patterns +import { GitHubIssueModel } from '../../../../../sessions/contrib/github/browser/models/githubIssueModel.js'; +// eslint-disable-next-line local/code-import-patterns +import { GitHubIssueFetcher } from '../../../../../sessions/contrib/github/browser/fetchers/githubIssueFetcher.js'; +// eslint-disable-next-line local/code-import-patterns import { IGitHubService } from '../../../../../sessions/contrib/github/browser/githubService.js'; // eslint-disable-next-line local/code-import-patterns -import { IGitHubPullRequest } from '../../../../../sessions/contrib/github/common/types.js'; +import { IGitHubIssue, IGitHubPullRequest } from '../../../../../sessions/contrib/github/common/types.js'; interface IFixturePullRequestEntry { readonly owner: string; @@ -26,6 +30,12 @@ interface IFixturePullRequestEntry { readonly pullRequest: IGitHubPullRequest; } +interface IFixtureIssueEntry { + readonly owner: string; + readonly repo: string; + readonly issue: IGitHubIssue; +} + class FixtureGitHubPRFetcher extends mock<GitHubPRFetcher>() { } class FixtureGitHubPullRequestModel extends GitHubPullRequestModel { @@ -53,8 +63,44 @@ class FixtureGitHubPullRequestModelReferenceCollection extends ReferenceCollecti } } -export function createFixtureGitHubService(entries: readonly IFixturePullRequestEntry[]): IGitHubService { +class FixtureGitHubIssueFetcher extends mock<GitHubIssueFetcher>() { } + +class FixtureGitHubIssueModel extends GitHubIssueModel { + + override readonly issue: IObservable<IGitHubIssue | undefined>; + + constructor(owner: string, repo: string, issueNumber: number, issue: IGitHubIssue | undefined) { + super(owner, repo, issueNumber, new FixtureGitHubIssueFetcher(), new NullLogService()); + this.issue = constObservable(issue); + } + + override refresh(): Promise<void> { + return Promise.resolve(); + } + + override startPolling(): IDisposable { + return Disposable.None; + } +} + +class FixtureGitHubIssueModelReferenceCollection extends ReferenceCollection<GitHubIssueModel> { + + constructor(private readonly _issues: Map<string, IGitHubIssue>) { + super(); + } + + protected override createReferencedObject(key: string, owner: string, repo: string, issueNumber: number): GitHubIssueModel { + return new FixtureGitHubIssueModel(owner, repo, issueNumber, this._issues.get(key)); + } + + protected override destroyReferencedObject(key: string, object: GitHubIssueModel): void { + object.dispose(); + } +} + +export function createFixtureGitHubService(entries: readonly IFixturePullRequestEntry[], issueEntries: readonly IFixtureIssueEntry[] = []): IGitHubService { const pullRequests = new Map(entries.map(entry => [toPullRequestKey(entry.owner, entry.repo, entry.pullRequest.number), entry.pullRequest])); + const issues = new Map(issueEntries.map(entry => [toIssueKey(entry.owner, entry.repo, entry.issue.number), entry.issue])); return new class extends mock<IGitHubService>() { override readonly activeSessionPullRequestObs = constObservable<GitHubPullRequestModel | undefined>(undefined); @@ -62,13 +108,22 @@ export function createFixtureGitHubService(entries: readonly IFixturePullRequest override readonly activeSessionPullRequestReviewThreadsObs = constObservable<GitHubPullRequestReviewThreadsModel | undefined>(undefined); private readonly _references = new FixtureGitHubPullRequestModelReferenceCollection(pullRequests); + private readonly _issueReferences = new FixtureGitHubIssueModelReferenceCollection(issues); override createPullRequestModelReference(owner: string, repo: string, prNumber: number): IReference<GitHubPullRequestModel> { return this._references.acquire(toPullRequestKey(owner, repo, prNumber), owner, repo, prNumber); } + + override createIssueModelReference(owner: string, repo: string, issueNumber: number): IReference<GitHubIssueModel> { + return this._issueReferences.acquire(toIssueKey(owner, repo, issueNumber), owner, repo, issueNumber); + } }(); } function toPullRequestKey(owner: string, repo: string, prNumber: number): string { return `${owner}/${repo}/${prNumber}`; } + +function toIssueKey(owner: string, repo: string, issueNumber: number): string { + return `${owner}/${repo}/issues/${issueNumber}`; +} diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/openIssue.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/openIssue.fixture.ts new file mode 100644 index 00000000000..6b791f0b7f7 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/openIssue.fixture.ts @@ -0,0 +1,230 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../base/common/uri.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { IObservable, constObservable, observableValue } from '../../../../../base/common/observable.js'; +import { MenuItemAction } from '../../../../../platform/actions/common/actions.js'; +// eslint-disable-next-line local/code-import-patterns +import { IGitHubInfo, IGitHubIssueRef, ISessionFolder, ISessionGitRepository, ISessionWorkspace } from '../../../../../sessions/services/sessions/common/session.js'; +// eslint-disable-next-line local/code-import-patterns +import { IActiveSession } from '../../../../../sessions/services/sessions/common/sessionsManagement.js'; +// eslint-disable-next-line local/code-import-patterns +import { ISessionContext, SessionContext } from '../../../../../sessions/services/sessions/browser/sessionContext.js'; +// eslint-disable-next-line local/code-import-patterns +import { computeIssueIcon, GitHubIssueState, GitHubIssueStateReason, IGitHubIssue } from '../../../../../sessions/contrib/github/common/types.js'; +// eslint-disable-next-line local/code-import-patterns +import { IGitHubService } from '../../../../../sessions/contrib/github/browser/githubService.js'; +// eslint-disable-next-line local/code-import-patterns +import { createIssueHoverElement, createIssueListElement } from '../../../../../sessions/contrib/github/browser/issueHover.js'; +// eslint-disable-next-line local/code-import-patterns +import { OpenIssueActionViewItem } from '../../../../../sessions/contrib/github/browser/issueActions.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; +import { createFixtureGitHubService } from './githubFixtureUtils.js'; + +// eslint-disable-next-line local/code-import-patterns +import '../../../../../sessions/browser/parts/media/chatCompositeBar.css'; +import '../../../../../base/browser/ui/hover/hoverWidget.css'; +import '../../../../../platform/hover/browser/hover.css'; + +// ============================================================================ +// Mock helpers +// ============================================================================ + +function createMockWorkspace(issues: readonly IGitHubIssueRef[]): ISessionWorkspace { + const root = URI.file('/home/user/projects/vscode'); + const gitHubInfo: IGitHubInfo = { owner: 'microsoft', repo: 'vscode', issues }; + + const gitRepository: ISessionGitRepository = { + uri: root, + workTreeUri: undefined, + baseBranchName: 'main', + gitHubInfo: constObservable(gitHubInfo), + }; + + const folder: ISessionFolder = { + root, + workingDirectory: root, + name: 'vscode', + description: undefined, + gitRepository, + }; + + return { + uri: root, + label: 'vscode', + icon: Codicon.folder, + folders: [folder], + requiresWorkspaceTrust: false, + isVirtualWorkspace: false, + }; +} + +function createMockSession(issues: readonly IGitHubIssueRef[]): IActiveSession { + return new class extends mock<IActiveSession>() { + override readonly resource = URI.parse('session:1'); + override readonly workspace: IObservable<ISessionWorkspace | undefined> = observableValue('workspace', createMockWorkspace(issues)); + }(); +} + +function toIssueRef(issue: IGitHubIssue): IGitHubIssueRef { + return { + owner: 'microsoft', + repo: 'vscode', + number: issue.number, + uri: URI.parse(`https://github.com/microsoft/vscode/issues/${issue.number}`), + }; +} + +// ============================================================================ +// Render helpers +// ============================================================================ + +function renderIssuePill(ctx: ComponentFixtureContext, issues: readonly IGitHubIssue[]): void { + const { container, disposableStore } = ctx; + + const session = observableValue<IActiveSession | undefined>('session', createMockSession(issues.map(toIssueRef))); + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + reg.defineInstance(ISessionContext, new SessionContext(session)); + reg.defineInstance(IGitHubService, createFixtureGitHubService([], issues.map(issue => ({ owner: 'microsoft', repo: 'vscode', issue })))); + }, + }); + + // Build the real menu item action the session header contributes, then + // render the production action view item against it. + const action = instantiationService.createInstance( + MenuItemAction, + { id: 'workbench.agentSessions.action.openIssue', title: 'Open Issue' }, + undefined, + undefined, + undefined, + undefined, + ); + + const item = disposableStore.add(instantiationService.createInstance(OpenIssueActionViewItem, action, {})); + + // Recreate the session header meta toolbar host so the inline-label styling + // (.chat-composite-bar-meta-toolbar) applies as in production. + const toolbar = document.createElement('div'); + toolbar.classList.add('chat-composite-bar-meta-toolbar'); + container.appendChild(toolbar); + item.render(toolbar); + + container.style.padding = '8px'; + container.style.backgroundColor = 'var(--vscode-sideBar-background)'; +} + +function renderInHoverWidget(ctx: ComponentFixtureContext, content: HTMLElement, width: string): void { + const { container } = ctx; + + container.style.padding = '24px'; + container.style.width = width; + container.style.backgroundColor = 'var(--vscode-sideBar-background)'; + + const hover = document.createElement('div'); + hover.classList.add('monaco-hover', 'workbench-hover'); + hover.style.position = 'static'; + hover.style.display = 'inline-block'; + + const row = document.createElement('div'); + row.classList.add('hover-row', 'markdown-hover'); + hover.appendChild(row); + + const contents = document.createElement('div'); + contents.classList.add('hover-contents', 'html-hover-contents'); + contents.appendChild(content); + row.appendChild(contents); + + container.appendChild(hover); +} + +function renderIssueHover(ctx: ComponentFixtureContext, issue: IGitHubIssue): void { + renderInHoverWidget(ctx, createIssueHoverElement({ + owner: 'microsoft', + repo: 'vscode', + number: issue.number, + repositoryHref: 'https://github.com/microsoft/vscode', + issue, + }), '580px'); +} + +function renderIssueList(ctx: ComponentFixtureContext, issues: readonly IGitHubIssue[]): void { + renderInHoverWidget(ctx, createIssueListElement(issues.map(issue => ({ + number: issue.number, + title: issue.title, + icon: computeIssueIcon(issue.state, issue.stateReason), + })), () => { }), '480px'); +} + +// ============================================================================ +// Data +// ============================================================================ + +const openIssue: IGitHubIssue = { + number: 12345, + title: 'Terminal hangs when running a long build task in a detached worktree', + body: 'Steps to reproduce: open a session on a worktree, start `npm run watch`, then switch to another session. The terminal stops streaming output and the task never reports completion.', + state: GitHubIssueState.Open, + stateReason: undefined, + author: { login: 'hariharjeevan', avatarUrl: '' }, + createdAt: '2026-06-22T10:00:00Z', + updatedAt: '2026-06-24T12:00:00Z', + closedAt: undefined, +}; + +const completedIssue: IGitHubIssue = { + number: 678, + title: 'Session header pill should show the referenced issue', + body: 'The session header already surfaces the pull request. It should do the same for the GitHub issues the user referenced in their messages.', + state: GitHubIssueState.Closed, + stateReason: GitHubIssueStateReason.Completed, + author: { login: 'alex', avatarUrl: '' }, + createdAt: '2026-06-05T10:00:00Z', + updatedAt: '2026-06-18T09:30:00Z', + closedAt: '2026-06-18T09:30:00Z', +}; + +const notPlannedIssue: IGitHubIssue = { + number: 42, + title: 'Add a setting to disable issue detection entirely, including for cross-repository references', + body: 'Not planned — the pill is already scoped to explicit references.', + state: GitHubIssueState.Closed, + stateReason: GitHubIssueStateReason.NotPlanned, + author: { login: 'alex', avatarUrl: '' }, + createdAt: '2026-05-30T10:00:00Z', + updatedAt: '2026-06-02T08:00:00Z', + closedAt: '2026-06-02T08:00:00Z', +}; + +// ============================================================================ +// Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ path: 'sessions/' }, { + + OpenIssue_Single: defineComponentFixture({ + render: (ctx) => renderIssuePill(ctx, [openIssue]), + }), + + OpenIssue_Closed: defineComponentFixture({ + render: (ctx) => renderIssuePill(ctx, [completedIssue]), + }), + + OpenIssue_Multiple: defineComponentFixture({ + render: (ctx) => renderIssuePill(ctx, [openIssue, completedIssue, notPlannedIssue]), + }), + + OpenIssue_Hover: defineComponentFixture({ + render: (ctx) => renderIssueHover(ctx, openIssue), + }), + + OpenIssue_List: defineComponentFixture({ + render: (ctx) => renderIssueList(ctx, [openIssue, completedIssue, notPlannedIssue]), + }), +}); From cf939b157650d4835714fe726206d6f0cd152382 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:03:42 +0200 Subject: [PATCH 61/86] sessions: mark agent feedback as submitted when the request is queued (#328378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * sessions: mark agent feedback as submitted when the request is queued `AgentFeedbackService.submitFeedback` awaited `widget.acceptInput('/act-on-feedback')` before calling `markFeedbackSubmitted`. When a request is already in progress the chat widget queues the message and then awaits the queued deferred, which only resolves once the queued request actually runs. The await therefore stayed pending, the feedback items never left the `Accepted` state and the transient agent-host attachment was never cleaned up, so the "Submit" button appeared to do nothing even though the message was queued. Add an `onRequestAccepted` callback to `IChatAcceptInputOptions`, fired by `ChatWidget` as soon as the request has been handed to the chat service (sent or queued) and before it awaits the queued deferred. Both submit paths in `AgentFeedbackService` now go through a shared `_sendActOnFeedbackRequest` helper that marks the feedback submitted and clears the transient attachment at that point. If the widget never accepts the request, `submitFeedback` now resolves `false` and leaves the items `Accepted` instead of falsely marking them submitted. Fixes #328177 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chat: cover the onRequestAccepted contract in ChatWidget The `onRequestAccepted` contract was only exercised through `AgentFeedbackService`'s widget mock, which invoked the callback itself. Those tests kept passing if the `ChatWidget` invocation were removed, moved after the queued deferred settles, or fired for a rejected result — i.e. they did not pin the behaviour that was actually fixed. Extract the result handling into an exported `acceptAndAwaitSentRequest` helper, matching the existing pattern of testable module-scope helpers in this file, and cover the sent, queued (accepted before the deferred settles), rejected, and queued-then-rejected paths. Each new test was verified to fail against a mutation of the corresponding behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/agentFeedbackService.ts | 68 ++++++++++++------- .../test/browser/agentFeedbackService.test.ts | 53 ++++++++++++++- src/vs/workbench/contrib/chat/browser/chat.ts | 7 ++ .../contrib/chat/browser/widget/chatWidget.ts | 25 ++++++- .../test/browser/widget/chatWidget.test.ts | 64 ++++++++++++++++- 5 files changed, 185 insertions(+), 32 deletions(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index 8e8b89ae0a3..56d68129658 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../../base/common/event.js'; +import { DeferredPromise } from '../../../../base/common/async.js'; +import { createSingleCallFunction } from '../../../../base/common/functional.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { derived, IObservable, runOnChange } from '../../../../base/common/observable.js'; @@ -20,7 +22,7 @@ import { ISessionsService } from '../../../services/sessions/browser/sessionsSer import { editingEntriesContainResource } from '../../../../workbench/contrib/chat/browser/sessionResourceMatching.js'; import { changeMatchesResource, getActiveResourceCandidates, IAgentFeedbackContext } from './agentFeedbackEditorUtils.js'; import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; -import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; +import { IChatWidget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ICodeReviewSuggestion } from '../../codeReview/browser/codeReviewService.js'; import { ISession, ISessionFileChange, ISessionWorkspace, SessionStatus } from '../../../services/sessions/common/session.js'; @@ -254,7 +256,10 @@ export interface IAgentFeedbackService { /** * Submit the currently accumulated accepted feedback for the session to the - * agent and mark those items as submitted. Returns whether the feedback was submitted. + * agent and mark those items as submitted. Resolves once the request has been + * accepted by the chat widget — which, while another request is in progress, + * means it was queued rather than sent. Returns whether the feedback was + * submitted. */ submitFeedback(sessionResource: URI): Promise<boolean>; @@ -846,7 +851,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe // submitted via the "Submit Feedback" button). Attach the accepted // items — which are about to become submitted — to this single request // so the agent receives the comments, then remove the transient - // attachment again once the request has been sent. + // attachment again once the request has been accepted. if (this._isAgentHostSession(sessionResource)) { const acceptedItems = this.getFeedback(sessionResource).filter(item => item.state === AgentFeedbackState.Accepted); const attachmentId = ATTACHMENT_ID_PREFIX + sessionResource.toString(); @@ -856,32 +861,43 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe widget.attachmentModel.addContext(createAgentFeedbackVariableEntry(sessionResource, acceptedItems, annotationsResource)); } - try { - await widget.acceptInput('/act-on-feedback'); - } catch (err) { - this._logService.error('[AgentFeedback] Failed to submit feedback', err); - return false; - } finally { - widget.attachmentModel.delete(attachmentId); + return this._sendActOnFeedbackRequest(widget, sessionResource, () => widget.attachmentModel.delete(attachmentId)); + } + + // For non-agent-host sessions the reactive attachment contribution also + // marks submission on send; marking from the helper is idempotent and + // covers sessions without that contribution. + return this._sendActOnFeedbackRequest(widget, sessionResource); + } + + /** + * Sends the `/act-on-feedback` request and marks the accepted feedback as + * submitted as soon as the request has been accepted by the chat widget. + * The request is queued when the agent is still working on another request, + * in which case awaiting {@link IChatWidget.acceptInput} would only resolve + * once that queued request eventually runs — the feedback items must move to + * the submitted state right away. + */ + private _sendActOnFeedbackRequest(widget: IChatWidget, sessionResource: URI, cleanup?: () => void): Promise<boolean> { + const submitted = new DeferredPromise<boolean>(); + const cleanupOnce = cleanup && createSingleCallFunction(cleanup); + + widget.acceptInput('/act-on-feedback', { + onRequestAccepted: () => { + cleanupOnce?.(); + this.markFeedbackSubmitted(sessionResource); + submitted.complete(true); } - - this.markFeedbackSubmitted(sessionResource); - return true; - } - - // Send first so the accepted feedback is still attached to the request, - // then mark the items as submitted. For non-agent-host sessions the - // attachment contribution also marks submission on send; marking here is - // idempotent and covers sessions without that contribution. - try { - await widget.acceptInput('/act-on-feedback'); - } catch (err) { + }).then(() => { + cleanupOnce?.(); + submitted.complete(false); + }, err => { this._logService.error('[AgentFeedback] Failed to submit feedback', err); - return false; - } + cleanupOnce?.(); + submitted.complete(false); + }); - this.markFeedbackSubmitted(sessionResource); - return true; + return submitted.p; } markFeedbackSubmitted(sessionResource: URI): void { diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts index 7a345cefcec..65ed9ef5502 100644 --- a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts @@ -14,9 +14,10 @@ import { mock } from '../../../../../base/test/common/mock.js'; import { AGENT_FEEDBACK_NEW_SESSION_RESOURCE, AgentFeedbackKind, AgentFeedbackService, AgentFeedbackState, IAgentFeedbackService } from '../../browser/agentFeedbackService.js'; import { getSessionEditorComments } from '../../browser/sessionEditorComments.js'; import { IChatEditingService } from '../../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; -import { IChatWidget, IChatWidgetService } from '../../../../../workbench/contrib/chat/browser/chat.js'; +import { IChatWidget, IChatWidgetService, IChatAcceptInputOptions } from '../../../../../workbench/contrib/chat/browser/chat.js'; import { IAgentFeedbackVariableEntry } from '../../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { DeferredPromise } from '../../../../../base/common/async.js'; import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IEditorService, IVisibleEditorsChangeEvent } from '../../../../../workbench/services/editor/common/editorService.js'; @@ -656,10 +657,16 @@ suite('AgentFeedbackService - Submit (agent host)', () => { let fileA: URI; let widgetOps: string[]; let addedEntries: IAgentFeedbackVariableEntry[]; + /** Resolves when the (possibly queued) request is actually sent, i.e. when `acceptInput` resolves. */ + let acceptInputSent: DeferredPromise<void>; + /** Whether the widget hands the request over to the chat service. */ + let acceptsRequest: boolean; setup(() => { widgetOps = []; addedEntries = []; + acceptInputSent = new DeferredPromise<void>(); + acceptsRequest = true; const instantiationService = store.add(new TestInstantiationService()); instantiationService.stub(IChatEditingService, new class extends mock<IChatEditingService>() { }); instantiationService.stub(ITelemetryService, NullTelemetryService); @@ -687,7 +694,15 @@ suite('AgentFeedbackService - Submit (agent host)', () => { widgetOps.push(`add:${entries[0]?.id}`); }, }, - acceptInput: async (query: string) => { widgetOps.push(`accept:${query}`); return undefined; }, + acceptInput: async (query: string, options?: IChatAcceptInputOptions) => { + widgetOps.push(`accept:${query}`); + if (acceptsRequest) { + options?.onRequestAccepted?.(); + } + await acceptInputSent.p; + widgetOps.push(`sent:${query}`); + return undefined; + }, } as unknown as IChatWidget; instantiationService.stub(IChatWidgetService, new class extends mock<IChatWidgetService>() { override getWidgetBySessionResource(_resource: URI): IChatWidget { return widget; } @@ -726,4 +741,38 @@ suite('AgentFeedbackService - Submit (agent host)', () => { state: AgentFeedbackState.Submitted, }); }); + + test('marks feedback as submitted once the request is queued behind an in-progress request', async () => { + service.addFeedback(session, fileA, r(10), 'Please simplify'); + + // `acceptInputSent` is still pending: the request was queued and only runs + // once the in-progress request completes. + const submitted = await service.submitFeedback(session); + + assert.deepStrictEqual({ + submitted, + state: service.getFeedback(session)[0].state, + sent: widgetOps.includes('sent:/act-on-feedback'), + }, { + submitted: true, + state: AgentFeedbackState.Submitted, + sent: false, + }); + }); + + test('keeps feedback accepted when the request is not accepted by the widget', async () => { + acceptsRequest = false; + acceptInputSent.complete(); + service.addFeedback(session, fileA, r(10), 'Please simplify'); + + const submitted = await service.submitFeedback(session); + + assert.deepStrictEqual({ + submitted, + state: service.getFeedback(session)[0].state, + }, { + submitted: false, + state: AgentFeedbackState.Accepted, + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 82f0d2a7621..e1389d07498 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -356,6 +356,13 @@ export interface IChatAcceptInputOptions { preserveFocus?: boolean; /** Keeps the input box contents and attachments after submitting a programmatic query, and omits them from it. The query itself is sent as-is: prompt slash commands in it are not resolved. */ preserveInput?: boolean; + /** + * Called once the request has been handed over to the chat service, i.e. it was either sent + * right away or queued because another request is in progress. Callers that must not wait for + * a queued request to actually run should use this instead of awaiting `acceptInput`, which + * only resolves once the request has been sent. + */ + onRequestAccepted?: () => void; } export interface IChatWidgetViewModelChangeEvent { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 7a3d5cc9102..a36a3143a4f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -59,7 +59,7 @@ import { ChatMode, getModeNameForTelemetry, IChatMode } from '../../common/chatM import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../../common/requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js'; import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../attachments/chatVariables.js'; -import { ChatRequestQueueKind, ChatSendResult, IChatLocationData, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js'; +import { ChatRequestQueueKind, ChatSendResult, ChatSendResultSent, IChatLocationData, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js'; import { IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; import { IChatSlashCommandService } from '../../common/participants/chatSlashCommands.js'; import { IChatTodoListService } from '../../common/tools/chatTodoListService.js'; @@ -152,6 +152,25 @@ export function getImmediateSilentSlashCommandPart(parsedRequest: IParsedChatReq ); } +/** + * Settles the outcome of a `IChatService.sendRequest` call. + * + * A request that could not be handed over to the chat service is never accepted. Anything else is + * accepted right away — a queued request is accepted the moment it enters the queue, which is + * potentially long before it runs — so {@link onRequestAccepted} fires before the queued request + * settles. Resolves with the request once it has actually been sent, or `undefined` if it never was. + */ +export async function acceptAndAwaitSentRequest(result: ChatSendResult, onRequestAccepted?: () => void): Promise<ChatSendResultSent | undefined> { + if (ChatSendResult.isRejected(result)) { + return undefined; + } + + onRequestAccepted?.(); + + const sent = ChatSendResult.isQueued(result) ? await result.deferred : result; + return ChatSendResult.isSent(sent) ? sent : undefined; +} + type ChatHandoffClickEvent = { fromAgent: string; toAgent: string; @@ -2933,8 +2952,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this._maybeStartGoalSummary(requestInputs.input); } - const sent = ChatSendResult.isQueued(result) ? await result.deferred : result; - if (!ChatSendResult.isSent(sent)) { + const sent = await acceptAndAwaitSentRequest(result, options.onRequestAccepted); + if (!sent) { return; } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatWidget.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatWidget.test.ts index 332c084c6ff..ff1c9ba59d6 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatWidget.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatWidget.test.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { DeferredPromise } from '../../../../../../base/common/async.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { OffsetRange } from '../../../../../../editor/common/core/ranges/offsetRange.js'; import { Range } from '../../../../../../editor/common/core/range.js'; -import { getImmediateSilentSlashCommandPart, layoutChatWidgetForInputHeight } from '../../../browser/widget/chatWidget.js'; +import { acceptAndAwaitSentRequest, getImmediateSilentSlashCommandPart, layoutChatWidgetForInputHeight } from '../../../browser/widget/chatWidget.js'; +import { ChatSendResult, ChatSendResultSent, IChatSendRequestData } from '../../../common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../common/constants.js'; import { ChatRequestSlashCommandPart, ChatRequestTextPart, IParsedChatRequest } from '../../../common/requestParser/chatParserTypes.js'; @@ -84,3 +86,63 @@ suite('ChatWidget', () => { ]); }); }); + +suite('ChatWidget - acceptAndAwaitSentRequest', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + function sentResult(): ChatSendResultSent { + return { kind: 'sent', data: {} as IChatSendRequestData }; + } + + test('an immediately sent request is accepted and returned', async () => { + let accepted = 0; + const result = sentResult(); + + const sent = await acceptAndAwaitSentRequest(result, () => accepted++); + + assert.deepStrictEqual({ accepted, sent }, { accepted: 1, sent: result }); + }); + + test('a queued request is accepted before the queued request settles', async () => { + const deferred = new DeferredPromise<ChatSendResult>(); + let accepted = 0; + + const pending = acceptAndAwaitSentRequest({ kind: 'queued', deferred: deferred.p }, () => accepted++); + // The queued request has not run yet, so `pending` is still unresolved here. + const acceptedWhileQueued = accepted === 1; + + const result = sentResult(); + await deferred.complete(result); + + assert.deepStrictEqual({ acceptedWhileQueued, accepted, sent: await pending }, { + acceptedWhileQueued: true, + accepted: 1, + sent: result, + }); + }); + + test('a rejected request is never accepted', async () => { + let accepted = 0; + + const sent = await acceptAndAwaitSentRequest({ kind: 'rejected', reason: 'Empty message' }, () => accepted++); + + assert.deepStrictEqual({ accepted, sent }, { accepted: 0, sent: undefined }); + }); + + test('a queued request that is rejected when it runs stays accepted but is not sent', async () => { + const deferred = new DeferredPromise<ChatSendResult>(); + let accepted = 0; + + const pending = acceptAndAwaitSentRequest({ kind: 'queued', deferred: deferred.p }, () => accepted++); + await deferred.complete({ kind: 'rejected', reason: 'Session is read-only' }); + + assert.deepStrictEqual({ accepted, sent: await pending }, { accepted: 1, sent: undefined }); + }); + + test('accepting is optional', async () => { + const result = sentResult(); + + assert.strictEqual(await acceptAndAwaitSentRequest(result), result); + }); +}); From 9bfc406666f36355e57fee31085c042ac63d1324 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:58:03 +0200 Subject: [PATCH 62/86] sessions: wait for the session's chat model to load before submitting feedback (#328388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `IChatWidgetService.getWidgetBySessionResource` matches on the widget's *loaded* view model, so it returns `undefined` while a session is still being restored into its chat widget. Submitting feedback in that window bailed out with an error log and the feedback was silently dropped. This is reachable from the comments input banner: "Address comments" first accepts the created comments — which immediately hides the banner, since it only renders comments in the `Created` state — and then submits. With no widget yet, nothing was sent and the banner was gone. Add a `whenWidgetForSession` helper that resolves the widget once any widget loads the session (watching `onDidChangeViewModel` on known widgets plus `onDidAddWidget`), with a timeout, and use it in both agent feedback submit paths so every submit entry point — the editor overlay, the Changes toolbar and the comments banner — benefits. Also replaces the arbitrary 100ms sleep in `addFeedbackAndSubmit`'s no-widget fallback, which was papering over the same race. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/agentFeedbackService.ts | 70 ++++++++-- .../test/browser/agentFeedbackService.test.ts | 125 +++++++++++++++++- 2 files changed, 181 insertions(+), 14 deletions(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index 56d68129658..bd3753c9c6a 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../../base/common/event.js'; -import { DeferredPromise } from '../../../../base/common/async.js'; +import { DeferredPromise, raceTimeout } from '../../../../base/common/async.js'; import { createSingleCallFunction } from '../../../../base/common/functional.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { derived, IObservable, runOnChange } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; @@ -44,6 +44,57 @@ export { AgentFeedbackKind, AgentFeedbackState, type IAgentFeedback }; /** Shared feedback scope for every undefined or uncreated active session. */ export const AGENT_FEEDBACK_NEW_SESSION_RESOURCE = URI.from({ scheme: 'agent-feedback', path: '/new-session' }); +/** + * How long submitting feedback waits for the session's chat model to be loaded into a chat widget + * before giving up. + */ +const WIDGET_LOAD_TIMEOUT_MS = 10_000; + +/** + * Resolves the chat widget that has the session loaded, waiting for it to appear when the session's + * model has not been loaded into a widget yet. + * + * Feedback can be submitted (e.g. from the Changes editor or the comments input banner) while the + * session is still being restored into its chat widget. `getWidgetBySessionResource` matches on the + * widget's *loaded* view model, so it returns `undefined` until the model arrives — submitting then + * would silently drop the feedback. Resolves `undefined` if no widget loads the session in time. + * + * Exported for tests. + */ +export async function whenWidgetForSession(chatWidgetService: IChatWidgetService, sessionResource: URI, timeoutMs: number = WIDGET_LOAD_TIMEOUT_MS): Promise<IChatWidget | undefined> { + const existing = chatWidgetService.getWidgetBySessionResource(sessionResource); + if (existing) { + return existing; + } + + const store = new DisposableStore(); + try { + const loaded = new Promise<IChatWidget>(resolve => { + const check = () => { + const widget = chatWidgetService.getWidgetBySessionResource(sessionResource); + if (widget) { + resolve(widget); + } + }; + + const observe = (candidate: IChatWidget) => store.add(candidate.onDidChangeViewModel(check)); + + chatWidgetService.getAllWidgets().forEach(observe); + store.add(chatWidgetService.onDidAddWidget(added => { + observe(added); + check(); + })); + + // A widget may have loaded the session while the listeners were being wired up. + check(); + }); + + return await raceTimeout(loaded, timeoutMs); + } finally { + store.dispose(); + } +} + export interface INavigableSessionComment { readonly id: string; } @@ -256,10 +307,10 @@ export interface IAgentFeedbackService { /** * Submit the currently accumulated accepted feedback for the session to the - * agent and mark those items as submitted. Resolves once the request has been - * accepted by the chat widget — which, while another request is in progress, - * means it was queued rather than sent. Returns whether the feedback was - * submitted. + * agent and mark those items as submitted. Waits for the session's chat model to be loaded + * into a chat widget, then resolves once the request has been accepted by that widget — which, + * while another request is in progress, means it was queued rather than sent. Returns whether + * the feedback was submitted. */ submitFeedback(sessionResource: URI): Promise<boolean>; @@ -808,9 +859,9 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe if (!this._isAgentHostSession(sessionResource)) { // Wait for the attachment contribution to update the chat widget's attachment model - const widget = this._chatWidgetService.getWidgetBySessionResource(sessionResource); + const widget = await whenWidgetForSession(this._chatWidgetService, sessionResource); if (widget) { - const attachmentId = 'agentFeedback:' + sessionResource.toString(); + const attachmentId = ATTACHMENT_ID_PREFIX + sessionResource.toString(); const hasAttachment = () => widget.attachmentModel.attachments.some(a => a.id === attachmentId); if (!hasAttachment()) { @@ -820,7 +871,6 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe } } else { this._logService.error('[AgentFeedback] addFeedbackAndSubmit: no chat widget found for session, feedback may not be submitted correctly', sessionResource.toString()); - await new Promise(resolve => setTimeout(resolve, 100)); } } @@ -840,7 +890,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe return this._sessionsService.submitNewSessionInput(); } - const widget = this._chatWidgetService.getWidgetBySessionResource(sessionResource); + const widget = await whenWidgetForSession(this._chatWidgetService, sessionResource); if (!widget) { this._logService.error('[AgentFeedback] submitFeedback: no chat widget found for session', sessionResource.toString()); return false; diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts index 65ed9ef5502..adfde814666 100644 --- a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts @@ -11,13 +11,13 @@ import { Range } from '../../../../../editor/common/core/range.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { mock } from '../../../../../base/test/common/mock.js'; -import { AGENT_FEEDBACK_NEW_SESSION_RESOURCE, AgentFeedbackKind, AgentFeedbackService, AgentFeedbackState, IAgentFeedbackService } from '../../browser/agentFeedbackService.js'; +import { AGENT_FEEDBACK_NEW_SESSION_RESOURCE, AgentFeedbackKind, AgentFeedbackService, AgentFeedbackState, IAgentFeedbackService, whenWidgetForSession } from '../../browser/agentFeedbackService.js'; import { getSessionEditorComments } from '../../browser/sessionEditorComments.js'; import { IChatEditingService } from '../../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; -import { IChatWidget, IChatWidgetService, IChatAcceptInputOptions } from '../../../../../workbench/contrib/chat/browser/chat.js'; +import { IChatWidget, IChatWidgetService, IChatAcceptInputOptions, IChatWidgetViewModelChangeEvent } from '../../../../../workbench/contrib/chat/browser/chat.js'; import { IAgentFeedbackVariableEntry } from '../../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { DeferredPromise } from '../../../../../base/common/async.js'; +import { DeferredPromise, timeout } from '../../../../../base/common/async.js'; import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IEditorService, IVisibleEditorsChangeEvent } from '../../../../../workbench/services/editor/common/editorService.js'; @@ -661,12 +661,17 @@ suite('AgentFeedbackService - Submit (agent host)', () => { let acceptInputSent: DeferredPromise<void>; /** Whether the widget hands the request over to the chat service. */ let acceptsRequest: boolean; + /** Whether the widget has the session's chat model loaded. */ + let sessionLoaded: boolean; + /** Simulates the widget loading the session's chat model. */ + let loadSession: () => void; setup(() => { widgetOps = []; addedEntries = []; acceptInputSent = new DeferredPromise<void>(); acceptsRequest = true; + sessionLoaded = true; const instantiationService = store.add(new TestInstantiationService()); instantiationService.stub(IChatEditingService, new class extends mock<IChatEditingService>() { }); instantiationService.stub(ITelemetryService, NullTelemetryService); @@ -685,7 +690,9 @@ suite('AgentFeedbackService - Submit (agent host)', () => { }); instantiationService.stub(ISessionsService, { activeSession: observableValue<IActiveSession | undefined>('activeSession', undefined) } as unknown as ISessionsService); + const onDidChangeViewModel = store.add(new Emitter<IChatWidgetViewModelChangeEvent>()); const widget = { + onDidChangeViewModel: onDidChangeViewModel.event, attachmentModel: { attachments: [], delete: (id: string) => widgetOps.push(`delete:${id}`), @@ -704,8 +711,16 @@ suite('AgentFeedbackService - Submit (agent host)', () => { return undefined; }, } as unknown as IChatWidget; + loadSession = () => { + sessionLoaded = true; + onDidChangeViewModel.fire({ previousSessionResource: undefined, currentSessionResource: session }); + }; instantiationService.stub(IChatWidgetService, new class extends mock<IChatWidgetService>() { - override getWidgetBySessionResource(_resource: URI): IChatWidget { return widget; } + override onDidAddWidget = Event.None; + override getAllWidgets(): readonly IChatWidget[] { return [widget]; } + override getWidgetBySessionResource(_resource: URI): IChatWidget | undefined { + return sessionLoaded ? widget : undefined; + } }); service = store.add(instantiationService.createInstance(AgentFeedbackService)); @@ -775,4 +790,106 @@ suite('AgentFeedbackService - Submit (agent host)', () => { state: AgentFeedbackState.Accepted, }); }); + + test('waits for the session model to load into the widget before submitting', async () => { + sessionLoaded = false; + service.addFeedback(session, fileA, r(10), 'Please simplify'); + + const pending = service.submitFeedback(session); + await timeout(0); + const submittedBeforeLoad = widgetOps.length > 0; + + loadSession(); + + assert.deepStrictEqual({ + submittedBeforeLoad, + submitted: await pending, + state: service.getFeedback(session)[0].state, + accepted: widgetOps.includes('accept:/act-on-feedback'), + }, { + submittedBeforeLoad: false, + submitted: true, + state: AgentFeedbackState.Submitted, + accepted: true, + }); + }); +}); + +suite('AgentFeedbackService - whenWidgetForSession', () => { + + const store = new DisposableStore(); + const session = URI.parse('test://session/1'); + + teardown(() => store.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + /** + * Builds a widget service whose single widget only reports the session once `load` is + * called, mirroring a chat widget that has not loaded its model yet. + */ + function createWidgetHost(): { widget: IChatWidget; service: IChatWidgetService; load: () => void } { + const onDidChangeViewModel = store.add(new Emitter<IChatWidgetViewModelChangeEvent>()); + const widget = { onDidChangeViewModel: onDidChangeViewModel.event } as unknown as IChatWidget; + let loaded = false; + + const service = new class extends mock<IChatWidgetService>() { + override onDidAddWidget = Event.None; + override getAllWidgets(): readonly IChatWidget[] { return [widget]; } + override getWidgetBySessionResource(_resource: URI): IChatWidget | undefined { + return loaded ? widget : undefined; + } + }; + + return { + widget, + service, + load: () => { + loaded = true; + onDidChangeViewModel.fire({ previousSessionResource: undefined, currentSessionResource: session }); + }, + }; + } + + test('resolves immediately when the session is already loaded', async () => { + const host = createWidgetHost(); + host.load(); + + assert.strictEqual(await whenWidgetForSession(host.service, session, 0), host.widget); + }); + + test('resolves once a widget loads the session', async () => { + const host = createWidgetHost(); + + const pending = whenWidgetForSession(host.service, session, 5000); + await timeout(0); + host.load(); + + assert.strictEqual(await pending, host.widget); + }); + + test('resolves undefined when no widget loads the session in time', async () => { + const host = createWidgetHost(); + + assert.strictEqual(await whenWidgetForSession(host.service, session, 1), undefined); + }); + + test('resolves when a widget that already has the session is added later', async () => { + const onDidAddWidget = store.add(new Emitter<IChatWidget>()); + const widget = { onDidChangeViewModel: Event.None } as unknown as IChatWidget; + let widgets: IChatWidget[] = []; + + const service = new class extends mock<IChatWidgetService>() { + override onDidAddWidget = onDidAddWidget.event; + override getAllWidgets(): readonly IChatWidget[] { return widgets; } + override getWidgetBySessionResource(_resource: URI): IChatWidget | undefined { return widgets[0]; } + }; + + const pending = whenWidgetForSession(service, session, 5000); + await timeout(0); + widgets = [widget]; + onDidAddWidget.fire(widget); + + assert.strictEqual(await pending, widget); + }); }); From 07175f3b36915b07829245fe9a30cb3cfb31b2b9 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:58:28 +0200 Subject: [PATCH 63/86] AgentHost - add multi-root support for the checkpoint service (#328389) Initial implementation --- .../common/agentHostCheckpointService.ts | 67 ++-- .../agentHost/common/sessionDataService.ts | 27 +- .../node/agentHostChangesetService.ts | 22 +- .../node/agentHostCheckpointService.ts | 332 ++++++++++-------- .../platform/agentHost/node/agentHostMain.ts | 10 +- .../agentHost/node/agentHostReviewService.ts | 42 +-- .../agentHost/node/agentHostServerMain.ts | 6 +- .../platform/agentHost/node/agentService.ts | 32 +- .../agentHost/node/agentSideEffects.ts | 12 +- .../agentHost/node/copilot/copilotAgent.ts | 15 +- .../agentHost/node/sessionDataService.ts | 3 +- .../node/agentHostChangesetService.test.ts | 11 +- .../agentHostReviewService.integrationTest.ts | 2 +- .../agentHost/test/node/agentService.test.ts | 26 +- .../test/node/agentSideEffects.test.ts | 32 +- 15 files changed, 385 insertions(+), 254 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHostCheckpointService.ts b/src/vs/platform/agentHost/common/agentHostCheckpointService.ts index d166b446cf9..28689427968 100644 --- a/src/vs/platform/agentHost/common/agentHostCheckpointService.ts +++ b/src/vs/platform/agentHost/common/agentHostCheckpointService.ts @@ -8,12 +8,6 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; export const IAgentHostCheckpointService = createDecorator<IAgentHostCheckpointService>('agentHostCheckpointService'); -/** - * `session_metadata` key under which the per-session baseline (turn/0) - * checkpoint ref is stored. - */ -export const META_CHECKPOINT_BASE_REF = 'checkpoint.baseRef'; - /** * Returns the canonical name for a per-turn checkpoint ref. * Distinct from the chat extension's `refs/sessions/...` so the two can @@ -42,24 +36,30 @@ export interface IAgentHostCheckpointService { readonly _serviceBrand: undefined; /** - * Captures the session's baseline (turn/0) checkpoint. Idempotent: if - * a baseline already exists for the session, returns the existing ref. - * Returns `undefined` when the working directory is not a git work tree - * (folder-isolation against a non-git folder) or when checkpoint - * capture fails. + * Captures the session's baseline (turn/0) checkpoint in each of + * `workingDirectories`. Idempotent per repository: a directory that + * already has a baseline ref is skipped, as is one that is not a git + * work tree (folder-isolation against a non-git folder). Best-effort — + * a failure for one repository does not stop the others. * * Called once per session, immediately after the session's working - * directory has been resolved and any worktree metadata has been + * directories have been resolved and any worktree metadata has been * persisted (e.g. `CopilotAgent._materializeProvisional`). + * + * The caller must pass the directories it just resolved rather than + * letting this service look them up: at that point the resolved set + * (which for an isolated session is the *worktree*, not the folder the + * user picked) has not necessarily reached the state manager yet, so a + * lookup can silently capture the baseline against the wrong repository. */ - captureBaseline(sessionUri: URI, workingDirectory: URI | undefined): Promise<string | undefined>; + captureBaselineCheckpoint(sessionUri: URI, workingDirectories: readonly URI[] | undefined): Promise<void>; /** - * Captures an end-of-turn checkpoint, chained to the previous turn's - * checkpoint (or the baseline for turn 1). Persists the ref against - * the turn via `ISessionDatabase.setTurnCheckpointRef`. Returns - * `undefined` when the session is not git-backed, the baseline is - * missing, or capture fails. + * Captures an end-of-turn checkpoint in each of `workingDirectories`, + * chained to the previous turn's checkpoint (or the baseline for turn 1). + * Persists the ref against the turn via `ISessionDatabase.setTurnCheckpointRef` + * once at least one repository captured successfully. A directory that is + * not git-backed, or has no baseline, is skipped. * * If the captured tree OID matches the parent's tree OID (no-op turn) * the parent ref is recorded against the turn rather than creating a @@ -68,15 +68,19 @@ export interface IAgentHostCheckpointService { * Called from `AgentSideEffects` when a `ChatTurnComplete` action * fires, BEFORE the changeset service's `onTurnComplete` hook so the * per-turn changeset compute can pick up the new refs. + * + * As with {@link captureBaselineCheckpoint}, the caller supplies the directories + * so that every checkpoint operation is explicit about the repositories + * it acts on rather than depending on live session state. */ - captureTurnCheckpoint(sessionUri: URI, turnId: string): Promise<string | undefined>; + captureTurnCheckpoint(sessionUri: URI, turnId: string, workingDirectories: readonly URI[] | undefined): Promise<void>; /** * Returns the `{ parent, current }` checkpoint refs for a turn, or * `undefined` when either is missing. Used by the changeset service * to decide whether to take the git-diff fast path for per-turn diffs. */ - getTurnCheckpointPair(sessionUri: URI, turnId: string): Promise<{ parent: string; current: string } | undefined>; + getTurnCheckpointPair(sessionUri: URI, turnId: string, workingDirectory?: URI): Promise<{ parent: string; current: string } | undefined>; /** * Returns the session's baseline checkpoint ref, or `undefined` when @@ -84,7 +88,7 @@ export interface IAgentHostCheckpointService { * failed). Used by the changeset service to resolve compare-turns * URIs whose `originalTurnId` is the `BASELINE_TURN_ID` sentinel. */ - getBaselineCheckpointRef(sessionUri: URI): Promise<string | undefined>; + getBaselineCheckpoint(sessionUri: URI, workingDirectory?: URI): Promise<string | undefined>; /** * Deletes every checkpoint ref this service created for the session @@ -93,22 +97,25 @@ export interface IAgentHostCheckpointService { * * Called from a subscriber to `ISessionDataService.onWillDeleteSessionData` * before the session's data directory is removed. + * + * `workingDirectories` identifies the repositories holding the refs. + * There is deliberately no fallback to the session's live state: by + * the time this runs the session has typically already been removed + * from the state manager, so omitting them is a silent no-op that + * leaks the refs. */ - disposeSessionData(sessionUri: URI): Promise<void>; + deleteCheckpoints(sessionUri: URI, workingDirectories?: readonly string[]): Promise<void>; } /** * A no-op implementation of {@link IAgentHostCheckpointService} used as a - * fallback in test fixtures that don't exercise checkpoint capture, and - * as the default value for the optional `_checkpointService` parameter - * on `AgentService` so existing test callsites keep compiling without - * forced fixture updates. + * fallback in test fixtures that don't exercise checkpoint capture. */ export const NULL_CHECKPOINT_SERVICE: IAgentHostCheckpointService = { _serviceBrand: undefined, - captureBaseline: async () => undefined, - captureTurnCheckpoint: async () => undefined, + captureBaselineCheckpoint: async () => { }, + captureTurnCheckpoint: async () => { }, getTurnCheckpointPair: async () => undefined, - getBaselineCheckpointRef: async () => undefined, - disposeSessionData: async () => { }, + getBaselineCheckpoint: async () => undefined, + deleteCheckpoints: async () => { }, }; diff --git a/src/vs/platform/agentHost/common/sessionDataService.ts b/src/vs/platform/agentHost/common/sessionDataService.ts index 7f235cdd735..58fca5ce51f 100644 --- a/src/vs/platform/agentHost/common/sessionDataService.ts +++ b/src/vs/platform/agentHost/common/sessionDataService.ts @@ -391,8 +391,14 @@ export interface ISessionDataService { /** * Recursively deletes the data directory for a session, if it exists. + * + * `workingDirectories` is forwarded verbatim to + * {@link IWillDeleteSessionDataEvent.workingDirectories}. Callers that + * tear down live session state as part of disposal must resolve it + * *before* doing so, otherwise subscribers cannot locate the + * repositories they need to clean up. */ - deleteSessionData(session: URI): Promise<void>; + deleteSessionData(session: URI, workingDirectories?: readonly string[]): Promise<void>; /** * Fires immediately before a session's data directory (and the @@ -405,6 +411,10 @@ export interface ISessionDataService { * list of checkpoint refs from the (still-readable) database and * delete them before the directory is removed. * + * The repositories to clean up are identified by + * {@link IWillDeleteSessionDataEvent.workingDirectories}, which the + * caller resolves before tearing down live session state. + * * Subscribers must own their own error handling — exceptions * propagated out of `waitUntil` promises are logged and ignored; * deletion proceeds regardless. @@ -431,6 +441,21 @@ export interface ISessionDataService { */ export interface IWillDeleteSessionDataEvent { readonly session: URI; + /** + * The session's working directories (index 0 = primary), as resolved + * by the caller of {@link ISessionDataService.deleteSessionData} + * *before* any live session state was torn down. + * + * Subscribers that need to touch the session's repositories (deleting + * checkpoint or reviewed refs) must use this rather than querying + * session state themselves: by the time this event fires the session + * has typically already been removed from the state manager, so a + * live lookup returns `undefined` and the cleanup silently no-ops. + * + * `undefined` when the session had no working directories, or when + * the caller did not supply them. + */ + readonly workingDirectories: readonly string[] | undefined; /** * Register an asynchronous task that must settle before the session's * data directory is removed. diff --git a/src/vs/platform/agentHost/node/agentHostChangesetService.ts b/src/vs/platform/agentHost/node/agentHostChangesetService.ts index b66a493809b..e7aa0a3ab9e 100644 --- a/src/vs/platform/agentHost/node/agentHostChangesetService.ts +++ b/src/vs/platform/agentHost/node/agentHostChangesetService.ts @@ -37,7 +37,6 @@ import { IAgentHostGitService, META_DIFF_BASE_BRANCH, resolveDiffBaseBranchName import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; import { NodeWorkerDiffComputeService } from './diffComputeService.js'; import { computeSessionDiffs, computeTurnDiffs, computeUnionedDiffs, type IIncrementalDiffOptions, type ISessionDiffSource } from './sessionDiffAggregator.js'; -import { META_CHECKPOINT_WORKING_DIR } from './agentHostCheckpointService.js'; import { IAgentHostChangesetService, IPersistedChangesetMetadata, IRestoredChangesetDiffs, CHANGESET_DB_METADATA_KEYS, META_CHANGES_SUMMARY, META_CHANGESET_BRANCH, META_CHANGESET_SESSION, META_LEGACY_DIFFS, StaticChangesetKind } from '../common/agentHostChangesetService.js'; import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js'; import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js'; @@ -475,7 +474,7 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC this._publishChangesetDiffs(session, compareUri, []); return compareUri; } - const workingDir = await this._resolveWorkingDirectory(ref.object); + const workingDir = await this._resolveWorkingDirectory(session); if (!workingDir) { this._stateManager.dispatchServerAction(compareUri, { type: ActionType.ChangesetStatusChanged, @@ -588,7 +587,7 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC private async _computeTurnDiffsPreferCheckpoint(session: ProtocolURI, db: ISessionDatabase, turnId: string): Promise<readonly ISessionFileDiff[]> { const pair = await this._checkpointService.getTurnCheckpointPair(URI.parse(session), turnId); if (pair && pair.parent !== pair.current) { - const workingDir = await this._resolveWorkingDirectory(db); + const workingDir = await this._resolveWorkingDirectory(session); if (workingDir) { const fromRefDiffs = await this._gitService.computeFileDiffsBetweenRefs(workingDir, { sessionUri: session, @@ -609,13 +608,14 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC return computeTurnDiffs(session, db, this._diffComputeService, turnId); } - private async _resolveWorkingDirectory(db: ISessionDatabase): Promise<URI | undefined> { - // Checkpoint baseline writes `checkpoint.workingDir` alongside - // `checkpoint.baseRef`. We use that as the canonical working - // directory for checkpoint diff computation; reading it here keeps - // the changeset service out of agent-specific metadata keys. - const raw = await db.getMetadata(META_CHECKPOINT_WORKING_DIR); - return raw ? URI.parse(raw) : undefined; + private async _resolveWorkingDirectory(session: ProtocolURI): Promise<URI | undefined> { + // For the time being we default to the first working directory in the list, if any. + // In the future we may want to support multiple working directories per session, + // but for now we only support one. + const workingDirectories = this._configurationService.getEffectiveWorkingDirectories(session); + return workingDirectories && workingDirectories.length > 0 + ? URI.parse(workingDirectories[0]) + : undefined; } // ---- Lifecycle hooks invoked by AgentSideEffects ----------------------- @@ -1027,7 +1027,7 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC const sessionUri = URI.parse(session); const [baseline, pair] = await Promise.all([ - this._checkpointService.getBaselineCheckpointRef(sessionUri), + this._checkpointService.getBaselineCheckpoint(sessionUri), this._checkpointService.getTurnCheckpointPair(sessionUri, latestTurnId), ]); if (!baseline || !pair) { diff --git a/src/vs/platform/agentHost/node/agentHostCheckpointService.ts b/src/vs/platform/agentHost/node/agentHostCheckpointService.ts index 22459af8929..222407d2d51 100644 --- a/src/vs/platform/agentHost/node/agentHostCheckpointService.ts +++ b/src/vs/platform/agentHost/node/agentHostCheckpointService.ts @@ -7,19 +7,11 @@ import { SequencerByKey } from '../../../base/common/async.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; import { ILogService } from '../../log/common/log.js'; -import { IAgentHostCheckpointService, META_CHECKPOINT_BASE_REF, buildCheckpointRefName } from '../common/agentHostCheckpointService.js'; +import { IAgentHostCheckpointService, buildCheckpointRefName } from '../common/agentHostCheckpointService.js'; import { AgentSession } from '../common/agentService.js'; import { ISessionDatabase, ISessionDataService } from '../common/sessionDataService.js'; import { IAgentHostGitService } from '../common/agentHostGitService.js'; - -/** - * `session_metadata` key under which the working directory used for - * checkpoint capture is persisted (set when the baseline is created). - * Stored as `URI.toString()`. Read by `captureTurnCheckpoint` / - * `disposeSessionData` so they can resolve the repo without per-call - * working-directory plumbing. - */ -export const META_CHECKPOINT_WORKING_DIR = 'checkpoint.workingDir'; +import { IAgentConfigurationService } from './agentConfigurationService.js'; export class AgentHostCheckpointService extends Disposable implements IAgentHostCheckpointService { declare readonly _serviceBrand: undefined; @@ -34,6 +26,7 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost constructor( @ISessionDataService private readonly _sessionDataService: ISessionDataService, + @IAgentConfigurationService private readonly _agentConfigService: IAgentConfigurationService, @IAgentHostGitService private readonly _gitService: IAgentHostGitService, @ILogService private readonly _logService: ILogService, ) { @@ -42,185 +35,222 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost // deleted, enumerate and delete every checkpoint ref we created // for that session BEFORE the database file disappears. The // `waitUntil` API blocks `deleteSessionData` until our promise - // settles, so the deletion can't race the ref read. + // settles, so the deletion can't race the ref read. The working + // directories come from the event because the session has already + // been removed from the state manager by this point. this._register(this._sessionDataService.onWillDeleteSessionData(e => { - e.waitUntil(this.disposeSessionData(e.session)); + e.waitUntil(this.deleteCheckpoints(e.session, e.workingDirectories)); })); } - captureBaseline(sessionUri: URI, workingDirectory: URI | undefined): Promise<string | undefined> { - return this._sequencer.queue(sessionUri.toString(), () => this._captureBaseline(sessionUri, workingDirectory)); + captureBaselineCheckpoint(sessionUri: URI, workingDirectories: readonly URI[] | undefined): Promise<void> { + return this._sequencer.queue(sessionUri.toString(), () => this._captureBaseline(sessionUri, workingDirectories)); } - private async _captureBaseline(sessionUri: URI, workingDirectory: URI | undefined): Promise<string | undefined> { - if (!workingDirectory) { - return undefined; + private async _captureBaseline(sessionUri: URI, workingDirectories: readonly URI[] | undefined): Promise<void> { + if (!workingDirectories || workingDirectories.length === 0) { + this._logService.trace(`[AgentHostCheckpoint] Skipping baseline capture for ${sessionUri.toString()} as no working directories are found`); + return; } - const ref = this._sessionDataService.openDatabase(sessionUri); - try { - const existing = await ref.object.getMetadata(META_CHECKPOINT_BASE_REF); - if (existing) { - return existing; + + const sanitized = this._sanitizedSessionId(sessionUri); + const baselineRefName = buildCheckpointRefName(sanitized, 0); + + for (const workingDirectoryUri of workingDirectories) { + try { + // Check that the working directory has a git repository + const repositoryRootUri = await this._gitService.getRepositoryRoot(workingDirectoryUri); + if (!repositoryRootUri) { + continue; + } + + // Check if the baseline ref already exists + const baselineCheckpointRef = await this.getBaselineCheckpoint(sessionUri, repositoryRootUri); + if (baselineCheckpointRef) { + continue; + } + + // Create checkpoint commit + const commit = await this._writeCheckpointCommit(repositoryRootUri, undefined, `Agent host session ${sanitized} - baseline checkpoint`); + if (!commit) { + continue; + } + + // Update the baseline ref to point to the new commit + await this._gitService.updateRef(repositoryRootUri, baselineRefName, commit); + this._logService.trace(`[AgentHostCheckpoint] Captured baseline for ${sessionUri.toString()} at ${baselineRefName} in working directory ${workingDirectoryUri.toString()}`); + } catch (err) { + this._logService.warn(`[AgentHostCheckpoint] Failed to capture baseline for ${sessionUri.toString()} in working directory ${workingDirectoryUri.toString()}`, err); } - const sanitized = this._sanitizedSessionId(sessionUri); - const refName = buildCheckpointRefName(sanitized, 0); - const commit = await this._writeCheckpointCommit(workingDirectory, undefined, `Agent host session ${sanitized} - baseline checkpoint`); - if (!commit) { - return undefined; - } - const repoRoot = await this._gitService.getRepositoryRoot(workingDirectory); - if (!repoRoot) { - return undefined; - } - await this._gitService.updateRef(repoRoot, refName, commit.commitOid); - await ref.object.setMetadata(META_CHECKPOINT_BASE_REF, refName); - await ref.object.setMetadata(META_CHECKPOINT_WORKING_DIR, workingDirectory.toString()); - this._logService.trace(`[AgentHostCheckpoint] Captured baseline for ${sessionUri.toString()} at ${refName}`); - return refName; - } catch (err) { - this._logService.warn(`[AgentHostCheckpoint] Failed to capture baseline for ${sessionUri.toString()}`, err); - return undefined; - } finally { - ref.dispose(); } } - captureTurnCheckpoint(sessionUri: URI, turnId: string): Promise<string | undefined> { - return this._sequencer.queue(sessionUri.toString(), () => this._captureTurnCheckpoint(sessionUri, turnId)); + captureTurnCheckpoint(sessionUri: URI, turnId: string, workingDirectories: readonly URI[] | undefined): Promise<void> { + return this._sequencer.queue(sessionUri.toString(), () => this._captureTurnCheckpoint(sessionUri, turnId, workingDirectories)); } - private async _captureTurnCheckpoint(sessionUri: URI, turnId: string): Promise<string | undefined> { + private async _captureTurnCheckpoint(sessionUri: URI, turnId: string, workingDirectories: readonly URI[] | undefined): Promise<void> { + if (!workingDirectories || workingDirectories.length === 0) { + this._logService.trace(`[AgentHostCheckpoint] Skipping turn checkpoint capture for ${sessionUri.toString()} as no working directories are found`); + return; + } + const ref = this._sessionDataService.openDatabase(sessionUri); + try { - const [baseRef, workingDirRaw, existing, prevTurnRef] = await Promise.all([ - ref.object.getMetadata(META_CHECKPOINT_BASE_REF), - ref.object.getMetadata(META_CHECKPOINT_WORKING_DIR), - ref.object.getTurnCheckpointRef(turnId), - ref.object.getPreviousCheckpointRef(turnId), - ]); - if (existing) { - return existing; - } - if (!baseRef || !workingDirRaw) { - // Baseline never captured — session is not git-backed or - // baseline failed. Nothing to chain from. - return undefined; - } - const workingDirectory = URI.parse(workingDirRaw); - const repoRoot = await this._gitService.getRepositoryRoot(workingDirectory); - if (!repoRoot) { - return undefined; - } - const parentRef = prevTurnRef ?? baseRef; - const parentCommitOid = await this._gitService.revParse(repoRoot, parentRef); - if (!parentCommitOid) { - this._logService.warn(`[AgentHostCheckpoint] Parent ref ${parentRef} missing for session ${sessionUri.toString()}`); - return undefined; - } - - const tree = await this._gitService.captureWorkingTreeAsTree(workingDirectory); - if (!tree) { - return undefined; - } - - // No-op turn: if the tree is identical to the parent's tree, - // don't create a redundant commit/ref — point the turn at the - // parent ref so per-turn diffs against it are empty by - // construction. - const parentTree = await this._gitService.revParse(repoRoot, `${parentCommitOid}^{tree}`); - if (parentTree && parentTree === tree) { - await ref.object.setTurnCheckpointRef(turnId, parentRef); - this._logService.trace(`[AgentHostCheckpoint] No-op turn ${turnId} for ${sessionUri.toString()}; reusing ${parentRef}`); - return parentRef; - } - const sanitized = this._sanitizedSessionId(sessionUri); const turnNumber = await this._nextTurnNumber(ref.object); const refName = buildCheckpointRefName(sanitized, turnNumber); - const commitOid = await this._gitService.commitTree(repoRoot, tree, parentCommitOid, `Agent host session ${sanitized} - turn ${turnNumber}`); - if (!commitOid) { - return undefined; - } - await this._gitService.updateRef(repoRoot, refName, commitOid); - await ref.object.setTurnCheckpointRef(turnId, refName); - this._logService.trace(`[AgentHostCheckpoint] Captured turn ${turnNumber} for ${sessionUri.toString()} at ${refName}`); - return refName; - } catch (err) { - this._logService.warn(`[AgentHostCheckpoint] Failed to capture turn checkpoint for ${sessionUri.toString()}/${turnId}`, err); - return undefined; - } finally { - ref.dispose(); - } - } - async getTurnCheckpointPair(sessionUri: URI, turnId: string): Promise<{ parent: string; current: string } | undefined> { - const ref = this._sessionDataService.openDatabase(sessionUri); - try { - const [current, prev, baseRef] = await Promise.all([ + const [checkpointRef, prevTurnCheckpointRef] = await Promise.all([ ref.object.getTurnCheckpointRef(turnId), ref.object.getPreviousCheckpointRef(turnId), - ref.object.getMetadata(META_CHECKPOINT_BASE_REF), ]); - if (!current) { - return undefined; + + if (checkpointRef) { + // Already captured for this + // turn, return the existing ref. + return; } - const parent = prev ?? baseRef; - if (!parent) { - return undefined; + + let capturedCheckpointRef = false; + for (const workingDirectoryUri of workingDirectories) { + try { + // Check that the working directory has a git repository + const repositoryRootUri = await this._gitService.getRepositoryRoot(workingDirectoryUri); + if (!repositoryRootUri) { + continue; + } + + // Check if the baseline ref exists for this repository. If it + // doesn't exist, we cannot capture a turn checkpoint for this repository. + const baselineCheckpointRef = await this.getBaselineCheckpoint(sessionUri, repositoryRootUri); + if (!baselineCheckpointRef) { + continue; + } + + const parentRef = prevTurnCheckpointRef ?? baselineCheckpointRef; + const parentCommitOid = await this._gitService.revParse(repositoryRootUri, parentRef); + if (!parentCommitOid) { + this._logService.warn(`[AgentHostCheckpoint] Parent ref ${parentRef} missing for session ${sessionUri.toString()} in working directory ${workingDirectoryUri.toString()}`); + continue; + } + + const tree = await this._gitService.captureWorkingTreeAsTree(repositoryRootUri); + if (!tree) { + continue; + } + + const commitOid = await this._gitService.commitTree(repositoryRootUri, tree, parentCommitOid, `Agent host session ${sanitized} - turn ${turnNumber}`); + if (!commitOid) { + continue; + } + + await this._gitService.updateRef(repositoryRootUri, refName, commitOid); + capturedCheckpointRef = true; + + this._logService.trace(`[AgentHostCheckpoint] Captured turn ${turnNumber} for ${sessionUri.toString()} in working directory ${workingDirectoryUri.toString()} at ${refName}`); + } catch (err) { + this._logService.warn(`[AgentHostCheckpoint] Failed to capture turn checkpoint for ${sessionUri.toString()} in working directory ${workingDirectoryUri.toString()}`, err); + } } - return { parent, current }; + + if (capturedCheckpointRef) { + await ref.object.setTurnCheckpointRef(turnId, refName); + } + } catch (err) { + this._logService.warn(`[AgentHostCheckpoint] Failed to capture turn checkpoint for ${sessionUri.toString()}/${turnId}`, err); } finally { ref.dispose(); } } - async getBaselineCheckpointRef(sessionUri: URI): Promise<string | undefined> { + async getTurnCheckpointPair( + sessionUri: URI, + turnId: string, + workingDirectory?: URI + ): Promise<{ parent: string; current: string } | undefined> { const ref = this._sessionDataService.openDatabase(sessionUri); try { - return await ref.object.getMetadata(META_CHECKPOINT_BASE_REF); + const [currentCheckpointRef, previousCheckpointRef, baselineCheckpointRef] = await Promise.all([ + ref.object.getTurnCheckpointRef(turnId), + ref.object.getPreviousCheckpointRef(turnId), + this.getBaselineCheckpoint(sessionUri, workingDirectory) + ]); + if (!currentCheckpointRef || !baselineCheckpointRef) { + return undefined; + } + + return { + current: currentCheckpointRef, + parent: previousCheckpointRef ?? baselineCheckpointRef + }; } finally { ref.dispose(); } } - async disposeSessionData(sessionUri: URI): Promise<void> { - await this._sequencer.queue(sessionUri.toString(), () => this._disposeSessionData(sessionUri)); + async getBaselineCheckpoint(sessionUri: URI, workingDirectory?: URI): Promise<string | undefined> { + if (!workingDirectory) { + const workingDirectories = this._agentConfigService.getEffectiveWorkingDirectories(sessionUri.toString()); + if (!workingDirectories || workingDirectories.length === 0) { + return undefined; + } + + workingDirectory = URI.parse(workingDirectories[0]); + } + + const sanitized = this._sanitizedSessionId(sessionUri); + const baselineRefName = buildCheckpointRefName(sanitized, 0); + + const baselineRef = await this._gitService.revParse(workingDirectory, baselineRefName); + return baselineRef ? baselineRefName : undefined; } - private async _disposeSessionData(sessionUri: URI): Promise<void> { + async deleteCheckpoints(sessionUri: URI, workingDirectories?: readonly string[]): Promise<void> { + await this._sequencer.queue(sessionUri.toString(), () => this._deleteCheckpoints(sessionUri, workingDirectories)); + } + + private async _deleteCheckpoints(sessionUri: URI, workingDirectories?: readonly string[]): Promise<void> { + if (!workingDirectories || workingDirectories.length === 0) { + return; + } + const refHandle = await this._sessionDataService.tryOpenDatabase(sessionUri); if (!refHandle) { return; } + try { - const [workingDirRaw, baseRef, turnRefs] = await Promise.all([ - refHandle.object.getMetadata(META_CHECKPOINT_WORKING_DIR), - refHandle.object.getMetadata(META_CHECKPOINT_BASE_REF), - refHandle.object.getAllCheckpointRefs(), - ]); - if (!workingDirRaw) { + const turnRefs = await refHandle.object.getAllCheckpointRefs(); + if (turnRefs.length === 0) { return; } - const workingDirectory = URI.parse(workingDirRaw); - const repoRoot = await this._gitService.getRepositoryRoot(workingDirectory); - if (!repoRoot) { - return; + + for (const workingDirectory of workingDirectories) { + try { + const workingDirectoryUri = URI.parse(workingDirectory); + + const repositoryRootUri = await this._gitService.getRepositoryRoot(workingDirectoryUri); + if (!repositoryRootUri) { + continue; + } + + const baselineCheckpointRef = await this.getBaselineCheckpoint(sessionUri, repositoryRootUri); + if (!baselineCheckpointRef) { + continue; + } + + // Dedup baseRef and turnRefs (a no-op turn may reuse its + // parent's ref). Deleting the same ref twice is harmless but + // noisy, and the batch API takes a list. + const checkpointRefs = new Set<string>([baselineCheckpointRef, ...turnRefs]); + await this._gitService.deleteRefs(repositoryRootUri, [...checkpointRefs]); + this._logService.trace(`[AgentHostCheckpoint] Deleted ${checkpointRefs.size} checkpoint refs for ${sessionUri.toString()} in working directory ${workingDirectory}`); + } catch (err) { + this._logService.warn(`[AgentHostCheckpoint] Failed to delete checkpoint refs for ${sessionUri.toString()} in working directory ${workingDirectory}`, err); + } } - // Dedup baseRef and turnRefs (a no-op turn may reuse its - // parent's ref). Deleting the same ref twice is harmless but - // noisy, and the batch API takes a list. - const all = new Set<string>(); - if (baseRef) { - all.add(baseRef); - } - for (const r of turnRefs) { - all.add(r); - } - if (all.size === 0) { - return; - } - await this._gitService.deleteRefs(repoRoot, [...all]); - this._logService.trace(`[AgentHostCheckpoint] Deleted ${all.size} checkpoint refs for ${sessionUri.toString()}`); } catch (err) { this._logService.warn(`[AgentHostCheckpoint] Failed to dispose checkpoint refs for ${sessionUri.toString()}`, err); } finally { @@ -229,23 +259,21 @@ export class AgentHostCheckpointService extends Disposable implements IAgentHost } private async _writeCheckpointCommit( - workingDirectory: URI, + repositoryRootUri: URI, parentOid: string | undefined, message: string, - ): Promise<{ commitOid: string } | undefined> { - const tree = await this._gitService.captureWorkingTreeAsTree(workingDirectory); + ): Promise<string | undefined> { + const tree = await this._gitService.captureWorkingTreeAsTree(repositoryRootUri); if (!tree) { return undefined; } - const repoRoot = await this._gitService.getRepositoryRoot(workingDirectory); - if (!repoRoot) { - return undefined; - } - const commitOid = await this._gitService.commitTree(repoRoot, tree, parentOid, message); + + const commitOid = await this._gitService.commitTree(repositoryRootUri, tree, parentOid, message); if (!commitOid) { return undefined; } - return { commitOid }; + + return commitOid; } /** diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index 07df867e2f3..af8d32ac0bd 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -86,7 +86,6 @@ import { IAgentPluginManager } from '../common/agentPluginManager.js'; import { AgentPluginManager } from './agentPluginManager.js'; import { AgentHostGitService } from './agentHostGitService.js'; import { IAgentHostGitService } from '../common/agentHostGitService.js'; -import { AgentHostCheckpointService } from './agentHostCheckpointService.js'; import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js'; import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; @@ -185,12 +184,6 @@ async function startAgentHost(): Promise<void> { diServices.set(ISandboxHelperService, new SandboxHelperService()); const gitService = instantiationService.createInstance(AgentHostGitService); diServices.set(IAgentHostGitService, gitService); - // Checkpoint service depends on session data + git services, so - // construct it AFTER both are registered. Consumed by CopilotAgent - // (baseline capture) and AgentService's inner DI (changeset - // pipeline / end-of-turn capture). - const checkpointService = disposables.add(instantiationService.createInstance(AgentHostCheckpointService)); - diServices.set(IAgentHostCheckpointService, checkpointService); // 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. @@ -210,7 +203,7 @@ async function startAgentHost(): Promise<void> { diServices.set(IByokLmProxyService, byokLmProxyService); const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService, fetchFn)); diServices.set(IAgentHostOTelService, agentHostOTelService); - agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, checkpointService, rootConfigResource, telemetryService, fileMonitorService, undefined, fetchFn, [createCodexProviderConfiguration(environmentService.userHome)]); + agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, rootConfigResource, telemetryService, fileMonitorService, undefined, fetchFn, [createCodexProviderConfiguration(environmentService.userHome)]); const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); diServices.set(INetworkDiagnosticsService, networkDiagnosticsService); agentService.setNetworkDiagnosticsService(networkDiagnosticsService); @@ -231,6 +224,7 @@ async function startAgentHost(): Promise<void> { diServices.set(IEditArcReporterService, editArcReporterService); diServices.set(IAgentHostGitHubEndpointService, agentService.gitHubEndpointService); diServices.set(IAgentHostCompletions, agentService.completionsService); + diServices.set(IAgentHostCheckpointService, agentService.checkpointService); // CopilotApiService and the proxies that consume it are created AFTER the // GitHub endpoint service is re-exported (above) so CAPI endpoint discovery diff --git a/src/vs/platform/agentHost/node/agentHostReviewService.ts b/src/vs/platform/agentHost/node/agentHostReviewService.ts index 73b0c63766a..6da7c46519e 100644 --- a/src/vs/platform/agentHost/node/agentHostReviewService.ts +++ b/src/vs/platform/agentHost/node/agentHostReviewService.ts @@ -51,10 +51,11 @@ export class AgentHostReviewService extends Disposable implements IAgentHostRevi // When a session's data directory is about to be deleted, delete the // reviewed ref we created for it. The working directory needed to - // resolve the repository root is supplied by the event (resolved from - // live session state) so we don't persist our own copy. + // resolve the repository root is supplied by the event (resolved + // before the session's live state was torn down) so we don't + // persist our own copy. this._register(this._sessionDataService.onWillDeleteSessionData(e => { - e.waitUntil(this.disposeSessionData(e.session.toString())); + e.waitUntil(this.disposeSessionData(e.session.toString(), e.workingDirectories)); })); } @@ -229,30 +230,31 @@ export class AgentHostReviewService extends Disposable implements IAgentHostRevi return { repoRoot, baselineTree, reviewedRef, reviewedCommit, reviewedTree }; } - async disposeSessionData(session: ProtocolURI): Promise<void> { - await this._sequencer.queue(session, () => this._disposeSessionData(session)); + async disposeSessionData(session: ProtocolURI, workingDirectories?: readonly string[]): Promise<void> { + await this._sequencer.queue(session, () => this._disposeSessionData(session, workingDirectories)); } - private async _disposeSessionData(session: ProtocolURI): Promise<void> { - const workingDirectory = this._stateManager.getSessionState(session)?.workingDirectories?.[0]; - if (!workingDirectory) { - // No working directory means we can't resolve the repository root - // (session was never git-backed, or its working directory is gone). + private async _disposeSessionData(session: ProtocolURI, workingDirectories?: readonly string[]): Promise<void> { + if (!workingDirectories || workingDirectories.length === 0) { return; } - const repoRoot = await this._gitService.getRepositoryRoot(URI.parse(workingDirectory)); - if (!repoRoot) { - return; - } + const sanitizedSessionId = this._sanitizedSessionId(session); + const reviewedRef = buildReviewedRefName(sanitizedSessionId); - try { - const reviewedRef = buildReviewedRefName(this._sanitizedSessionId(session)); - await this._gitService.deleteRefs(repoRoot, [reviewedRef]); + for (const workingDirectory of workingDirectories) { + try { + const workingDirectoryUri = URI.parse(workingDirectory); + const repositoryRootUri = await this._gitService.getRepositoryRoot(workingDirectoryUri); + if (!repositoryRootUri) { + continue; + } - this._logService.trace(`[AgentHostReview][_disposeSessionData] Deleted reviewed ref for ${session}`); - } catch (err) { - this._logService.warn(`[AgentHostReview][_disposeSessionData] Failed to dispose reviewed ref for ${session}`, err); + await this._gitService.deleteRefs(repositoryRootUri, [reviewedRef]); + this._logService.trace(`[AgentHostReview][_disposeSessionData] Deleted reviewed ref for ${session} in working directory ${workingDirectory}`); + } catch (err) { + this._logService.warn(`[AgentHostReview][_disposeSessionData] Failed to dispose reviewed ref for ${session} in working directory ${workingDirectory}`, err); + } } } diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index 50123ea0cc0..3f86955bed6 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -83,7 +83,6 @@ import { IAgentPluginManager } from '../common/agentPluginManager.js'; import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; import { AgentHostGitService } from './agentHostGitService.js'; import { IAgentHostGitService } from '../common/agentHostGitService.js'; -import { AgentHostCheckpointService } from './agentHostCheckpointService.js'; import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js'; import { createAgentHostTelemetryService } from './agentHostTelemetryService.js'; @@ -254,11 +253,9 @@ async function main(): Promise<void> { diServices.set(ISandboxHelperService, new SandboxHelperService()); const gitService = instantiationService.createInstance(AgentHostGitService); diServices.set(IAgentHostGitService, gitService); - const checkpointService = disposables.add(instantiationService.createInstance(AgentHostCheckpointService)); - diServices.set(IAgentHostCheckpointService, checkpointService); // Create the agent service (owns AgentHostStateManager + AgentSideEffects internally) - const agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, checkpointService, rootConfigResource, telemetryService, fileMonitorService, undefined, fetchFn, [createCodexProviderConfiguration(environmentService.userHome)]); + const agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, rootConfigResource, telemetryService, fileMonitorService, undefined, fetchFn, [createCodexProviderConfiguration(environmentService.userHome)]); disposables.add(agentService); diServices.set(IAgentService, agentService); diServices.set(IAgentHostStateManager, agentService.stateManager); @@ -283,6 +280,7 @@ async function main(): Promise<void> { diServices.set(IEditArcReporterService, editArcReporterService); diServices.set(IAgentHostGitHubEndpointService, agentService.gitHubEndpointService); diServices.set(IAgentHostCompletions, agentService.completionsService); + diServices.set(IAgentHostCheckpointService, agentService.checkpointService); diServices.set(IAgentHostGitService, gitService); // Register `ICopilotApiService` BEFORE `IClaudeProxyService` — // the proxy service constructor requires it. diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 4f2d2492a42..5ee95e9fd1e 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -53,7 +53,7 @@ import { type IChatContextSnapshot, type ISessionServerToolAccessor } from './sh import { WorktreeIsolation, WORKTREE_META_REPOSITORY_ROOT, worktreeProjectFromRepositoryRoot } from './shared/worktreeIsolation.js'; import { AgentHostChangesetService } from './agentHostChangesetService.js'; import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js'; -import { IAgentHostCheckpointService, NULL_CHECKPOINT_SERVICE } from '../common/agentHostCheckpointService.js'; +import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; import { IAgentHostReviewService } from '../common/agentHostReviewService.js'; import { AgentHostChangesetCoordinator } from './agentHostChangesetCoordinator.js'; import { AgentHostCompletions, IAgentHostCompletions } from './agentHostCompletions.js'; @@ -86,6 +86,7 @@ import { AgentHostDiscardChangesOperationContribution } from './agentHostDiscard import { AgentHostPullRequestOperationContribution } from './agentHostPullRequestOperationProvider.js'; import { AgentHostSyncOperationContribution } from './agentHostSyncOperationProvider.js'; import { AgentHostReviewService } from './agentHostReviewService.js'; +import { AgentHostCheckpointService } from './agentHostCheckpointService.js'; /** * Grace period before an empty, unsubscribed session is garbage-collected @@ -235,6 +236,9 @@ export class AgentService extends Disposable implements IAgentService { /** Exposes the GitHub endpoint service so agent providers share GitHub (Enterprise) resource resolution. */ get gitHubEndpointService(): IAgentHostGitHubEndpointService { return this._gitHubEndpointService; } + /** Exposes the checkpoint service so agent providers can capture session baselines. */ + get checkpointService(): IAgentHostCheckpointService { return this._checkpointService; } + /** Registered providers keyed by their {@link AgentProvider} id. */ private readonly _providers = new Map<AgentProvider, IAgent>(); /** Maps each active session URI (toString) to its owning provider. */ @@ -286,6 +290,8 @@ export class AgentService extends Disposable implements IAgentService { /** Server-side host for the agent host's server tools. */ private readonly _serverToolHost: AgentServerToolHost; private readonly _configurationService: AgentConfigurationService; + /** Captures baseline / per-turn git checkpoints backing the changeset pipeline. */ + private readonly _checkpointService: IAgentHostCheckpointService; /** * Host-owned worktree isolation controller. Set post-construction via * {@link setWorktreeIsolation} because it depends on the branch-name @@ -382,7 +388,6 @@ export class AgentService extends Disposable implements IAgentService { private readonly _sessionDataService: ISessionDataService, private readonly _productService: IProductService, private readonly _gitService: IAgentHostGitService, - private readonly _checkpointService: IAgentHostCheckpointService = NULL_CHECKPOINT_SERVICE, private readonly _rootConfigResource?: URI, private readonly _telemetryService: ITelemetryService = NullTelemetryService, _fileMonitorService?: IAgentHostFileMonitorService, @@ -449,10 +454,7 @@ export class AgentService extends Disposable implements IAgentService { this._gitStateService = this._register(instantiationService.createInstance(AgentHostGitStateService)); services.set(IAgentHostGitStateService, this._gitStateService); - // The checkpoint service is constructed in the outer agent-host - // DI scope and passed via {@link _checkpointService}; register it - // in the inner service collection so the changeset service / - // side effects can resolve it via DI. + this._checkpointService = this._register(instantiationService.createInstance(AgentHostCheckpointService)); services.set(IAgentHostCheckpointService, this._checkpointService); // The subscription service manages the lifecycle of changeset subscriptions. The service @@ -1950,12 +1952,27 @@ export class AgentService extends Disposable implements IAgentService { async disposeSession(session: URI): Promise<void> { this._logService.trace(`[AgentService] disposeSession: ${session.toString()}`); + // Resolve the working directories up front and pass them explicitly: + // the checkpoint and review services need them to locate the + // repositories holding this session's refs, and reading them from + // session state would silently break the moment `deleteSession` below + // is reordered ahead of the data deletion. + const workingDirectories = this._configurationService.getEffectiveWorkingDirectories(session.toString()); const provider = this._findProviderForSession(session); if (provider) { await this._disposeSession(provider, session); this._sessionToProvider.delete(session.toString()); this._clearDownloadProgressInterest(session.toString()); } + // Remove the VS Code per-session data directory (metadata DB + checkpoints) to mirror the SDK-side cleanup + // performed by the provider above. No-op when the directory does not exist. + // + // Runs before the worktree is removed: subscribers of the will-delete + // event drop this session's git refs, and for a worktree-isolated + // session the working directory *is* the worktree, so once it is gone + // the repository can no longer be resolved and the refs would leak + // into the main repository (`refs/agents/*` is shared, not per-worktree). + await this._sessionDataService.deleteSessionData(session, workingDirectories); // Remove any worktree this process created for the session (host-owned; // agents stay unaware). await this._worktree?.removeCreatedWorktree(AgentSession.id(session)); @@ -1967,9 +1984,6 @@ export class AgentService extends Disposable implements IAgentService { // Remove all subagent sessions for this parent this._sideEffects.removeSubagentSessions(session.toString()); this._stateManager.deleteSession(session.toString()); - // Remove the VS Code per-session data directory (metadata DB + checkpoints) to mirror the SDK-side cleanup - // performed by the provider above. No-op when the directory does not exist. - await this._sessionDataService.deleteSessionData(session); } // ---- Protocol methods --------------------------------------------------- diff --git a/src/vs/platform/agentHost/node/agentSideEffects.ts b/src/vs/platform/agentHost/node/agentSideEffects.ts index 8f4ccd682a6..d2e745f5afd 100644 --- a/src/vs/platform/agentHost/node/agentSideEffects.ts +++ b/src/vs/platform/agentHost/node/agentSideEffects.ts @@ -16,6 +16,7 @@ import { IInstantiationService } from '../../instantiation/common/instantiation. import { ILogService } from '../../log/common/log.js'; import { IAgentHostChangesetService } from '../common/agentHostChangesetService.js'; import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; +import { IAgentConfigurationService } from './agentConfigurationService.js'; import { AgentHostClientType } from '../common/agentHostClientInfo.js'; import { readAgentModelByokIdentifier } from '../common/agentModelByokMeta.js'; import { AgentSession, AgentSignal, IAgent, IAgentToolPendingConfirmationSignal } from '../common/agentService.js'; @@ -190,6 +191,7 @@ export class AgentSideEffects extends Disposable { @IAgentHostChangesetService private readonly _changesets: IAgentHostChangesetService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IAgentHostCheckpointService private readonly _checkpointService: IAgentHostCheckpointService, + @IAgentConfigurationService private readonly _agentConfigService: IAgentConfigurationService, ) { super(); this._telemetryReporter = new AgentHostTelemetryReporter(this._telemetryService); @@ -826,7 +828,15 @@ export class AgentSideEffects extends Disposable { // completion since those have always been fire-and-forget; the // ordering guarantee we care about is checkpoint-then-changeset. if (turnId !== undefined) { - this._checkpointService.captureTurnCheckpoint(URI.parse(sessionUri), turnId).then(() => { + // Resolved here rather than inside the checkpoint service so the + // repositories a checkpoint acts on are always explicit at the + // call site. Note the changeset service below deliberately keeps + // its own resolution: `onTurnComplete` only schedules deferred + // recomputes that are shared with subscription, truncation and + // mid-turn-debounce entry points, so it has no single point at + // which a caller-supplied set would apply. + const workingDirectories = this._agentConfigService.getEffectiveWorkingDirectories(sessionUri)?.map(w => URI.parse(w)); + this._checkpointService.captureTurnCheckpoint(URI.parse(sessionUri), turnId, workingDirectories).then(() => { this._changesets.onTurnComplete(sessionUri, turnId); }, err => { this._logService.warn(`[AgentSideEffects] Turn checkpoint capture failed for ${sessionUri}/${turnId}: ${err instanceof Error ? err.message : String(err)}`); diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 827e3b6bbef..7e92ba0a548 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -2097,8 +2097,13 @@ export class CopilotAgent extends Disposable implements IAgent { const project = await projectFromCopilotContext({ cwd: workingDirectory?.fsPath }, this._gitService); + // The resolved root set (index 0 = process root, e.g. a worktree). + // Shared by the persisted metadata, the baseline checkpoint and the + // materialize receipt so all three agree on the same directories. + const materializedWorkingDirectories = resolvedWorkingDirectories ?? (workingDirectory ? [workingDirectory] : undefined); + this._provisionalSessions.delete(sessionId); - await this._storeSessionMetadata(sessionUri, provisional.model, workingDirectory, resolvedWorkingDirectories ?? (workingDirectory ? [workingDirectory] : undefined), customizationDirectory, project, true); + await this._storeSessionMetadata(sessionUri, provisional.model, workingDirectory, materializedWorkingDirectories, customizationDirectory, project, true); if (agent !== undefined) { await this._storeSessionAgentMetadata(sessionUri, agent); } @@ -2109,14 +2114,18 @@ export class CopilotAgent extends Disposable implements IAgent { // invisible to the FileEditTracker pipeline. Best-effort: a // non-git folder or capture failure leaves the session running // with the legacy `file_edits`-based per-turn diff path. - this._checkpointService.captureBaseline(sessionUri, workingDirectory).catch(err => { + // + // The resolved directories are passed explicitly: the state manager + // does not learn about them until it observes the materialize event + // fired below, so a lookup here would still see the pre-worktree set. + this._checkpointService.captureBaselineCheckpoint(sessionUri, materializedWorkingDirectories).catch(err => { this._logService.warn(`[Copilot:${sessionId}] Baseline checkpoint capture failed: ${err instanceof Error ? err.message : String(err)}`); }); this._logService.info(`[Copilot] Session materialized: ${sessionUri.toString()}`); // Emit the resolved working-directory set (index 0 = process root). The host // replaces index 0 of the session set with it, preserving the tail. - this._onDidMaterializeSession.fire({ session: sessionUri, project, workingDirectories: resolvedWorkingDirectories ?? (workingDirectory ? [workingDirectory] : undefined) }); + this._onDidMaterializeSession.fire({ session: sessionUri, project, workingDirectories: materializedWorkingDirectories }); return agentSession; } diff --git a/src/vs/platform/agentHost/node/sessionDataService.ts b/src/vs/platform/agentHost/node/sessionDataService.ts index 6e96324a49a..e4ac6ec491b 100644 --- a/src/vs/platform/agentHost/node/sessionDataService.ts +++ b/src/vs/platform/agentHost/node/sessionDataService.ts @@ -110,7 +110,7 @@ export class SessionDataService implements ISessionDataService { return this._databases.acquire(key); } - async deleteSessionData(session: URI): Promise<void> { + async deleteSessionData(session: URI, workingDirectories?: readonly string[]): Promise<void> { const dir = this.getSessionDataDir(session); // Fire the will-delete event first so subscribers (notably the // checkpoint service) can perform async cleanup that needs the @@ -120,6 +120,7 @@ export class SessionDataService implements ISessionDataService { try { this._onWillDeleteSessionData.fire({ session, + workingDirectories, waitUntil: p => { pending.push(p); }, }); } catch (err) { diff --git a/src/vs/platform/agentHost/test/node/agentHostChangesetService.test.ts b/src/vs/platform/agentHost/test/node/agentHostChangesetService.test.ts index 7530e629396..a96ecb425c1 100644 --- a/src/vs/platform/agentHost/test/node/agentHostChangesetService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostChangesetService.test.ts @@ -24,7 +24,6 @@ import { AgentHostStateManager } from '../../node/agentHostStateManager.js'; import { AgentConfigurationService } from '../../node/agentConfigurationService.js'; import { SessionDatabase } from '../../node/sessionDatabase.js'; import { createNoopGitService, createNullSessionDataService, createSessionDataService, TestSessionDatabase } from '../common/sessionTestHelpers.js'; -import { META_CHECKPOINT_WORKING_DIR } from '../../node/agentHostCheckpointService.js'; /** * Builds a test subscription service backed by a mutable set of subscribed @@ -1095,9 +1094,6 @@ suite.skip('AgentHostChangesetService', () => { const sessionStr = sessionUri.toString(); setupSession('file:///wd'); - const db = new TestSessionDatabase(); - await db.setMetadata(META_CHECKPOINT_WORKING_DIR, 'file:///wd'); - const expectedDiffs = [ { after: { uri: 'file:///wd/a.ts', content: { uri: 'file:///wd/a.ts' } }, diff: { added: 4, removed: 1 } }, ]; @@ -1110,7 +1106,7 @@ suite.skip('AgentHostChangesetService', () => { const svc = disposables.add(new AgentHostChangesetService( stateManager, new NullLogService(), - createSessionDataService(db), + createSessionDataService(new TestSessionDatabase()), gitService, makeCheckpointService({ 'orig': { parent: 'ref-orig-parent', current: 'ref-orig' }, @@ -1199,15 +1195,12 @@ suite.skip('AgentHostChangesetService', () => { const sessionStr = sessionUri.toString(); setupSession('file:///wd'); - const db = new TestSessionDatabase(); - await db.setMetadata(META_CHECKPOINT_WORKING_DIR, 'file:///wd'); - const gitService = createNoopGitService(); gitService.computeFileDiffsBetweenRefs = async () => undefined; const svc = disposables.add(new AgentHostChangesetService( stateManager, new NullLogService(), - createSessionDataService(db), + createSessionDataService(new TestSessionDatabase()), gitService, makeCheckpointService({ 'orig': { parent: 'p', current: 'ref-orig' }, diff --git a/src/vs/platform/agentHost/test/node/agentHostReviewService.integrationTest.ts b/src/vs/platform/agentHost/test/node/agentHostReviewService.integrationTest.ts index 06d116f5db9..e67c7335492 100644 --- a/src/vs/platform/agentHost/test/node/agentHostReviewService.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/agentHostReviewService.integrationTest.ts @@ -164,7 +164,7 @@ suite.skip('AgentHostReviewService (real git)', () => { await svc!.markFileReviewed(sessionUri.toString(), wd(), undefined, URI.file(join(tmpRoot!, 'a.txt'))); const beforeDispose = chainLength(); - await svc!.disposeSessionData(sessionUri.toString()); + await svc!.disposeSessionData(sessionUri.toString(), [wd().toString()]); const afterDispose = chainLength(); assert.deepStrictEqual({ beforeDispose, afterDispose }, { beforeDispose: 1, afterDispose: 0 }); diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index 2cd41bc5e3d..44469852b41 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -36,7 +36,6 @@ import { MockAgent, ScriptedMockAgent } from './mockAgent.js'; import { mapSessionEventsToHistoryRecords } from './historyRecordFixtures.js'; import { type ISessionEvent } from './copilotTestEvents.js'; import { createNoopGitService, createSessionDataService, TestSessionDatabase } from '../common/sessionTestHelpers.js'; -import { NULL_CHECKPOINT_SERVICE } from '../../common/agentHostCheckpointService.js'; import { buildSessionChangesetUri, buildUncommittedChangesetUri } from '../../common/changesetUri.js'; import { type ICopilotApiService, type ICopilotApiServiceRequestOptions, type ICopilotUtilityChatCompletionRequest } from '../../node/shared/copilotApiService.js'; import { WorktreeIsolation } from '../../node/shared/worktreeIsolation.js'; @@ -631,7 +630,6 @@ suite('AgentService (node dispatcher)', () => { sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService(), - NULL_CHECKPOINT_SERVICE, undefined, undefined, undefined, @@ -658,7 +656,7 @@ suite('AgentService (node dispatcher)', () => { const localDisposables = new DisposableStore(); try { const rootConfigResource = joinPath(tempDir, 'agent-host-config.json'); - const svc = localDisposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService(), NULL_CHECKPOINT_SERVICE, rootConfigResource)); + const svc = localDisposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService(), rootConfigResource)); const agent = new MockAgent('copilot'); localDisposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -1201,6 +1199,28 @@ suite('AgentService (node dispatcher)', () => { // Should not throw await service.disposeSession(unknownSession); }); + + test('deletes session data before removing the worktree', async () => { + // Subscribers of the will-delete event drop this session's git refs, + // which requires resolving the repository from the working directory. + // For a worktree-isolated session that directory *is* the worktree, so + // removing it first would strand the refs in the main repository. + const order: string[] = []; + const sessionDataService: ISessionDataService = { + ...nullSessionDataService, + deleteSessionData: async () => { order.push('deleteSessionData'); }, + }; + const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + svc.registerProvider(copilotAgent); + const session = await svc.createSession({ provider: 'copilot' }); + svc.setWorktreeIsolation({ + removeCreatedWorktree: async () => { order.push('removeCreatedWorktree'); }, + } as unknown as WorktreeIsolation); + + await svc.disposeSession(session); + + assert.deepStrictEqual(order, ['deleteSessionData', 'removeCreatedWorktree']); + }); }); // ---- listSessions / listModels -------------------------------------- diff --git a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts index 13937f3897c..73eb9efc613 100644 --- a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts @@ -106,6 +106,7 @@ function createTestSideEffects( telemetryService: ITelemetryService = NullTelemetryService, changesets: IAgentHostChangesetService = new FakeChangesetService(), terminalManager: IAgentHostTerminalManager = disposables.add(new TestAgentHostTerminalManager()), + checkpointService: IAgentHostCheckpointService = NULL_CHECKPOINT_SERVICE, ): AgentSideEffects { const logService = new NullLogService(); const configService = disposables.add(new AgentConfigurationService(stateManager, logService)); @@ -113,7 +114,7 @@ function createTestSideEffects( [ILogService, logService], [IAgentConfigurationService, configService], [IAgentHostChangesetService, changesets], - [IAgentHostCheckpointService, NULL_CHECKPOINT_SERVICE], + [IAgentHostCheckpointService, checkpointService], [ITelemetryService, telemetryService], [IAgentHostTerminalManager, terminalManager], [ISessionDataService, options.sessionDataService], @@ -5642,6 +5643,35 @@ suite('AgentSideEffects', () => { assert.deepStrictEqual(changesets.turnCompletes, [{ session: sessionUri.toString(), turnId: 'turn-1' }]); }); + test('turn complete passes the resolved working directories to the checkpoint capture', async () => { + const workingDirectory = URI.file('/wd').toString(); + setupSession(workingDirectory); + startTurn('turn-1'); + + const captures: { turnId: string; workingDirectories: readonly string[] | undefined }[] = []; + const checkpoints: IAgentHostCheckpointService = { + ...NULL_CHECKPOINT_SERVICE, + captureTurnCheckpoint: async (_session, turnId, workingDirectories) => { + captures.push({ turnId, workingDirectories: workingDirectories?.map(w => w.toString()) }); + }, + }; + const localSideEffects = createTestSideEffects(disposables, stateManager, { + getAgent: () => agent, + agents: agentList, + sessionDataService: createNullSessionDataService(), + onTurnComplete: () => { }, + }, undefined, NullTelemetryService, new FakeChangesetService(), undefined, checkpoints); + disposables.add(localSideEffects.registerProgressListener(agent)); + + agent.fireProgress({ + kind: 'action', resource: URI.parse(defaultChatUri), + action: { type: ActionType.ChatTurnComplete, turnId: 'turn-1', duration: 1000 }, + }); + await Promise.resolve(); + + assert.deepStrictEqual(captures, [{ turnId: 'turn-1', workingDirectories: [workingDirectory] }]); + }); + test('ChatTruncated fires onSessionTruncated once', () => { setupSession(); From 72fcde3681507c6dfd51bf46efa216f09c4c6c4c Mon Sep 17 00:00:00 2001 From: BeniBenj <besimmonds@microsoft.com> Date: Fri, 31 Jul 2026 14:37:47 +0200 Subject: [PATCH 64/86] fix: update layout and styling for breadcrumbs and editor content in single-pane mode --- src/vs/sessions/LAYOUT.md | 2 +- .../sessions/browser/dockedAuxiliaryBarController.ts | 6 +----- .../workbench/browser/parts/editor/editorGroupView.ts | 11 ++++------- .../browser/parts/editor/editorTitleControl.ts | 10 +++++++--- .../componentFixtures/editor/editorTabBar.fixture.ts | 4 +++- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index 73941d8015a..aa2a8313cae 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -250,7 +250,7 @@ The main editor part can be explicitly revealed for workflows that target it dir The entire third-pane redesign is gated behind the experimental setting `sessions.layout.singlePaneDetailPanel`, read **once at startup** (a window reload applies a change). When the setting is **off** (default) the Agents window renders exactly as documented above (auxiliary bar as its own grid column with its composite tab strip + title, the standard multi-diff Changes editor). When **on**, the third pane becomes a **single pane with one full-width tab bar**: - The auxiliary bar is removed from the workbench grid and **docked inside the editor part** (absolutely positioned on the right, below the editor tab strip); the grid's top-right row becomes `Sessions | Editor`, and the editor part spans the editor + detail-panel width. -- The editor group's **title/tab strip spans the full width** while its content is inset on the right by the detail-panel width, via the concrete `EditorPart.setContentRightInset(px)` method (`EditorPart`/`EditorGroupView`; not on the `IEditorPart` interface; `0` = no-op for all other layouts). +- The editor group's **tab strip spans the full width** while its breadcrumbs and editor content are inset on the right by the detail-panel width, via the concrete `EditorPart.setContentRightInset(px)` method (`EditorPart`/`EditorGroupView`; not on the `IEditorPart` interface; `0` = no-op for all other layouts). The detail panel is always docked on the right, so no left margin is needed. - A **full-width header** sits below the tab bar, spanning the editor content and the docked detail panel, and hosts contributed actions. **The header menus are a group-level configuration; opting in is per-editor.** An editor part configures its groups with optional menu ids via `IEditorGroupViewOptions.menuIds` (`{ headerPrimary, headerSecondary, editorActions, tabsBarContext }`) — the core `EditorGroupView` never references any concrete menu point, it just renders whatever menu ids it was constructed with. `EditorPart.getGroupViewOptions()` is a protected hook (default `undefined`) that supplies these options to every group the part creates; `SinglePaneMainEditorPart` overrides it to return `Menus.SessionsEditorHeaderPrimary` / `Menus.SessionsEditorHeaderSecondary` / `Menus.SessionsEditorTitle` / `Menus.SessionsEditorTabsBarContext` (all defined in the sessions layer's shared menu registry, `browser/menus.ts`, not in core `platform/actions`). A header only renders while the **active editor opts in** via `IEditorPane.getHeaderActions()`, which returns just `{ instantiationService }` (the editor-scoped instantiation service so the header actions' `when` clauses evaluate in the editor's context) or `undefined` for no header; `EditorGroupView._renderEditorHeader` (run on every active-editor change) renders the group's configured menus as leading/trailing `MenuWorkbenchToolBar`s (`.editor-group-header-primary` / `.editor-group-header-secondary`, wrap-reversed so trailing actions float up) using that scoped service, hiding the whole header while both menus are empty. The header is a **real flow row inside the editor group** — `EditorGroupView` renders an optional `.editor-group-header` between its `.title` (tabs) and `.editor-container`, and **owns the header rendering and sizing**: the internal `setHeaderContent(render)` creates the inner content element, runs the render callback, and keeps the row **auto-sized to the content** via a `ResizeObserver` (wrapping and growing as needed, firing `onDidChangeHeaderHeight`); `headerHeight` exposes the reserved height. The group lays it out in flow (no absolute positioning) and shifts the editor pane down by its height. `SinglePaneMainEditorPart` renders no header DOM; it only offsets the docked auxiliary bar + sash down by `group.headerHeight` (`IDockedAuxiliaryBarHost.getHeaderHeight()`, re-applied on `onDidChangeHeaderHeight` via `_registerGroupHeader()`). The **Changes editor** (`SessionChangesEditor`) implements `getHeaderActions()` in single-pane (returning its scoped instantiation service), so the group renders `Menus.SessionsEditorHeaderPrimary` (to the left, `navigation` group: the *Branch Changes* dropdown, then the diff-stats action — the same clickable "+X -Y" pill (`VIEW_SESSION_CHANGES_COMMAND_ID`, rendered by `ChangesDiffStatsActionItem`) used by the classic Changes view header, always shown regardless of whether the editor area is visible or collapsed and opening/re-opening the Changes editor on click — then a separate `1_codeReview` group (separator before it) with *Run Code Review*, shown only when `SessionHasChangesContext` is true) and `Menus.SessionsEditorHeaderSecondary` (to the right, all inline unless overflowed: a `1_diff` group with collapse/expand + *Show Side by Side Diff* / *Show Inline Diff* (mutually exclusive by render mode); the sentinel `secondary` group — *View as List/Tree* — falls into the toolbar's overflow "…" menu) for the Changes tab only. The **Create Pull Request** button bar (`ChangesActionsBar`) is hosted in the editor tabs title: a header anchor action (`CHANGES_HEADER_ACTIONS_ID`, registered in `changesViewActions.ts`, contributed to `Menus.SessionsEditorTitle` group `navigation` order 5 and gated on the active Changes editor, top-right editor group, main window, dock-detail-panel setting, and `SessionHasChangesContext`) is rendered by the editor group's title actions. Its custom view item is supplied by `SessionChangesEditor.getActionViewItem()` as `ChangesActionsBarActionViewItem`, and the CSS makes the editor-actions side shrink to 50px before the tab scroller shrinks; split-button labels ellipsize while the dropdown segment stays visible. It hides entirely when its `AgentsChangesToolbar` menu has no actions. (In the classic non-single-pane layout the same `ChangesActionsBar` is still rendered inside `SessionChangesEditor`'s internal header.) The header-primary custom action view items (picker, diff-stats pill) are registered globally by `(menuId, actionId)` via `IActionViewItemService` in `ChangesEditorHeaderContribution` (`contrib/changes/browser/changesView.ts`), so the group's generic menu toolbars resolve them. The same *Branch Changes* picker and diff-stats actions are also contributed to the classic aux-bar Changes view menus (`ChatEditingSessionChangesFileHeaderToolbar` / `…RightToolbar`), which that view renders with its own action view items — so the two surfaces stay independent. - A vertical **sash** on the left edge of the docked panel resizes it (`DockedAuxiliaryBarController` in `browser/dockedAuxiliaryBarController.ts` owns `layout()` / `_ensureSash()`, created/driven by `SinglePaneMainEditorPart`). The preferred first-open width is 300px; explicit user resizes persist via the part-sizes snapshot. While the panel is visible it clamps to `[220px, editorWidth - 300px]`; dragging the raw sash width down to ~0 hides the docked detail panel, leaving the editor content visible. Temporary width growth from collapsing the sessions list is restored before persistence and must not become the user's detail width. - Collapsing the sessions list transfers the freed sidebar width to the editor grid node when the editor content is **visible**, and to the **detail panel** (`_dockedAuxiliaryBarWidth`, with the editor node kept equal to it) when the editor content is **hidden** (detail-only). Reopening the sessions list restores the pre-collapse editor-node width / detail width. Keeping the hidden-editor node equal to the detail width ensures the width-based reveal-sync never mistakes a wide detail-only node for a revealed editor. diff --git a/src/vs/sessions/browser/dockedAuxiliaryBarController.ts b/src/vs/sessions/browser/dockedAuxiliaryBarController.ts index a982515717a..0e6c06cf339 100644 --- a/src/vs/sessions/browser/dockedAuxiliaryBarController.ts +++ b/src/vs/sessions/browser/dockedAuxiliaryBarController.ts @@ -20,11 +20,7 @@ export interface IDockedAuxiliaryBarHost { isAuxiliaryBarVisible(): boolean; /** Hide the docked auxiliary bar via the workbench part-visibility API. */ hideAuxiliaryBar(): void; - /** - * Reserves an inset (px) on the right of the editor content while the editor - * tab bar keeps the full width, so the docked panel can sit beside it. `0` - * restores full-width content. - */ + /** Reserves space on the right of the breadcrumbs and editor pane while tabs remain full-width. */ setEditorContentRightInset(px: number): void; /** Extra top offset (px) below the tab bar, e.g. reserved by the full-width header. */ getHeaderHeight(): number; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 9be6fd142cf..8b43f64a4a1 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -2233,13 +2233,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.lastLayout = { width, height, top, left }; this.element.classList.toggle('max-height-478px', height <= 478); - // Layout the title control first to receive the size it occupies. The - // title always spans the full group width (so the tab strip and its - // toolbar can extend across any docked right inset). + // Keep tabs full-width while breadcrumbs follow the editor content inset. const titleControlSize = this.titleControl.layout({ container: new Dimension(width, height), available: new Dimension(width, height - this.editorPane.minimumHeight) - }); + }, this._contentRightInset); // Update progress bar location this.progressBar.getContainer().style.top = `${Math.max(this.titleHeight.offset - 2, 0)}px`; @@ -2260,9 +2258,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } /** - * Sets the right inset (px) reserved beside the editor pane while the title - * keeps the full group width, then relayouts. `0` restores the default - * full-width content. + * Sets the right inset reserved beside the breadcrumbs and editor pane while tabs remain full-width. + * `0` restores the default full-width content. */ setContentRightInset(inset: number): void { const next = Math.max(0, Math.round(inset)); diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index 1fb1c7bd326..23b0abf0320 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -39,6 +39,7 @@ export class EditorTitleControl extends Themable { private readonly editorTabsControlDisposable = this._register(new DisposableStore()); private breadcrumbsControlFactory: BreadcrumbsControlFactory | undefined; + private breadcrumbsContainer: HTMLElement | undefined; private readonly breadcrumbsControlDisposables = this._register(new DisposableStore()); private get breadcrumbsControl() { return this.breadcrumbsControlFactory?.control; } @@ -79,11 +80,12 @@ export class EditorTitleControl extends Themable { private createBreadcrumbsControl(): BreadcrumbsControlFactory | undefined { if (this.groupsView.partOptions.showTabs === 'single') { + this.breadcrumbsContainer = undefined; return undefined; // Single tabs have breadcrumbs inlined. No tabs have no breadcrumbs. } // Breadcrumbs container - const breadcrumbsContainer = $('.breadcrumbs-below-tabs'); + const breadcrumbsContainer = this.breadcrumbsContainer = $('.breadcrumbs-below-tabs'); this.parent.appendChild(breadcrumbsContainer); const breadcrumbsControlFactory = this.breadcrumbsControlDisposables.add(this.instantiationService.createInstance(BreadcrumbsControlFactory, breadcrumbsContainer, this.groupView, { @@ -200,7 +202,7 @@ export class EditorTitleControl extends Themable { } } - layout(dimensions: IEditorTitleControlDimensions): Dimension { + layout(dimensions: IEditorTitleControlDimensions, breadcrumbsRightInset = 0): Dimension { // Layout tabs control const tabsControlDimension = this.editorTabsControl.layout(dimensions); @@ -208,7 +210,9 @@ export class EditorTitleControl extends Themable { // Layout breadcrumbs if visible let breadcrumbsControlDimension: Dimension | undefined = undefined; if (this.breadcrumbsControl?.isHidden() === false) { - breadcrumbsControlDimension = new Dimension(dimensions.container.width, BreadcrumbsControl.HEIGHT); + const breadcrumbsWidth = Math.max(0, dimensions.container.width - breadcrumbsRightInset); + this.breadcrumbsContainer!.style.width = `${breadcrumbsWidth}px`; + breadcrumbsControlDimension = new Dimension(breadcrumbsWidth, BreadcrumbsControl.HEIGHT); this.breadcrumbsControl.layout(breadcrumbsControlDimension); } diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.ts index cb46bff2ef2..e358c8f11f4 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.ts @@ -297,6 +297,7 @@ interface IRenderOptions { readonly filePath?: 'on' | 'off' | 'last'; readonly icons?: boolean; }; + readonly breadcrumbsRightInset?: number; readonly width?: number; /** Whether this group is the active group. Inactive groups exercise the * `alwaysShowEditorActions` filtering and unfocused tab styling. */ @@ -470,7 +471,7 @@ function renderTabBar(ctx: ComponentFixtureContext, options: IRenderOptions): vo titleControl.layout({ container: new Dimension(width, titleControl.getHeight().total), available: new Dimension(width, 200), - }); + }, options.breadcrumbsRightInset); }; groupView.relayoutFn = layout; @@ -505,6 +506,7 @@ function createFixtures(modernUI: boolean, additionalThemes: readonly ComponentF // breadcrumbs BreadcrumbsFilePathLast: defineComponentFixture({ render: render(modernUI, { breadcrumbs: { filePath: 'last' }, editors: nestedActiveEditorSpecs() }) }), BreadcrumbsIconsOff: defineComponentFixture({ render: render(modernUI, { breadcrumbs: { icons: false } }) }), + BreadcrumbsWithRightInset: defineComponentFixture({ render: render(modernUI, { breadcrumbs: {}, breadcrumbsRightInset: 300 }) }), // tabSizing TabSizingShrink: defineComponentFixture({ render: render(modernUI, { partOptions: { tabSizing: 'shrink' }, editors: manyEditorSpecs() }) }), From 9bbf5e43c8c2872b4cff92678b75c79a01c09b1c Mon Sep 17 00:00:00 2001 From: Logan Ramos <lramos15@gmail.com> Date: Fri, 31 Jul 2026 08:37:52 -0400 Subject: [PATCH 65/86] Coalesce content exclusion fetches to stop exhausting the GitHub API rate limit (#328268) * Coalesce content exclusion fetches to stop exhausting the GitHub API rate limit Every caller that discovered a new repository triggered a refresh of the content exclusion rules for *every* known repository, so request volume grew quadratically with repository count. In a workspace with many git repos (an AOSP checkout, in the reported case) this produced ~16k requests to api.github.com in 30 minutes, exhausting the account's 5k/hour REST budget and starving everything sharing it, including Copilot token refresh. The endpoint is https://api.github.com/copilot_internal/content_exclusion, so it draws on the user's ordinary REST quota rather than a CAPI budget. - Coalesce per repository using shared DeferredPromises, a short batching window and a bounded-concurrency Limiter, so each repo is fetched at most once per TTL and each caller only waits on the repos it asked for. A regression test measures 75 requests -> 3 for 26 repositories. - Only cache rules on a successful response. Empty placeholder rules were written on discovery and left in place on failure, making a failed fetch indistinguishable from "this repo has no exclusions" and preventing a retry for 30 minutes, so exclusions silently stopped applying while rate limited. - Only memoise a negative verdict once the relevant rules actually loaded, which otherwise left files checked during an outage permanently allowed. - Add a shared rateLimitBackoffMiddleware covering 429 and quota-exhausted 403 responses, honouring Retry-After and x-ratelimit-reset, and move both RemoteContentExclusion and CloudSessionApiClient onto it. This replaces a third hand-rolled copy of the same backoff logic. - Precompile glob patterns, track the regex rule count directly, and only invalidate memoised results when rules that could change an outcome arrive. Fixes #322275 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32f94728-158b-4639-b789-a5dd483f043f * Address review feedback on cache invalidation and request lifecycle Five correctness issues raised in review, each with a test that fails against the previous implementation. - rateLimitBackoffMiddleware: a response that was already in flight could clear a block established by a concurrent rate-limited request, letting later calls reach the server during the window the server asked us to wait out. The backoff is now only reset once the active block has elapsed. - isIgnored returned a memoised verdict before reaching the staleness check, so a URI that had been evaluated once never triggered a refresh and could miss newly added exclusions indefinitely. Verdicts are now tagged with a rule generation and are only trusted while the rules behind them are unchanged and unexpired. - applyRules only invalidated verdicts when the incoming rules were non-empty, so a refresh that removed the last rule left files excluded permanently. Incoming rules are now compared against the previous set, which also avoids invalidating on an unchanged refresh. - drainPendingRepos cleared the pending map before its batches completed, so a lookup arriving while a request was slow queued a duplicate fetch every batching window. Entries now stay registered until their request settles, and are removed only if still owned by that attempt. - dispose only settled repos still queued. Limiter.dispose drops queued factories without running them, so with more than five batches the callers awaiting them never resolved. Pending entries are now settled before the limiter is disposed, and enqueues after disposal resolve immediately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32f94728-158b-4639-b789-a5dd483f043f --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32f94728-158b-4639-b789-a5dd483f043f --- .../chronicle/node/cloudSessionApiClient.ts | 327 ++++++------ .../node/test/cloudSessionApiClient.spec.ts | 70 ++- .../ignore/node/remoteContentExclusion.ts | 493 +++++++++++++----- .../ignore/node/test/mockCAPIClientService.ts | 101 +++- .../node/test/remoteContentExclusion.spec.ts | 265 +++++++++- .../middleware/rateLimitBackoffMiddleware.ts | 98 ++++ .../test/rateLimitBackoffMiddleware.spec.ts | 161 ++++++ 7 files changed, 1187 insertions(+), 328 deletions(-) create mode 100644 extensions/copilot/src/shared-fetch-utils/common/middleware/rateLimitBackoffMiddleware.ts create mode 100644 extensions/copilot/src/shared-fetch-utils/common/test/rateLimitBackoffMiddleware.spec.ts diff --git a/extensions/copilot/src/extension/chronicle/node/cloudSessionApiClient.ts b/extensions/copilot/src/extension/chronicle/node/cloudSessionApiClient.ts index f14ff1a7db5..928aca02c08 100644 --- a/extensions/copilot/src/extension/chronicle/node/cloudSessionApiClient.ts +++ b/extensions/copilot/src/extension/chronicle/node/cloudSessionApiClient.ts @@ -6,7 +6,9 @@ import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; import { ICopilotTokenManager } from '../../../platform/authentication/common/copilotTokenManager'; import { INTEGRATION_ID } from '../../../platform/endpoint/common/licenseAgreement'; -import { IFetcherService } from '../../../platform/networking/common/fetcherService'; +import { IFetcherService, type Response } from '../../../platform/networking/common/fetcherService'; +import { FetchBlockedError, type HttpFetchFn, type HttpResponse } from '../../../shared-fetch-utils/common/fetchTypes'; +import { rateLimitBackoffMiddleware } from '../../../shared-fetch-utils/common/middleware/rateLimitBackoffMiddleware'; import type { CreateSessionFailureReason, CreateSessionResult, CloudSession, SessionEvent, SubmitSessionEventsResult } from '../common/cloudSessionTypes'; /** Timeout for individual cloud API requests (ms). */ @@ -15,6 +17,31 @@ const REQUEST_TIMEOUT_MS = 10_000; /** Cloud sessions endpoint path. */ const SESSIONS_PATH = '/agents/sessions'; +/** Initial backoff applied when the server reports a rate limit without a hint. */ +const RATE_LIMIT_INITIAL_BACKOFF_MS = 60_000; + +/** Upper bound on any rate limit backoff, including one the server asks for. */ +const RATE_LIMIT_MAX_BACKOFF_MS = 600_000; + +/** What a cloud request produced, so each caller can map it onto its own result shape. */ +type CloudFetchOutcome = + | { readonly kind: 'response'; readonly response: Response } + | { readonly kind: 'rateLimited' } + | { readonly kind: 'error' }; + +/** Carries the underlying response through the middleware, which only reads status and headers. */ +type AdaptedResponse = HttpResponse & { readonly original: Response }; + +/** Options for a single cloud API call. */ +type CloudRequestInit = { + readonly method: string; + readonly json?: unknown; + /** Passed to the fetcher for request telemetry. */ + readonly callSite: string; + /** Reported to {@link CloudSessionApiClient.onRateLimited}. */ + readonly operation: string; +}; + // ── Cloud agent application IDs ───────────────────────────────────────────────── /** Agent application IDs used by the cloud sessions API (`agent_id` field). */ @@ -37,43 +64,84 @@ export const CloudAgentId = { */ export class CloudSessionApiClient { - /** Timestamp (epoch ms) until which all requests should be skipped due to 429. */ + /** Timestamp (epoch ms) until which all requests should be skipped due to a rate limit. */ private _rateLimitedUntil = 0; - /** Number of times we've been rate-limited. */ - private _rateLimitCount = 0; - - /** Callback fired when a 429 is received. */ + /** Callback fired when the server reports a new rate limit. */ onRateLimited: ((callSite: string, retryAfterSec: number) => void) | undefined; + /** + * Shared rate limit handling. Only this middleware is applied: `403` here means policy + * blocked rather than an auth failure, and `5xx` backoff is owned by the exporter's circuit + * breaker, so neither the auth nor the server error middleware belongs in this stack. + */ + private readonly _rateLimitedFetch: HttpFetchFn; + constructor( private readonly _tokenManager: ICopilotTokenManager, private readonly _authService: IAuthenticationService, private readonly _fetcherService: IFetcherService, - ) { } + // Injectable so tests can exercise the backoff without waiting on the wall clock. + private readonly _now: () => number = Date.now, + ) { + this._rateLimitedFetch = rateLimitBackoffMiddleware({ + initialDelayMs: RATE_LIMIT_INITIAL_BACKOFF_MS, + maxDelayMs: RATE_LIMIT_MAX_BACKOFF_MS, + now: this._now, + })(async (request) => { + const { method, json, callSite } = request.state as CloudRequestInit; + const original = await this._fetcherService.fetch(request.url, { + callSite, + // FetchOptions.method is typed narrowly (GET/POST/PUT) for CAPI + // compatibility; the underlying fetcher accepts DELETE at runtime. + method: method as 'POST', + headers: request.headers, + json, + timeout: REQUEST_TIMEOUT_MS, + }); + return { + status: original.status, + headers: original.headers, + body: null, + text: () => original.text(), + json: () => original.json(), + original, + } satisfies AdaptedResponse; + }); + } /** Returns true if we're currently rate-limited and should skip requests. */ isRateLimited(): boolean { - return Date.now() < this._rateLimitedUntil; + return this._now() < this._rateLimitedUntil; } - /** Record a 429 response and back off for the indicated duration. */ - private _handleRateLimit(res: { headers?: { get?(name: string): string | null } }, callSite: string): void { - let retryAfterSec = 60; // Default: 60 seconds + /** + * Performs a cloud API request, short-circuiting while rate limited. + * + * The middleware decides how long to wait; this only mirrors that window so + * {@link isRateLimited} can be polled synchronously by the exporter. + */ + private async _fetch(path: string, init: CloudRequestInit): Promise<CloudFetchOutcome> { + // Checked before building the request so a blocked call costs no token lookup, and so the + // telemetry callback only fires for newly reported limits. + if (this.isRateLimited()) { + return { kind: 'rateLimited' }; + } + const { url, headers } = await this._buildRequest(path); + if (!url) { + return { kind: 'error' }; + } try { - const header = res.headers?.get?.('Retry-After'); - if (header) { - const parsed = parseInt(header, 10); - if (!isNaN(parsed) && parsed > 0 && parsed <= 600) { - retryAfterSec = parsed; - } + const response = await this._rateLimitedFetch({ url, headers, state: init }); + return { kind: 'response', response: (response as AdaptedResponse).original }; + } catch (err) { + if (err instanceof FetchBlockedError) { + this._rateLimitedUntil = Math.max(this._rateLimitedUntil, this._now() + err.retryAfterMs); + this.onRateLimited?.(init.operation, Math.round(err.retryAfterMs / 1000)); + return { kind: 'rateLimited' }; } - } catch { - // Use default + return { kind: 'error' }; } - this._rateLimitedUntil = Date.now() + retryAfterSec * 1000; - this._rateLimitCount++; - this.onRateLimited?.(callSite, retryAfterSec); } /** @@ -87,43 +155,31 @@ export class CloudSessionApiClient { sessionId: string, indexingLevel: 'user' | 'repo_and_user' = 'user', ): Promise<CreateSessionResult> { - if (this.isRateLimited()) { - return { ok: false, reason: 'rate_limited' }; - } - try { - const { url, headers } = await this._buildRequest(SESSIONS_PATH); - if (!url) { - return { ok: false, reason: 'error' }; - } - - const body = { + const outcome = await this._fetch(SESSIONS_PATH, { + method: 'POST', + callSite: 'chronicle.cloudCreateSession', + operation: 'createSession', + json: { owner_id: ownerId, repo_id: repoId, agent_task_id: sessionId, indexing_level: indexingLevel, - }; + }, + }); + if (outcome.kind !== 'response') { + return { ok: false, reason: outcome.kind === 'rateLimited' ? 'rate_limited' : 'error' }; + } - const res = await this._fetcherService.fetch(url, { - callSite: 'chronicle.cloudCreateSession', - method: 'POST', - headers, - json: body, - timeout: REQUEST_TIMEOUT_MS, - }); - - if (res.status === 429) { - this._handleRateLimit(res, 'createSession'); - return { ok: false, reason: 'rate_limited' }; - } - - if (!res.ok) { - const reason: CreateSessionFailureReason = res.status === 403 ? 'policy_blocked' : 'error'; - return { ok: false, reason }; - } + const res = outcome.response; + if (!res.ok) { + const reason: CreateSessionFailureReason = res.status === 403 ? 'policy_blocked' : 'error'; + return { ok: false, reason }; + } + try { const response = await res.json() as { id: string; task_id?: string; agent_task_id?: string }; return { ok: true, response }; - } catch (err) { + } catch { return { ok: false, reason: 'error' }; } } @@ -137,69 +193,40 @@ export class CloudSessionApiClient { sessionId: string, events: SessionEvent[], ): Promise<SubmitSessionEventsResult> { - if (this.isRateLimited()) { - return { ok: false, reason: 'rate_limited' }; + const outcome = await this._fetch(`${SESSIONS_PATH}/${sessionId}/events`, { + method: 'POST', + callSite: 'chronicle.cloudSubmitEvents', + operation: 'submitEvents', + json: { events }, + }); + if (outcome.kind !== 'response') { + return { ok: false, reason: outcome.kind === 'rateLimited' ? 'rate_limited' : 'error' }; } - try { - const { url, headers } = await this._buildRequest(`${SESSIONS_PATH}/${sessionId}/events`); - if (!url) { - return { ok: false, reason: 'error' }; - } - const res = await this._fetcherService.fetch(url, { - callSite: 'chronicle.cloudSubmitEvents', - method: 'POST', - headers, - json: { events }, - timeout: REQUEST_TIMEOUT_MS, - }); - - if (res.status === 429) { - this._handleRateLimit(res, 'submitEvents'); - return { ok: false, reason: 'rate_limited' }; - } - - if (!res.ok) { - const reason: 'policy_blocked' | 'error' = res.status === 403 ? 'policy_blocked' : 'error'; - return { ok: false, reason }; - } - - return { ok: true }; - } catch (err) { - return { ok: false, reason: 'error' }; + const res = outcome.response; + if (!res.ok) { + const reason: 'policy_blocked' | 'error' = res.status === 403 ? 'policy_blocked' : 'error'; + return { ok: false, reason }; } + + return { ok: true }; } /** * Get a session by ID (used for reattach verification). */ async getSession(sessionId: string): Promise<CloudSession | undefined> { - if (this.isRateLimited()) { + const outcome = await this._fetch(`${SESSIONS_PATH}/${sessionId}`, { + method: 'GET', + callSite: 'chronicle.cloudGetSession', + operation: 'getSession', + }); + if (outcome.kind !== 'response' || !outcome.response.ok) { return undefined; } + try { - const { url, headers } = await this._buildRequest(`${SESSIONS_PATH}/${sessionId}`); - if (!url) { - return undefined; - } - - const res = await this._fetcherService.fetch(url, { - callSite: 'chronicle.cloudGetSession', - method: 'GET', - headers, - timeout: REQUEST_TIMEOUT_MS, - }); - - if (res.status === 429) { - this._handleRateLimit(res, 'getSession'); - return undefined; - } - - if (!res.ok) { - return undefined; - } - - return (await res.json()) as CloudSession; + return (await outcome.response.json()) as CloudSession; } catch { return undefined; } @@ -211,36 +238,21 @@ export class CloudSessionApiClient { */ async listSessions(): Promise<Array<{ id: string; task_id?: string; agent_task_id?: string; agent_id?: number; state: string; created_at: string }>> { const allSessions: Array<{ id: string; task_id?: string; agent_task_id?: string; agent_id?: number; state: string; created_at: string }> = []; - if (this.isRateLimited()) { - return allSessions; - } const pageSize = 100; let page = 1; try { while (true) { - const { url, headers } = await this._buildRequest(`${SESSIONS_PATH}?page_size=${pageSize}&page_number=${page}`); - if (!url) { - return allSessions; - } - - const res = await this._fetcherService.fetch(url, { - callSite: 'chronicle.cloudListSessions', + const outcome = await this._fetch(`${SESSIONS_PATH}?page_size=${pageSize}&page_number=${page}`, { method: 'GET', - headers, - timeout: REQUEST_TIMEOUT_MS, + callSite: 'chronicle.cloudListSessions', + operation: 'listSessions', }); - - if (res.status === 429) { - this._handleRateLimit(res, 'listSessions'); + if (outcome.kind !== 'response' || !outcome.response.ok) { return allSessions; } - if (!res.ok) { - return allSessions; - } - - const data = await res.json(); + const data = await outcome.response.json(); const sessions = Array.isArray(data) ? data : (data as Record<string, unknown>).sessions; const pageSessions = Array.isArray(sessions) ? sessions : []; @@ -272,38 +284,20 @@ export class CloudSessionApiClient { * treated as success), or 'error' on failure. */ async deleteSession(taskId: string): Promise<'deleted' | 'not_found' | 'error'> { - if (this.isRateLimited()) { + const outcome = await this._fetch(`/agents/tasks/${encodeURIComponent(taskId)}`, { + method: 'DELETE', + callSite: 'chronicle.cloudDeleteSession', + operation: 'deleteSession', + }); + if (outcome.kind !== 'response') { return 'error'; } - try { - const { url, headers } = await this._buildRequest(`/agents/tasks/${encodeURIComponent(taskId)}`); - if (!url) { - return 'error'; - } - const res = await this._fetcherService.fetch(url, { - callSite: 'chronicle.cloudDeleteSession', - // FetchOptions.method is typed narrowly (GET/POST/PUT) for CAPI - // compatibility; the underlying fetcher accepts DELETE at runtime. - method: 'DELETE' as 'POST', - headers, - timeout: REQUEST_TIMEOUT_MS, - }); - - if (res.status === 429) { - this._handleRateLimit(res, 'deleteSession'); - return 'error'; - } - if (res.status === 404) { - return 'not_found'; - } - if (res.ok) { - return 'deleted'; - } - return 'error'; - } catch (err) { - return 'error'; + const res = outcome.response; + if (res.status === 404) { + return 'not_found'; } + return res.ok ? 'deleted' : 'error'; } /** @@ -311,33 +305,18 @@ export class CloudSessionApiClient { * Single API call that queues all eligible sessions for reindexing. */ async backfillAnalytics(indexingLevel: 'user' | 'repo_and_user'): Promise<{ ok: true; sessionsQueued: number } | { ok: false }> { - if (this.isRateLimited()) { + const outcome = await this._fetch('/agents/analytics/backfill', { + method: 'POST', + callSite: 'chronicle.cloudBackfillAnalytics', + operation: 'backfillAnalytics', + json: { indexing_level: indexingLevel }, + }); + if (outcome.kind !== 'response' || !outcome.response.ok) { return { ok: false }; } + try { - const { url, headers } = await this._buildRequest('/agents/analytics/backfill'); - if (!url) { - return { ok: false }; - } - - const res = await this._fetcherService.fetch(url, { - callSite: 'chronicle.cloudBackfillAnalytics', - method: 'POST', - headers, - json: { indexing_level: indexingLevel }, - timeout: REQUEST_TIMEOUT_MS, - }); - - if (res.status === 429) { - this._handleRateLimit(res, 'backfillAnalytics'); - return { ok: false }; - } - - if (!res.ok) { - return { ok: false }; - } - - const data = await res.json() as { sessions_queued?: number }; + const data = await outcome.response.json() as { sessions_queued?: number }; return { ok: true, sessionsQueued: data.sessions_queued ?? 0 }; } catch { return { ok: false }; diff --git a/extensions/copilot/src/extension/chronicle/node/test/cloudSessionApiClient.spec.ts b/extensions/copilot/src/extension/chronicle/node/test/cloudSessionApiClient.spec.ts index fd731efd17c..98f9da3ebf0 100644 --- a/extensions/copilot/src/extension/chronicle/node/test/cloudSessionApiClient.spec.ts +++ b/extensions/copilot/src/extension/chronicle/node/test/cloudSessionApiClient.spec.ts @@ -31,11 +31,12 @@ function createMockServices() { return { tokenManager, authService, fetcherService }; } -function makeFetchResponse(status: number, body: unknown = {}): { ok: boolean; status: number; headers: { get: (n: string) => string | null }; json: () => Promise<unknown> } { +function makeFetchResponse(status: number, body: unknown = {}, headers: Record<string, string> = {}): { ok: boolean; status: number; headers: { get: (n: string) => string | null }; json: () => Promise<unknown> } { + const lowerCased = new Map(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])); return { ok: status >= 200 && status < 300, status, - headers: { get: () => null }, + headers: { get: (name: string) => lowerCased.get(name.toLowerCase()) ?? null }, json: async () => body, }; } @@ -124,4 +125,69 @@ describe('CloudSessionApiClient', () => { expect(result).toEqual({ ok: false, reason: 'rate_limited' }); }); }); + + describe('rate limiting', () => { + it('skips requests while rate limited and resumes once the window passes', async () => { + const { tokenManager, authService, fetcherService } = createMockServices(); + let now = Date.UTC(2026, 0, 1); + const fetch = fetcherService.fetch as any; + fetch.mockResolvedValue(makeFetchResponse(429, {}, { 'retry-after': '120' })); + + const client = new CloudSessionApiClient(tokenManager, authService, fetcherService, () => now); + + const first = await client.submitSessionEvents('sess-1', []); + const callsAfterLimit = fetch.mock.calls.length; + + // Inside the window the call short-circuits without touching the network. + now += 60_000; + const during = await client.submitSessionEvents('sess-1', []); + const callsDuringWindow = fetch.mock.calls.length; + + // Past the window the client tries again and recovers. + now += 61_000; + fetch.mockResolvedValue(makeFetchResponse(200)); + const after = await client.submitSessionEvents('sess-1', []); + + expect({ first, during, after, callsAfterLimit, callsDuringWindow, callsTotal: fetch.mock.calls.length, limitedNow: client.isRateLimited() }).toEqual({ + first: { ok: false, reason: 'rate_limited' }, + during: { ok: false, reason: 'rate_limited' }, + after: { ok: true }, + callsAfterLimit: 1, + callsDuringWindow: 1, + callsTotal: 2, + limitedNow: false, + }); + }); + + it('reports each new limit once through onRateLimited', async () => { + const { tokenManager, authService, fetcherService } = createMockServices(); + let now = Date.UTC(2026, 0, 1); + (fetcherService.fetch as any).mockResolvedValue(makeFetchResponse(429, {}, { 'retry-after': '90' })); + + const client = new CloudSessionApiClient(tokenManager, authService, fetcherService, () => now); + const reported: Array<{ callSite: string; retryAfterSec: number }> = []; + client.onRateLimited = (callSite, retryAfterSec) => reported.push({ callSite, retryAfterSec }); + + await client.createSession(1, 2, 'local-1'); + // A follow-up blocked by the same window must not report again. + now += 30_000; + await client.createSession(1, 2, 'local-2'); + + expect(reported).toEqual([{ callSite: 'createSession', retryAfterSec: 90 }]); + }); + + it('clamps an implausible retry-after to the maximum backoff', async () => { + const { tokenManager, authService, fetcherService } = createMockServices(); + const now = Date.UTC(2026, 0, 1); + (fetcherService.fetch as any).mockResolvedValue(makeFetchResponse(429, {}, { 'retry-after': '86400' })); + + const client = new CloudSessionApiClient(tokenManager, authService, fetcherService, () => now); + const reported: number[] = []; + client.onRateLimited = (_callSite, retryAfterSec) => reported.push(retryAfterSec); + + await client.getSession('sess-1'); + + expect(reported).toEqual([600]); + }); + }); }); diff --git a/extensions/copilot/src/platform/ignore/node/remoteContentExclusion.ts b/extensions/copilot/src/platform/ignore/node/remoteContentExclusion.ts index 7d51430ecbc..93a210110ab 100644 --- a/extensions/copilot/src/platform/ignore/node/remoteContentExclusion.ts +++ b/extensions/copilot/src/platform/ignore/node/remoteContentExclusion.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { RequestType } from '@vscode/copilot-api'; -import { minimatch } from 'minimatch'; +import { Minimatch } from 'minimatch'; import { createSha256Hash } from '../../../util/common/crypto'; import { coalesce } from '../../../util/vs/base/common/arrays'; -import { Limiter, raceCancellationError } from '../../../util/vs/base/common/async'; +import { DeferredPromise, Limiter, raceCancellationError, timeout } from '../../../util/vs/base/common/async'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { IDisposable } from '../../../util/vs/base/common/lifecycle'; import { ResourceMap } from '../../../util/vs/base/common/map'; @@ -18,9 +18,13 @@ import { IFileSystemService } from '../../filesystem/common/fileSystemService'; import { readFileFromTextBufferOrFS } from '../../filesystem/node/fileSystemServiceImpl'; import { IGitService, RepoContext, normalizeFetchUrl } from '../../git/common/gitService'; import { ILogService } from '../../log/common/logService'; -import { Response } from '../../networking/common/fetcherService'; import { IRequestLogger } from '../../requestLogger/common/requestLogger'; import { IWorkspaceService } from '../../workspace/common/workspaceService'; +import { composeFetchMiddleware } from '../../../shared-fetch-utils/common/advancedFetcher'; +import { FetchBlockedError, type HttpFetchFn, type HttpResponse } from '../../../shared-fetch-utils/common/fetchTypes'; +import { authBlockedMiddleware } from '../../../shared-fetch-utils/common/middleware/authBlockedMiddleware'; +import { rateLimitBackoffMiddleware } from '../../../shared-fetch-utils/common/middleware/rateLimitBackoffMiddleware'; +import { serverErrorBackoffMiddleware } from '../../../shared-fetch-utils/common/middleware/serverErrorBackoffMiddleware'; type ContentExclusionRule = { paths: string[]; @@ -36,20 +40,71 @@ type ContentExclusionResponse = { type RepoMetadata = { repoRootPath: string; fetchUrls: string[] }; -const NON_GIT_FILE_KEY = 'non-git-file'; +/** Rules for a single repo, along with when they were fetched so they can expire individually. */ +type CachedRules = { + patterns: string[]; + ifAnyMatch: RegExp[]; + ifNoneMatch: RegExp[]; + fetchedAt: number; +}; /** - * Fetches content exclusion policies from GH remotes + * A memoised {@link RemoteContentExclusion.isIgnored} result, tagged with the rule generation it + * was computed against so it can be discarded when the rules behind it change. + */ +type CachedVerdict = { verdict: boolean; generation: number }; + +/** A repo awaiting rules, shared by every caller that asks for it while the fetch is outstanding. */ +type PendingFetch = { readonly deferred: DeferredPromise<void>; dispatched: boolean }; + +const NON_GIT_FILE_KEY = 'non-git-file'; + +const MINIMATCH_OPTIONS = { + nocase: true, + matchBase: true, + nonegate: true, + dot: true +}; + +/** Max repos the content exclusion endpoint accepts in a single request. */ +const REPOS_PER_REQUEST = 10; +/** How many batches may be in flight at once. */ +const MAX_CONCURRENT_BATCHES = 5; +/** Window used to collect repos before dispatching, so bursts collapse into full batches. */ +const BATCH_WINDOW_MS = 50; +/** How long fetched rules stay valid before they are refreshed on next use. */ +const RULE_TTL_MS = 30 * 60 * 1000; + +/** + * Fetches content exclusion policies from GH remotes. + * + * The rules endpoint lives on api.github.com, so it shares the caller's regular REST rate limit + * budget. Requests are therefore coalesced per repo, batched, and backed off on failure. */ export class RemoteContentExclusion implements IDisposable { - // The cache which maps remote fetch url to the minimatch patterns, order of patterns matters here - private _contentExclusionCache: Map<string, { patterns: string[]; ifAnyMatch: RegExp[]; ifNoneMatch: RegExp[] }> = new Map(); - private _contentExclusionFetchPromise: Promise<void> | null = null; + // Rules keyed by remote fetch url. Only ever holds successfully fetched rules, so a failed + // request can never be mistaken for "this repo has no exclusions". + private readonly _contentExclusionCache: Map<string, CachedRules> = new Map(); + // Repos waiting to be fetched, along with the promise every caller for that repo shares. Entries + // stay registered until their request settles, so a lookup arriving mid-flight joins it. + private readonly _pendingRepos: Map<string, PendingFetch> = new Map(); + private readonly _batchLimiter: Limiter<void>; + private _scheduledDrain: Promise<void> | undefined; + private _disposed = false; + // Flattened, precompiled view of every glob rule so isIgnored does not recompile per call. + private _compiledGlobs: Minimatch[] = []; + private _regexRuleCount = 0; + // Bumped whenever the rules change, which retires every verdict memoised against them. + private _rulesGeneration = 0; + // When the soonest expiring rule set goes stale. Memoised verdicts are only trusted before this. + private _earliestRuleExpiry = 0; // This caches the ignore results as they can be expensive to compute and a single render can request results 100s of times - private _ignoreGlobResultCache: ResourceMap<boolean> = new ResourceMap(); + private _ignoreGlobResultCache: ResourceMap<CachedVerdict> = new ResourceMap(); // Map of the hash of file contents to the result of the regex check - private _ignoreRegexResultCache: Map<string, boolean> = new Map(); - private _lastRuleFetch = 0; + private _ignoreRegexResultCache: Map<string, CachedVerdict> = new Map(); + // Requests go through the shared middleware stack so rate limit and server error backoff are + // handled the same way as every other cached CAPI-client value. + private readonly _fetchExclusionRules: HttpFetchFn; private _disposables: IDisposable[] = []; private readonly _fileReadLimiter: Limiter<string | Uint8Array>; // Cache of repository root paths to their metadata to avoid calling getRepositoryFetchUrls for every file @@ -63,11 +118,10 @@ export class RemoteContentExclusion implements IDisposable { private readonly _capiClientService: ICAPIClientService, private readonly _fileSystemService: IFileSystemService, private readonly _workspaceService: IWorkspaceService, - private readonly _requestLogger: IRequestLogger + private readonly _requestLogger: IRequestLogger, + // Injectable so tests can exercise rule expiry and backoff without waiting on the wall clock. + private readonly _now: () => number = Date.now ) { - // This is a specialized entry to store the global rules that apply to files outside of any git repository - // The other option was to maintain a separate cache for non git files but that would be redundant - this._contentExclusionCache.set(NON_GIT_FILE_KEY, { patterns: [], ifAnyMatch: [], ifNoneMatch: [] }); this._disposables.push(this._gitService.onDidCloseRepository((r) => { const repoInfo = this.getRepositoryInfo(r); if (!repoInfo) { @@ -78,22 +132,33 @@ export class RemoteContentExclusion implements IDisposable { for (const url of repoInfo.fetchUrls) { this._contentExclusionCache.delete(url); } + this.rebuildCompiledRules(); + // Dropping a repo's rules can flip verdicts that were memoised while they applied. + this.invalidateVerdicts(); })); this._fileReadLimiter = new Limiter<string | Uint8Array>(10); this._disposables.push(this._fileReadLimiter); + this._batchLimiter = new Limiter<void>(MAX_CONCURRENT_BATCHES); + this._disposables.push(this._batchLimiter); + + this._fetchExclusionRules = composeFetchMiddleware( + // Order matters: the rate limit check sits inside the auth check so that a quota + // exhausted 403 is recognised by its headers and waits for the reset, instead of being + // misread as an auth failure and blocking the token for an hour. + authBlockedMiddleware(), + rateLimitBackoffMiddleware({ now: this._now }), + serverErrorBackoffMiddleware(), + )(request => this._capiClientService.makeRequest<HttpResponse>( + { headers: request.headers }, + { type: RequestType.ContentExclusion, repos: (request.state?.repos ?? []) as string[] } + )); } public async isIgnored(file: URI, token: CancellationToken = CancellationToken.None): Promise<boolean> { - // 1. If glob is not ignored, but there is no regex we can return false as the URI will not change - // 2. If glob is not ignored, but there are regex we need to read file content which will happen lower in the regex code. - // 3. If glob is ignored, it will return true despite regex since the most restrictive exclusion takes the cake - if ((this._ignoreGlobResultCache.has(file) && !this.isRegexContextExclusionsEnabled) || this._ignoreGlobResultCache.get(file)) { - return this._ignoreGlobResultCache.get(file) ?? false; - } - // Any pending requests that may be in flight should be awaited before returning a result - if (this._contentExclusionFetchPromise) { - await raceCancellationError(this._contentExclusionFetchPromise, token); + const memoised = this.memoisedVerdict(file); + if (memoised !== undefined) { + return memoised; } // Try to find the repository from the cache first to avoid expensive git extension calls @@ -118,29 +183,18 @@ export class RemoteContentExclusion implements IDisposable { const fileName = file.path.toLowerCase().replace(repoMetadata.repoRootPath.toLowerCase(), ''); - // We're missing entries for this repository in the cache, so we fetch it. - // Or it has been more than 30 minutes so the current rules are stale - if (this.shouldFetchContentExclusionRules(repoMetadata) || (Date.now() - this._lastRuleFetch > 30 * 60 * 1000)) { - this._logService.trace(`Fetching content exclusions, due to ${this.shouldFetchContentExclusionRules(repoMetadata) ? 'repository change' : 'stale cache'}.`); - this._lastRuleFetch = Date.now(); - await raceCancellationError(this.makeContentExclusionRequest(), token); - } + // Only waits on the repos this file actually belongs to, so an unrelated in-flight batch + // cannot block this lookup. + const rulesLoaded = await raceCancellationError(this.ensureRulesLoaded(repoMetadata.fetchUrls), token); + // Captured up front so that a refresh landing while this verdict is being computed retires + // it, rather than it being stored as if it reflected the newer rules. + const generation = this._rulesGeneration; - const minimatchConfig = { - nocase: true, - matchBase: true, - nonegate: true, - dot: true - }; - - for (const { patterns } of this._contentExclusionCache.values()) { - for (const rule of patterns) { - const matchesPattern = minimatch(fileName, rule, minimatchConfig) || minimatch(file.path, rule, minimatchConfig); - if (matchesPattern) { - this._logService.debug(`File ${file.path} is ignored by content exclusion rule ${rule}`); - this._ignoreGlobResultCache.set(file, true); - return true; - } + for (const glob of this._compiledGlobs) { + if (glob.match(fileName) || glob.match(file.path)) { + this._logService.debug(`File ${file.path} is ignored by content exclusion rule ${glob.pattern}`); + this._ignoreGlobResultCache.set(file, { verdict: true, generation }); + return true; } } let fileContents: string = ''; @@ -157,8 +211,9 @@ export class RemoteContentExclusion implements IDisposable { fileContents = typeof fileContentOrBuffer === 'string' ? fileContentOrBuffer : new TextDecoder().decode(fileContentOrBuffer); fileContentHash = await createSha256Hash(fileContents); // Cache hit for these file contents, no need to run the regex patterns - if (this._ignoreRegexResultCache.has(fileContentHash)) { - return this._ignoreRegexResultCache.get(fileContentHash) ?? false; + const cachedRegexVerdict = this._ignoreRegexResultCache.get(fileContentHash); + if (cachedRegexVerdict && cachedRegexVerdict.generation === generation) { + return cachedRegexVerdict.verdict; } } catch { // We failed to read the file, so it should just be ignored as we have no idea what the contents are or if it exists @@ -168,151 +223,301 @@ export class RemoteContentExclusion implements IDisposable { } if (ifAnyMatch.length > 0 && fileContents && ifAnyMatch.some(pattern => pattern.test(fileContents))) { this._logService.debug(`File ${file.path} is ignored by content exclusion rule ifAnyMatch`); - this._ignoreRegexResultCache.set(fileContentHash, true); + this._ignoreRegexResultCache.set(fileContentHash, { verdict: true, generation }); return true; } if (ifNoneMatch.length > 0 && fileContents && !ifNoneMatch.some(pattern => pattern.test(fileContents))) { this._logService.debug(`File ${file.path} is ignored by content exclusion rule ifNoneMatch`); - this._ignoreRegexResultCache.set(fileContentHash, true); + this._ignoreRegexResultCache.set(fileContentHash, { verdict: true, generation }); return true; } } - this._ignoreGlobResultCache.set(file, false); - this._ignoreRegexResultCache.set(fileContentHash, false); + // Only memoise a negative verdict once every relevant rule set has actually loaded. Caching it + // after a failed fetch would leave the file permanently allowed. + if (rulesLoaded) { + this._ignoreGlobResultCache.set(file, { verdict: false, generation }); + // Only meaningful when regex rules forced us to read (and hash) the file. + if (fileContentHash) { + this._ignoreRegexResultCache.set(fileContentHash, { verdict: false, generation }); + } + } return false; } + /** + * Returns a memoised verdict when it can still be trusted. + * + * A verdict is only reusable while the rules behind it are both unchanged and unexpired, + * otherwise the file has to be re-evaluated so that policy changes are picked up. Skipping the + * expiry check here would pin a file to its first answer forever, since a cached verdict + * short-circuits the refresh that would notice new rules. + */ + private memoisedVerdict(file: URI): boolean | undefined { + const cached = this._ignoreGlobResultCache.get(file); + if (!cached || cached.generation !== this._rulesGeneration || this._now() >= this._earliestRuleExpiry) { + return undefined; + } + // An exclusion is the most restrictive answer, so a positive verdict stands on its own. A + // negative one is only final when no regex rule could still exclude the file on content. + return cached.verdict || !this.isRegexContextExclusionsEnabled ? cached.verdict : undefined; + } + /** * Returns whether or not there are regex context exclusions. */ public get isRegexContextExclusionsEnabled(): boolean { - return [...this._contentExclusionCache.values()].some(({ ifAnyMatch, ifNoneMatch }: { ifAnyMatch: RegExp[]; ifNoneMatch: RegExp[] }) => ifAnyMatch.length > 0 || ifNoneMatch.length > 0); + return this._regexRuleCount > 0; } + /** * Loads the content exclusion rules for the given repositories. Primarily used to load a bunch of repos at once prior to a search for example. * @param repoUris The list of repository URIs to load the content exclusion rules for */ public async loadRepos(repoUris: URI[]) { const repos = await Promise.all(repoUris.map(uri => this._gitService.getRepositoryFetchUrls(uri))); - const repoInfos = repos.map(repo => { + const fetchUrls: string[] = []; + for (const repo of repos) { const repoInfo = this.getRepositoryInfo(repo); // Populate the repo root cache for future lookups if (repoInfo) { this._repoRootCache.set(repoInfo.repoRootPath, repoInfo); + fetchUrls.push(...repoInfo.fetchUrls); } - return this.shouldFetchContentExclusionRules(repoInfo); - }); - if (repoInfos.some(info => info)) { - this._lastRuleFetch = Date.now(); - await this.makeContentExclusionRequest(); } + await this.ensureRulesLoaded(fetchUrls); } public async asMinimatchPatterns() { - await this._contentExclusionFetchPromise; - const patterns: string[] = Array.from(this._contentExclusionCache.values()).flatMap(({ patterns }) => patterns); - return patterns; + // Anything already queued must land first so callers see a complete pattern set. + await Promise.all([...this._pendingRepos.values()].map(pending => pending.deferred.p)); + return Array.from(this._contentExclusionCache.values()).flatMap(({ patterns }) => patterns); } public dispose() { + this._disposed = true; + // Released before the limiter is disposed: it drops queued work without ever running it, so + // anything still registered here would otherwise leave its callers waiting forever. + this.settlePending([...this._pendingRepos]); + this._pendingRepos.clear(); this._disposables.forEach(d => d.dispose()); this._disposables = []; this._contentExclusionCache.clear(); + this._compiledGlobs = []; + this._regexRuleCount = 0; + this._earliestRuleExpiry = 0; } - private shouldFetchContentExclusionRules(repoInfo: RepoMetadata | undefined): boolean { - if (!repoInfo) { - return false; + /** + * Ensures rules for the given repos are loaded, fetching only what is missing or expired. + * + * Callers asking for the same repo share a single request, and each caller only waits on the + * repos it asked for, so a large background load cannot stall an individual file check. + * + * @returns whether every required rule set is now available. `false` means at least one fetch + * failed, and the caller must not memoise a verdict derived from the incomplete rules. + */ + private async ensureRulesLoaded(fetchUrls: readonly string[]): Promise<boolean> { + // Global/org rules are keyed under the non-git pseudo repo and can apply to any file. + const required = new Set<string>(fetchUrls); + required.add(NON_GIT_FILE_KEY); + + const now = this._now(); + const waits: Promise<void>[] = []; + for (const url of required) { + const cached = this._contentExclusionCache.get(url); + if (cached && now - cached.fetchedAt < RULE_TTL_MS) { + continue; + } + waits.push(this.enqueueRepo(url)); } - let shouldFetch = false; - for (const remoteRepoUrl of repoInfo?.fetchUrls ?? []) { - if (!this._contentExclusionCache.has(remoteRepoUrl)) { - shouldFetch = true; - this._contentExclusionCache.set(remoteRepoUrl, { patterns: [], ifAnyMatch: [], ifNoneMatch: [] }); + + if (waits.length > 0) { + await Promise.all(waits); + } + + for (const url of required) { + if (!this._contentExclusionCache.has(url)) { + return false; } } - return shouldFetch; + return true; + } + + /** Registers a repo for the next batch, joining an existing fetch when one is outstanding. */ + private enqueueRepo(url: string): Promise<void> { + const existing = this._pendingRepos.get(url); + if (existing) { + // Queued or already in flight; share that result rather than issuing a duplicate request. + return existing.deferred.p; + } + const pending: PendingFetch = { deferred: new DeferredPromise<void>(), dispatched: false }; + if (this._disposed) { + // Nothing will ever run, so release the caller instead of leaving it waiting. + pending.deferred.complete(undefined); + return pending.deferred.p; + } + this._pendingRepos.set(url, pending); + this.scheduleDrain(); + return pending.deferred.p; } /** - * A wrapper around the actual request - * TODO @lramos15 add cancellation to cancel the old request in flight - * @returns The promise which resolves when the request is complete + * Schedules a drain shortly after the first enqueue. The window is deliberately not reset by + * later enqueues so that a steady stream of repos cannot starve the fetch indefinitely. */ - private async makeContentExclusionRequest(): Promise<void> { - if (this._contentExclusionFetchPromise) { - await this._contentExclusionFetchPromise; + private scheduleDrain(): void { + if (this._scheduledDrain) { + return; } + this._scheduledDrain = (async () => { + await timeout(BATCH_WINDOW_MS); + this._scheduledDrain = undefined; + this.drainPendingRepos(); + })(); + } + + /** Dispatches everything queued but not yet sent as batched, concurrency limited requests. */ + private drainPendingRepos(): void { + if (this._disposed) { + return; + } + const batchable = [...this._pendingRepos].filter(([, pending]) => !pending.dispatched); + if (batchable.length === 0) { + return; + } + // Entries deliberately stay in the map until their request settles, so a lookup arriving + // while the request is slow joins it instead of queueing the same repo again. + batchable.forEach(([, pending]) => { pending.dispatched = true; }); + + for (let i = 0; i < batchable.length; i += REPOS_PER_REQUEST) { + const batch = batchable.slice(i, i + REPOS_PER_REQUEST); + this._batchLimiter.queue(() => this.fetchRulesForBatch(batch)); + } + } + + /** + * Fetches one batch of repos. Rules are only cached on success, so a transient failure is retried + * later rather than being remembered as "this repo has no exclusions". + */ + private async fetchRulesForBatch(batch: [string, PendingFetch][]): Promise<void> { + const repos = batch.map(([repo]) => repo); + const startTime = this._now(); try { - this._contentExclusionFetchPromise = this._contentExclusionRequest(); - await this._contentExclusionFetchPromise; - this._contentExclusionFetchPromise = null; - } catch { - this._contentExclusionFetchPromise = null; - } - } + const ghToken = (await this._authService.getGitHubSession('any', { silent: true }))?.accessToken; + const response = await this._fetchExclusionRules({ + url: `capi:${RequestType.ContentExclusion}`, + headers: { 'Authorization': `token ${ghToken}` }, + method: 'GET', + state: { repos } + }); - - /** - * The actual function that fetches the content exclusion rules from the GH API. - * Not recommended to call directly and instead use {@link makeContentExclusionRequest} as that ensures only one call is pending at any time - */ - private async _contentExclusionRequest(): Promise<void> { - // Clear the result cache as new rules will come and therefore it is no longer valid - this._ignoreGlobResultCache.clear(); - const startTime = Date.now(); - const capiClientService = this._capiClientService; - const ghToken = (await this._authService.getGitHubSession('any', { silent: true }))?.accessToken; - const remoteFetchUrls = Array.from(this._contentExclusionCache.keys()); - const updateRulesForRepos = async (reposToFetch: string[]) => { - - const response = await capiClientService.makeRequest<Response>({ - headers: { - 'Authorization': `token ${ghToken}` - }, - }, { type: RequestType.ContentExclusion, repos: reposToFetch }); - - if (!response.ok) { - this._logService.error(`Failed to fetch content exclusion rules: ${response?.statusText}`); + if (response.status < 200 || response.status >= 300) { + this._logService.error(`Failed to fetch content exclusion rules for ${repos.length} repo(s): ${response.status}`); return; } - const data: ContentExclusionResponse[] = await response.json(); - for (let j = 0; j < data.length; j++) { - const patterns = data[j].rules.map(rule => rule.paths).flat(); - const ifAnyMatch = coalesce(data[j].rules.map(rule => rule.ifAnyMatch).flat()).map(pattern => stringToRegex(pattern)); - const ifNoneMatch = coalesce(data[j].rules.map(rule => rule.ifNoneMatch).flat()).map(pattern => stringToRegex(pattern)); - const repo = reposToFetch[j]; - const rulesForRepo = { patterns, ifAnyMatch, ifNoneMatch }; - this._contentExclusionCache.set(repo, rulesForRepo); - this._logService.trace(`Fetched content exclusion rules for ${repo}: ${JSON.stringify(rulesForRepo)}`); + + this.applyRules(repos, await response.json() as ContentExclusionResponse[], startTime); + } catch (err) { + if (err instanceof FetchBlockedError) { + // A middleware is deliberately holding requests back. The repos stay uncached and are + // picked up again once the block lifts. + this._logService.warn(`Deferred content exclusion fetch for ${repos.length} repo(s): ${err.message}`); + } else { + this._logService.error(`Failed to fetch content exclusion rules: ${err}`); } - }; + } finally { + // Waiters always resume. On failure the repo stays uncached so it is fetched again later. + this.settlePending(batch); + } + } - // This is needed to fetch the global rules that could apply to non git files - if (remoteFetchUrls.length === 0) { - await updateRulesForRepos([]); + /** Releases a batch's waiters, deregistering entries that still belong to this attempt. */ + private settlePending(batch: readonly [string, PendingFetch][]): void { + for (const [url, pending] of batch) { + if (this._pendingRepos.get(url) === pending) { + this._pendingRepos.delete(url); + } + pending.deferred.complete(undefined); + } + } + + private applyRules(repos: string[], data: ContentExclusionResponse[], startTime: number): void { + const fetchedAt = this._now(); + const loggedRules: { patterns: string[]; ifAnyMatch: string[]; ifNoneMatch: string[] }[] = []; + let rulesChanged = false; + + for (let i = 0; i < repos.length; i++) { + // A missing entry means the server reported no rules for that repo. That is still a + // definitive answer, so it is cached to avoid refetching the repo forever. + const rules = data[i]?.rules ?? []; + const patterns = rules.flatMap(rule => rule.paths); + const ifAnyMatch = this.toRegexes(rules.flatMap(rule => rule.ifAnyMatch)); + const ifNoneMatch = this.toRegexes(rules.flatMap(rule => rule.ifNoneMatch)); + const previous = this._contentExclusionCache.get(repos[i]); + // Compared against what was there before, because rules being *removed* changes verdicts + // just as much as rules being added. + rulesChanged ||= !previous || !isSameRuleSet(previous, { patterns, ifAnyMatch, ifNoneMatch }); + this._contentExclusionCache.set(repos[i], { patterns, ifAnyMatch, ifNoneMatch, fetchedAt }); + loggedRules.push({ + patterns, + ifAnyMatch: ifAnyMatch.map(r => r.toString()), + ifNoneMatch: ifNoneMatch.map(r => r.toString()) + }); } - // Process in batches of 10 as that's the max content exclusion rules we can fetch at a time - for (let i = 0; i < remoteFetchUrls.length; i += 10) { - const batch = remoteFetchUrls.slice(i, i + 10); - await updateRulesForRepos(batch); - } - this._lastRuleFetch = Date.now(); - this._logService.info(`Fetched content exclusion rules in ${Date.now() - startTime}ms`); + this.rebuildCompiledRules(); - // Log the fetched rules to the request logger for debugging visibility - const repos = Array.from(this._contentExclusionCache.keys()); - const rules = repos.map(repo => { - const entry = this._contentExclusionCache.get(repo)!; - return { - patterns: entry.patterns, - ifAnyMatch: entry.ifAnyMatch.map(r => r.toString()), - ifNoneMatch: entry.ifNoneMatch.map(r => r.toString()) - }; - }); - this._requestLogger.logContentExclusionRules(repos, rules, Date.now() - startTime); + if (rulesChanged) { + this.invalidateVerdicts(); + } + + const duration = this._now() - startTime; + this._logService.info(`Fetched content exclusion rules for ${repos.length} repo(s) in ${duration}ms`); + this._requestLogger.logContentExclusionRules(repos, loggedRules, duration); + } + + /** Retires every memoised verdict, since the rules they were computed against no longer hold. */ + private invalidateVerdicts(): void { + this._rulesGeneration++; + this._ignoreGlobResultCache.clear(); + this._ignoreRegexResultCache.clear(); + } + + /** Rebuilds the flattened matcher list that {@link isIgnored} walks. */ + private rebuildCompiledRules(): void { + const globs: Minimatch[] = []; + let regexRuleCount = 0; + let earliestExpiry = Number.POSITIVE_INFINITY; + for (const { patterns, ifAnyMatch, ifNoneMatch, fetchedAt } of this._contentExclusionCache.values()) { + for (const pattern of patterns) { + try { + globs.push(new Minimatch(pattern, MINIMATCH_OPTIONS)); + } catch (err) { + this._logService.warn(`Skipping malformed content exclusion pattern '${pattern}': ${err}`); + } + } + regexRuleCount += ifAnyMatch.length + ifNoneMatch.length; + earliestExpiry = Math.min(earliestExpiry, fetchedAt + RULE_TTL_MS); + } + this._compiledGlobs = globs; + this._regexRuleCount = regexRuleCount; + // Zero while nothing is cached, which keeps memoised verdicts from being trusted before any + // rules have been loaded. + this._earliestRuleExpiry = this._contentExclusionCache.size > 0 ? earliestExpiry : 0; + } + + /** Compiles regex rules, skipping any the server sent that cannot be parsed. */ + private toRegexes(patterns: (string | undefined)[]): RegExp[] { + const compiled: RegExp[] = []; + for (const pattern of coalesce(patterns)) { + try { + compiled.push(stringToRegex(pattern)); + } catch (err) { + this._logService.warn(`Skipping malformed content exclusion regex '${pattern}': ${err}`); + } + } + return compiled; } @@ -357,10 +562,20 @@ export class RemoteContentExclusion implements IDisposable { } } +/** Compares two rule sets by content, so an unchanged refresh does not retire memoised verdicts. */ +function isSameRuleSet(a: Omit<CachedRules, 'fetchedAt'>, b: Omit<CachedRules, 'fetchedAt'>): boolean { + return equalStrings(a.patterns, b.patterns) + && equalStrings(a.ifAnyMatch.map(String), b.ifAnyMatch.map(String)) + && equalStrings(a.ifNoneMatch.map(String), b.ifNoneMatch.map(String)); +} + +function equalStrings(a: readonly string[], b: readonly string[]): boolean { + return a.length === b.length && a.every((value, index) => value === b[index]); +} + /** * Convert a given string /pattern/flags to a RegExp object - */ -function stringToRegex(str: string): RegExp { + */function stringToRegex(str: string): RegExp { // Handle Regex format of `pattern` vs /pattern/ if (!str.startsWith('/') && !str.endsWith('/')) { return new RegExp(str); diff --git a/extensions/copilot/src/platform/ignore/node/test/mockCAPIClientService.ts b/extensions/copilot/src/platform/ignore/node/test/mockCAPIClientService.ts index 5c52d5ebac5..622a6f61bb3 100644 --- a/extensions/copilot/src/platform/ignore/node/test/mockCAPIClientService.ts +++ b/extensions/copilot/src/platform/ignore/node/test/mockCAPIClientService.ts @@ -4,11 +4,45 @@ *--------------------------------------------------------------------------------------------*/ import type { FetchOptions, RequestMetadata } from '@vscode/copilot-api'; -import { Response } from '../../../networking/common/fetcherService'; +import { HeadersImpl, Response } from '../../../networking/common/fetcherService'; + +/** Shape of the content exclusion payload the endpoint returns for a single repo. */ +export type MockExclusionRules = { + paths?: string[]; + ifAnyMatch?: string[]; + ifNoneMatch?: string[]; +}; + +/** Builds a successful content exclusion response for the requested repos. */ +export function rulesResponse(rulesByRepo: ReadonlyMap<string, MockExclusionRules>, repos: string[]): Partial<Response> { + const payload = repos.map(repo => { + const rules = rulesByRepo.get(repo); + return { + last_updated_at: 0, + rules: rules ? [{ paths: rules.paths ?? [], ifAnyMatch: rules.ifAnyMatch, ifNoneMatch: rules.ifNoneMatch, source: { name: repo, type: 'Repository' } }] : [] + }; + }); + return { ok: true, status: 200, statusText: 'OK', json: () => Promise.resolve(payload) }; +} + +/** Builds a failing response, optionally carrying GitHub's rate limit headers. */ +export function failureResponse(status: number, headers: Record<string, string> = {}): Partial<Response> { + return { + ok: false, + status, + statusText: status === 403 ? 'Forbidden' : 'Error', + headers: new HeadersImpl(headers) + }; +} + +/** Builds a rate limited response of the shape api.github.com returns. */ +export function rateLimitedResponse(retryAfterSeconds: number): Partial<Response> { + return failureResponse(429, { 'retry-after': String(retryAfterSeconds) }); +} /** * A mock implementation of ICAPIClientService for testing. - * Returns an empty successful response by default. + * Records every request so tests can assert on batching and coalescing behaviour. * Note: Does not fully implement ICAPIClientService - only the methods needed for tests. */ export class MockCAPIClientService { @@ -16,25 +50,70 @@ export class MockCAPIClientService { abExpContext: string | undefined = undefined; - private _mockResponse: Response = { + /** Each entry is the list of repos sent in one request, in dispatch order. */ + readonly requestedBatches: string[][] = []; + + private _responder: (repos: string[]) => Partial<Response> = () => ({}); + private _gate: Promise<void> | undefined; + private _openGate: (() => void) | undefined; + + private readonly _defaultResponse: Response = { ok: true, status: 200, statusText: 'OK', - headers: new Map(), + headers: new HeadersImpl({}), text: () => Promise.resolve('[]'), json: () => Promise.resolve([]), arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), body: null, } as unknown as Response; - /** - * Sets the mock response to return from makeRequest. - */ - setMockResponse(response: Partial<Response>): void { - this._mockResponse = { ...this._mockResponse, ...response } as Response; + get requestCount(): number { + return this.requestedBatches.length; } - makeRequest<T>(_request: FetchOptions, _requestMetadata: RequestMetadata): Promise<T> { - return Promise.resolve(this._mockResponse as unknown as T); + /** Every repo requested across all batches, including any duplicates. */ + get requestedRepos(): string[] { + return this.requestedBatches.flat(); + } + + /** How many times the given repo was asked for. */ + timesRequested(repo: string): number { + return this.requestedRepos.filter(candidate => candidate === repo).length; + } + + reset(): void { + this.requestedBatches.length = 0; + } + + /** + * Sets a responder invoked with the repos of each request, so per-repo rules and + * per-attempt failures can be simulated. + */ + setResponder(responder: (repos: string[]) => Partial<Response>): void { + this._responder = responder; + } + + /** Holds every subsequent request open until {@link releaseRequests}, to model a slow endpoint. */ + blockRequests(): void { + this._gate = new Promise<void>(resolve => { this._openGate = resolve; }); + } + + releaseRequests(): void { + this._openGate?.(); + this._gate = undefined; + this._openGate = undefined; + } + + makeRequest<T>(_request: FetchOptions, requestMetadata: RequestMetadata): Promise<T> { + const repos = 'repos' in requestMetadata ? requestMetadata.repos : []; + // Recorded before awaiting the gate so tests can observe requests while they are in flight. + this.requestedBatches.push([...repos]); + const gate = this._gate; + if (!gate) { + return Promise.resolve({ ...this._defaultResponse, ...this._responder(repos) } as unknown as T); + } + return gate.then(() => ({ ...this._defaultResponse, ...this._responder(repos) }) as unknown as T); } } + diff --git a/extensions/copilot/src/platform/ignore/node/test/remoteContentExclusion.spec.ts b/extensions/copilot/src/platform/ignore/node/test/remoteContentExclusion.spec.ts index f799dca5f8f..5dfa9902dea 100644 --- a/extensions/copilot/src/platform/ignore/node/test/remoteContentExclusion.spec.ts +++ b/extensions/copilot/src/platform/ignore/node/test/remoteContentExclusion.spec.ts @@ -14,10 +14,13 @@ import { NullRequestLogger } from '../../../requestLogger/node/nullRequestLogger import { TestLogService } from '../../../testing/common/testLogService'; import { RemoteContentExclusion } from '../remoteContentExclusion'; import { MockAuthenticationService } from './mockAuthenticationService'; -import { MockCAPIClientService } from './mockCAPIClientService'; +import { MockCAPIClientService, failureResponse, rateLimitedResponse, rulesResponse, type MockExclusionRules } from './mockCAPIClientService'; import { MockGitService } from './mockGitService'; import { MockWorkspaceService } from './mockWorkspaceService'; +/** Key the implementation uses for global rules that apply outside any git repository. */ +const NON_GIT_FILE_KEY = 'non-git-file'; + suite('RemoteContentExclusion', () => { let remoteContentExclusion: RemoteContentExclusion; let mockGitService: MockGitService; @@ -26,8 +29,42 @@ suite('RemoteContentExclusion', () => { let mockCAPIClientService: MockCAPIClientService; let mockFileSystemService: MockFileSystemService; let mockWorkspaceService: MockWorkspaceService; + let now: number; + + function remoteFor(repoRoot: string): string { + return `https://github.com/org/${repoRoot.split('/').pop()}.git`; + } + + /** Routes each file to the repo whose root is the longest matching prefix of its path. */ + function routeToRepos(repoRoots: string[]): void { + const byLongestRoot = [...repoRoots].sort((a, b) => b.length - a.length); + mockGitService.getRepositoryFetchUrls = vi.fn().mockImplementation((uri: URI) => { + mockGitService.getRepositoryFetchUrlsCallCount++; + const root = byLongestRoot.find(candidate => uri.path === candidate || uri.path.startsWith(candidate + '/')); + return Promise.resolve(root ? { rootUri: URI.file(root), remoteFetchUrls: [remoteFor(root)] } : undefined); + }); + } + + function respondWithRules(rules: Record<string, MockExclusionRules>): void { + const byRepo = new Map(Object.entries(rules).map(([repoRoot, value]) => [remoteFor(repoRoot), value])); + mockCAPIClientService.setResponder(repos => rulesResponse(byRepo, repos)); + } + + /** Waits until the mock has recorded at least `count` requests, or gives up. */ + async function waitForRequests(count: number): Promise<void> { + const deadline = Date.now() + 2000; + while (mockCAPIClientService.requestCount < count && Date.now() < deadline) { + await new Promise(resolve => setTimeout(resolve, 5)); + } + } + + /** Gives any scheduled batching window time to elapse and dispatch. */ + function settleBatchWindow(): Promise<void> { + return new Promise(resolve => setTimeout(resolve, 250)); + } beforeEach(() => { + now = Date.UTC(2026, 0, 1); mockGitService = new MockGitService(); mockLogService = new TestLogService(); mockAuthService = new MockAuthenticationService(); @@ -45,7 +82,8 @@ suite('RemoteContentExclusion', () => { mockCAPIClientService as unknown as ICAPIClientService, mockFileSystemService, mockWorkspaceService, - new NullRequestLogger() + new NullRequestLogger(), + () => now ); }); @@ -232,4 +270,227 @@ suite('RemoteContentExclusion', () => { expect(mockGitService.getRepositoryFetchUrlsCallCount).toBe(0); }); }); + + describe('request coalescing', () => { + test('batches concurrent lookups instead of refreshing every repo per caller', async () => { + const repoRoots = Array.from({ length: 25 }, (_, i) => `/workspace/repo${i}`); + routeToRepos(repoRoots); + + await Promise.all(repoRoots.map(root => remoteContentExclusion.isIgnored(URI.file(`${root}/src/file.ts`), CancellationToken.None))); + + // 25 repos plus the non-git pseudo repo, sent 10 per request, each asked for exactly once. + expect({ + requests: mockCAPIClientService.requestCount, + reposSent: mockCAPIClientService.requestedRepos.length, + uniqueReposSent: new Set(mockCAPIClientService.requestedRepos).size + }).toEqual({ requests: 3, reposSent: 26, uniqueReposSent: 26 }); + }); + + test('only fetches repos that are missing from the cache', async () => { + routeToRepos(['/workspace/repo-a', '/workspace/repo-b']); + + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/one.ts'), CancellationToken.None); + mockCAPIClientService.reset(); + + // Same repo again: everything needed is already cached. + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/two.ts'), CancellationToken.None); + const afterCachedRepo = mockCAPIClientService.requestedRepos; + + // New repo: only the new remote is requested, not the whole cache. + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-b/one.ts'), CancellationToken.None); + + expect({ afterCachedRepo, afterNewRepo: mockCAPIClientService.requestedRepos }).toEqual({ + afterCachedRepo: [], + afterNewRepo: [remoteFor('/workspace/repo-b')] + }); + }); + + test('caches an empty ruleset so repos without rules are not refetched', async () => { + routeToRepos(['/workspace/repo-a']); + + // The default responder reports no rules for the requested repos. + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/one.ts'), CancellationToken.None); + mockCAPIClientService.reset(); + + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/two.ts'), CancellationToken.None); + + expect(mockCAPIClientService.requestCount).toBe(0); + }); + + test('refreshes rules once they expire', async () => { + routeToRepos(['/workspace/repo-a']); + + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/one.ts'), CancellationToken.None); + mockCAPIClientService.reset(); + + now += 31 * 60 * 1000; + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/two.ts'), CancellationToken.None); + + expect([...mockCAPIClientService.requestedRepos].sort()).toEqual([NON_GIT_FILE_KEY, remoteFor('/workspace/repo-a')].sort()); + }); + }); + + describe('failure handling', () => { + test('retries a repo whose fetch failed rather than treating it as unrestricted', async () => { + routeToRepos(['/workspace/repo-a']); + + let attempts = 0; + mockCAPIClientService.setResponder(repos => { + attempts++; + return attempts === 1 + ? rateLimitedResponse(60) + : rulesResponse(new Map([[remoteFor('/workspace/repo-a'), { paths: ['**/secret.ts'] }]]), repos); + }); + + const secret = URI.file('/workspace/repo-a/secret.ts'); + const whileFailing = await remoteContentExclusion.isIgnored(secret, CancellationToken.None); + + // Past the backoff window the rules load and the same file is now correctly excluded. + now += 5 * 60 * 1000; + const afterRecovery = await remoteContentExclusion.isIgnored(secret, CancellationToken.None); + + expect({ whileFailing, afterRecovery }).toEqual({ whileFailing: false, afterRecovery: true }); + }); + + test('stops issuing requests while the backoff is in effect', async () => { + routeToRepos(['/workspace/repo-a']); + mockCAPIClientService.setResponder(() => rateLimitedResponse(600)); + + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/one.ts'), CancellationToken.None); + const afterFirst = mockCAPIClientService.requestCount; + + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/two.ts'), CancellationToken.None); + + expect({ afterFirst, afterSecond: mockCAPIClientService.requestCount }).toEqual({ afterFirst: 1, afterSecond: 1 }); + }); + + test('waits for the reported reset window when rate limited', async () => { + routeToRepos(['/workspace/repo-a']); + const resetEpochSeconds = Math.floor((now + 10 * 60 * 1000) / 1000); + mockCAPIClientService.setResponder(() => failureResponse(403, { + 'x-ratelimit-remaining': '0', + 'x-ratelimit-reset': String(resetEpochSeconds) + })); + + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/one.ts'), CancellationToken.None); + + // Still inside the window the server reported, so no further calls are made. + now += 5 * 60 * 1000; + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/two.ts'), CancellationToken.None); + const duringWindow = mockCAPIClientService.requestCount; + + // Retrying after the reset also proves the quota 403 was classified as a rate limit + // rather than an auth failure, which would have blocked the token for an hour. + now += 6 * 60 * 1000; + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/three.ts'), CancellationToken.None); + + expect({ duringWindow, afterWindow: mockCAPIClientService.requestCount }).toEqual({ duringWindow: 1, afterWindow: 2 }); + }); + }); + + describe('rule matching', () => { + test('excludes files matching a fetched glob rule', async () => { + routeToRepos(['/workspace/repo-a']); + respondWithRules({ '/workspace/repo-a': { paths: ['**/*.env'] } }); + + expect({ + env: await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/config.env'), CancellationToken.None), + source: await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/index.ts'), CancellationToken.None) + }).toEqual({ env: true, source: false }); + }); + + test('ignores malformed patterns rather than failing every lookup', async () => { + routeToRepos(['/workspace/repo-a']); + respondWithRules({ '/workspace/repo-a': { paths: ['**/*.env'], ifAnyMatch: ['/(unclosed/'] } }); + + expect(await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/config.env'), CancellationToken.None)).toBe(true); + }); + }); + + describe('picking up rule changes', () => { + test('re-evaluates a file once its rules expire', async () => { + routeToRepos(['/workspace/repo-a']); + respondWithRules({}); + + const file = URI.file('/workspace/repo-a/secret.ts'); + const beforeRuleAdded = await remoteContentExclusion.isIgnored(file, CancellationToken.None); + + // The server starts excluding the file after the first verdict was memoised. + respondWithRules({ '/workspace/repo-a': { paths: ['**/secret.ts'] } }); + now += 31 * 60 * 1000; + const afterRuleAdded = await remoteContentExclusion.isIgnored(file, CancellationToken.None); + + expect({ beforeRuleAdded, afterRuleAdded }).toEqual({ beforeRuleAdded: false, afterRuleAdded: true }); + }); + + test('stops excluding a file once the server removes the last rule', async () => { + routeToRepos(['/workspace/repo-a']); + respondWithRules({ '/workspace/repo-a': { paths: ['**/secret.ts'] } }); + + const file = URI.file('/workspace/repo-a/secret.ts'); + const whileExcluded = await remoteContentExclusion.isIgnored(file, CancellationToken.None); + + // Replacing the rules with an empty set must retire the memoised exclusion. + respondWithRules({}); + now += 31 * 60 * 1000; + const afterRuleRemoved = await remoteContentExclusion.isIgnored(file, CancellationToken.None); + + expect({ whileExcluded, afterRuleRemoved }).toEqual({ whileExcluded: true, afterRuleRemoved: false }); + }); + + test('keeps memoised verdicts when a refresh returns identical rules', async () => { + routeToRepos(['/workspace/repo-a']); + respondWithRules({ '/workspace/repo-a': { paths: ['**/secret.ts'] } }); + + await remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/secret.ts'), CancellationToken.None); + const other = URI.file('/workspace/repo-a/index.ts'); + await remoteContentExclusion.isIgnored(other, CancellationToken.None); + + // An unchanged refresh should not force previously computed verdicts to be recomputed. + now += 31 * 60 * 1000; + await remoteContentExclusion.isIgnored(other, CancellationToken.None); + mockGitService.getRepositoryFetchUrlsCallCount = 0; + const afterUnchangedRefresh = await remoteContentExclusion.isIgnored(other, CancellationToken.None); + + expect({ afterUnchangedRefresh, gitLookups: mockGitService.getRepositoryFetchUrlsCallCount }).toEqual({ afterUnchangedRefresh: false, gitLookups: 0 }); + }); + }); + + describe('in-flight requests', () => { + test('joins a slow in-flight request instead of issuing a duplicate', async () => { + routeToRepos(['/workspace/repo-a']); + mockCAPIClientService.blockRequests(); + + const first = remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/one.ts'), CancellationToken.None); + await waitForRequests(1); + const whileDispatched = mockCAPIClientService.requestCount; + + // A second lookup for the same repo arrives while the request is still open. + const second = remoteContentExclusion.isIgnored(URI.file('/workspace/repo-a/two.ts'), CancellationToken.None); + await settleBatchWindow(); + const afterSecondLookup = mockCAPIClientService.requestCount; + + mockCAPIClientService.releaseRequests(); + await Promise.all([first, second]); + + expect({ whileDispatched, afterSecondLookup }).toEqual({ whileDispatched: 1, afterSecondLookup: 1 }); + }); + + test('releases callers waiting on queued batches when disposed', async () => { + // More batches than the limiter runs concurrently, so some are still queued on dispose. + const repoRoots = Array.from({ length: 80 }, (_, i) => `/workspace/repo${i}`); + routeToRepos(repoRoots); + mockCAPIClientService.blockRequests(); + + const lookups = Promise.all(repoRoots.map(root => remoteContentExclusion.isIgnored(URI.file(`${root}/file.ts`), CancellationToken.None))); + await waitForRequests(1); + + remoteContentExclusion.dispose(); + + // The limiter drops queued batches without running them, so their waiters must be + // settled by dispose or these lookups would never resolve. + await expect(lookups).resolves.toHaveLength(80); + mockCAPIClientService.releaseRequests(); + }); + }); }); diff --git a/extensions/copilot/src/shared-fetch-utils/common/middleware/rateLimitBackoffMiddleware.ts b/extensions/copilot/src/shared-fetch-utils/common/middleware/rateLimitBackoffMiddleware.ts new file mode 100644 index 00000000000..0830e48c68e --- /dev/null +++ b/extensions/copilot/src/shared-fetch-utils/common/middleware/rateLimitBackoffMiddleware.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { FetchBlockedError, type FetchMiddleware, type HttpHeaders } from '../fetchTypes'; + +export class RateLimitBackoffError extends FetchBlockedError { + constructor(retryAfterMs: number) { + super(`Rate limited, backing off for ${Math.round(retryAfterMs / 1000)}s`, retryAfterMs); + } +} + +export interface RateLimitBackoffOptions { + /** Delay applied to the first rate limit when the server sends no hint. */ + readonly initialDelayMs?: number; + readonly maxDelayMs?: number; + readonly multiplier?: number; + /** Injectable clock, primarily so tests do not have to wait on the wall clock. */ + readonly now?: () => number; +} + +/** + * Blocks subsequent requests once the server reports a rate limit, so a client that shares a + * quota with other callers cannot dig itself deeper. + * + * The wait is taken from the server whenever it says so, via `Retry-After` or GitHub's + * `x-ratelimit-remaining`/`x-ratelimit-reset` pair, and otherwise falls back to an + * exponentially increasing delay. Either way the wait is capped at {@link maxDelayMs}. The + * backoff resets on the first response that is not rate limited. + * + * This complements {@link serverErrorBackoffMiddleware}, which covers `5xx` responses. + */ +export function rateLimitBackoffMiddleware(options?: RateLimitBackoffOptions): FetchMiddleware { + const { + initialDelayMs = 60_000, + maxDelayMs = 15 * 60_000, + multiplier = 2, + now = Date.now, + } = options ?? {}; + let consecutiveRateLimits = 0; + let blockedUntil = 0; + + return (next) => async (request) => { + if (now() < blockedUntil) { + throw new RateLimitBackoffError(blockedUntil - now()); + } + + const response = await next(request); + + if (!isRateLimited(response.status, response.headers)) { + // A response that was already in flight when a concurrent request hit a rate limit must + // not clear that newer block, otherwise later calls reach the server during the window + // the server asked us to wait out. + if (now() >= blockedUntil) { + consecutiveRateLimits = 0; + blockedUntil = 0; + } + return response; + } + + consecutiveRateLimits++; + const hinted = retryAfterFromHeaders(response.headers, now); + const backoff = hinted ?? initialDelayMs * Math.pow(multiplier, consecutiveRateLimits - 1); + // `maxDelayMs` caps the server's hint too, so a bogus or hostile `Retry-After` cannot stall + // the client indefinitely. Retrying a little early simply re-arms the backoff. + const delay = Math.min(backoff, maxDelayMs); + blockedUntil = now() + delay; + throw new RateLimitBackoffError(delay); + }; +} + +function isRateLimited(status: number, headers: HttpHeaders): boolean { + if (status === 429) { + return true; + } + // GitHub reports an exhausted primary rate limit as a 403 carrying the quota headers, which + // has to be told apart from a plain authorization failure. + return status === 403 && readHeader(headers, 'x-ratelimit-remaining') === '0'; +} + +function retryAfterFromHeaders(headers: HttpHeaders, now: () => number): number | undefined { + const retryAfter = Number(readHeader(headers, 'retry-after')); + if (Number.isFinite(retryAfter) && retryAfter > 0) { + return retryAfter * 1000; + } + const reset = Number(readHeader(headers, 'x-ratelimit-reset')); + if (Number.isFinite(reset) && reset > 0) { + return Math.max(0, reset * 1000 - now()); + } + return undefined; +} + +/** HTTP header names are case insensitive, but not every headers implementation normalises them. */ +function readHeader(headers: HttpHeaders, lowerCaseName: string): string | undefined { + const canonical = lowerCaseName.replace(/(^|-)([a-z])/g, (_, separator: string, char: string) => separator + char.toUpperCase()); + return headers.get(lowerCaseName) ?? headers.get(canonical) ?? undefined; +} diff --git a/extensions/copilot/src/shared-fetch-utils/common/test/rateLimitBackoffMiddleware.spec.ts b/extensions/copilot/src/shared-fetch-utils/common/test/rateLimitBackoffMiddleware.spec.ts new file mode 100644 index 00000000000..868185b888c --- /dev/null +++ b/extensions/copilot/src/shared-fetch-utils/common/test/rateLimitBackoffMiddleware.spec.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { beforeEach, describe, expect, it } from 'vitest'; +import type { HttpHeaders, HttpRequest, HttpResponse } from '../fetchTypes'; +import { RateLimitBackoffError, rateLimitBackoffMiddleware } from '../middleware/rateLimitBackoffMiddleware'; + +function makeHeaders(entries: Record<string, string> = {}): HttpHeaders { + const map = new Map(Object.entries(entries).map(([key, value]) => [key.toLowerCase(), value])); + return { get: (name: string) => map.get(name.toLowerCase()) ?? null }; +} + +function makeResponse(status: number, headers: Record<string, string> = {}): HttpResponse { + return { + status, + headers: makeHeaders(headers), + body: null, + async text() { return ''; }, + async json() { return {}; }, + }; +} + +const request: HttpRequest = { url: 'https://api.github.com/example', headers: {} }; + +describe('rateLimitBackoffMiddleware', () => { + let now: number; + let calls: number; + + beforeEach(() => { + now = Date.UTC(2026, 0, 1); + calls = 0; + }); + + /** Wires the middleware around a stub that always returns the given response. */ + function withResponse(response: HttpResponse) { + return rateLimitBackoffMiddleware({ now: () => now })(async () => { + calls++; + return response; + }); + } + + async function expectBlocked(fetchFn: (request: HttpRequest) => Promise<HttpResponse>): Promise<number> { + try { + await fetchFn(request); + throw new Error('expected the request to be blocked'); + } catch (err) { + expect(err).toBeInstanceOf(RateLimitBackoffError); + return (err as RateLimitBackoffError).retryAfterMs; + } + } + + it('passes successful responses straight through', async () => { + const fetchFn = withResponse(makeResponse(200)); + + expect((await fetchFn(request)).status).toBe(200); + }); + + it('leaves a plain 403 alone so auth failures are not mistaken for rate limits', async () => { + const fetchFn = withResponse(makeResponse(403)); + + expect((await fetchFn(request)).status).toBe(403); + }); + + it('blocks further requests after a 429 and honours retry-after', async () => { + const fetchFn = withResponse(makeResponse(429, { 'retry-after': '120' })); + + const firstDelay = await expectBlocked(fetchFn); + // The second attempt is refused locally, without reaching the server. + const callsAfterBlock = calls; + await expectBlocked(fetchFn); + + expect({ firstDelay, callsAfterBlock, callsNow: calls }).toEqual({ firstDelay: 120_000, callsAfterBlock: 1, callsNow: 1 }); + }); + + it('treats an exhausted quota 403 as a rate limit and waits for the reset', async () => { + const resetEpochSeconds = Math.floor((now + 10 * 60_000) / 1000); + const fetchFn = withResponse(makeResponse(403, { + 'x-ratelimit-remaining': '0', + 'x-ratelimit-reset': String(resetEpochSeconds) + })); + + const delay = await expectBlocked(fetchFn); + + // Still blocked partway through the window, allowed through once it passes. + now += 5 * 60_000; + await expectBlocked(fetchFn); + const callsDuringWindow = calls; + + now += 6 * 60_000; + await expectBlocked(fetchFn); + + expect({ delay, callsDuringWindow, callsAfterWindow: calls }).toEqual({ delay: 10 * 60_000, callsDuringWindow: 1, callsAfterWindow: 2 }); + }); + + it('backs off exponentially when the server sends no hint', async () => { + const fetchFn = withResponse(makeResponse(429)); + + const delays: number[] = []; + for (let attempt = 0; attempt < 3; attempt++) { + delays.push(await expectBlocked(fetchFn)); + now += delays[delays.length - 1]; + } + + expect(delays).toEqual([60_000, 120_000, 240_000]); + }); + + it('caps the server hint so a bogus retry-after cannot stall the client', async () => { + const fetchFn = rateLimitBackoffMiddleware({ maxDelayMs: 600_000, now: () => now })(async () => { + calls++; + return makeResponse(429, { 'retry-after': '86400' }); + }); + + expect(await expectBlocked(fetchFn)).toBe(600_000); + }); + + it('keeps a newer block when an older successful response lands afterwards', async () => { + let releaseSlowResponse = () => { }; + const slowResponse = new Promise<void>(resolve => { releaseSlowResponse = resolve; }); + let isFirstCall = true; + const fetchFn = rateLimitBackoffMiddleware({ now: () => now })(async () => { + calls++; + if (isFirstCall) { + isFirstCall = false; + await slowResponse; + return makeResponse(200); + } + return makeResponse(429, { 'retry-after': '120' }); + }); + + // A slow success is still in flight when a second request is rate limited. + const inFlight = fetchFn(request); + await expectBlocked(fetchFn); + releaseSlowResponse(); + await inFlight; + + // The block established by the newer 429 must survive the older success. + const delayAfterSuccess = await expectBlocked(fetchFn); + + expect({ delayAfterSuccess, calls }).toEqual({ delayAfterSuccess: 120_000, calls: 2 }); + }); + + it('resets the backoff once a request succeeds', async () => { + let status = 429; + const fetchFn = rateLimitBackoffMiddleware({ now: () => now })(async () => { + calls++; + return makeResponse(status); + }); + + await expectBlocked(fetchFn); + now += 60_000; + + status = 200; + await fetchFn(request); + + // Back to the initial delay rather than continuing to double. + status = 429; + expect(await expectBlocked(fetchFn)).toBe(60_000); + }); +}); From 9fee93e1342c9be30bac248fa7d45f3ef9a57e47 Mon Sep 17 00:00:00 2001 From: Logan Ramos <lramos15@gmail.com> Date: Fri, 31 Jul 2026 08:44:40 -0400 Subject: [PATCH 66/86] Harden cache expiry notification logic (#328301) * Harden cache expiry notification logic * Update AHP snapshot for deduped prompt-cache meta writes The Copilot client-tool E2E capture has no cacheExpiresAt, so the old code called _setPromptCacheState(undefined) on every main-agent usage event and unconditionally wrote session meta, emitting two no-op session/metaChanged actions. Those writes are now deduped, so drop the entries from the snapshot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5a85f27d-5a24-405c-bc70-5fd820001e64 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5a85f27d-5a24-405c-bc70-5fd820001e64 --- .../node/copilot/copilotAgentSession.ts | 75 ++++--- .../test/node/copilotAgentSession.test.ts | 184 +++++++++++++++++- ...after_start_and_completes.traffic.ahp.yaml | 6 - .../agentHostPromptCacheNotification.ts | 9 +- .../agentHostPromptCacheNotification.test.ts | 8 +- 5 files changed, 236 insertions(+), 46 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index 4c5118fd1b7..25f04dc1ce0 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -45,7 +45,7 @@ import { isAgentFeedbackAnnotationsAttachment, renderAgentFeedbackAnnotationsAtt import { ISessionDatabase, ISessionDataService, SESSION_ATTACHMENTS_DIRNAME } from '../../common/sessionDataService.js'; import { MessageAttachmentKind, ToolCallContributorKind, type FileEdit, type MessageAttachment, type ToolCallContributor } from '../../common/state/protocol/state.js'; import { ActionType, isChatAction, type ChatAction, type SessionAction } from '../../common/state/sessionActions.js'; -import { MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallStatus, ToolResultContentType, buildSubagentSessionUri, getToolSubagentContent, isDefaultChatUri, isSubagentSession, withSessionPromptCacheState, type Message, type PendingMessage, type ChatInputAnswer, type ChatInputOption, type ChatInputQuestion, type ChatInputRequest, type ToolCallResult, type ToolResultContent, type ToolResultTerminalContent, type Turn, type UsageInfo, type UsageInfoMeta, type IContextAttributionData } from '../../common/state/sessionState.js'; +import { MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallStatus, ToolResultContentType, buildSubagentSessionUri, getToolSubagentContent, isDefaultChatUri, isSubagentSession, readSessionPromptCacheState, withSessionPromptCacheState, type Message, type PendingMessage, type ChatInputAnswer, type ChatInputOption, type ChatInputQuestion, type ChatInputRequest, type ToolCallResult, type ToolResultContent, type ToolResultTerminalContent, type Turn, type UsageInfo, type UsageInfoMeta, type IContextAttributionData, type ISessionPromptCacheState } from '../../common/state/sessionState.js'; import { IAgentConfigurationService } from '../agentConfigurationService.js'; import type { IExitPlanModeResponse } from './copilotAgent.js'; import { CopilotSessionWrapper } from './copilotSessionWrapper.js'; @@ -726,8 +726,10 @@ export class CopilotAgentSession extends Disposable { * per-event `copilotUsage` instead (see {@link CopilotTurn.copilotNanoAiu}). */ private _sessionTotalNanoAiu = 0; + private _promptCacheState: ISessionPromptCacheState | undefined; + private _promptCacheRefreshGeneration = 0; /** - * Serializes the metrics reads behind {@link _refreshSessionTotalNanoAiu}. Several + * Serializes the metrics reads behind {@link _refreshSessionUsageMetrics}. Several * handlers refresh the total, so without this their RPCs overlap and an older * one resolving last would publish a session cost that visibly regresses. A * high-water mark cannot be used to reject stale reads instead, because the @@ -735,7 +737,7 @@ export class CopilotAgentSession extends Disposable { * one read in flight makes out-of-order resolution impossible, and coalesces * the redundant reads that a burst of usage events would otherwise issue. */ - private readonly _sessionTotalRefreshThrottler = this._register(new Throttler()); + private readonly _sessionUsageMetricsRefreshThrottler = this._register(new Throttler()); /** SDK session wrapper, set by {@link initializeSession}. */ private _wrapper!: CopilotSessionWrapper; private readonly _slashCommandProvider: CopilotSlashCommandProvider; @@ -1179,20 +1181,19 @@ export class CopilotAgentSession extends Disposable { this._currentTurn = new CopilotTurn(turnId, this._nextTurnOrdinal++, senderClientId, clientType); } - /** - * Re-reads the SDK's session-wide nano-AIU total. Returns `true` when the - * value changed, i.e. when a usage report is worth re-emitting. - * - * Reads are serialized by {@link _sessionTotalRefreshThrottler}, so the value - * always reflects the most recent one. The total is not monotonic — - * `history.truncate` (checkpoint restore, editing an earlier message) makes - * the SDK re-fold usage from the surviving events, so it legitimately drops — - * which is why a decrease is adopted rather than rejected as stale. - */ - private async _refreshSessionTotalNanoAiu(): Promise<boolean> { + /** Refreshes prompt-cache state and the session-wide nano-AIU total from the SDK's authoritative usage metrics. */ + private async _refreshSessionUsageMetrics(): Promise<boolean> { try { - return await this._sessionTotalRefreshThrottler.queue(async () => { - const total = (await this._wrapper.session.rpc.usage.getMetrics()).totalNanoAiu; + return await this._sessionUsageMetricsRefreshThrottler.queue(async () => { + const promptCacheRefreshGeneration = this._promptCacheRefreshGeneration; + const metrics = await this._wrapper.session.rpc.usage.getMetrics(); + const modelId = metrics.currentModel; + if (!this._store.isDisposed && modelId && promptCacheRefreshGeneration === this._promptCacheRefreshGeneration) { + const cacheExpiresAt = metrics.modelMetrics[modelId]?.cacheExpiresAt; + this._setPromptCacheState(cacheExpiresAt ? { modelId, cacheExpiresAt } : undefined); + } + + const total = metrics.totalNanoAiu; if (typeof total !== 'number' || !Number.isFinite(total) || total < 0 || total === this._sessionTotalNanoAiu) { return false; } @@ -1706,6 +1707,13 @@ export class CopilotAgentSession extends Disposable { this._subscribeForMemoInvalidation(); this._subscribeForInstructionsCollectedTelemetry(); this._subscribeToPermissionConfigChanges(); + this._promptCacheState = readSessionPromptCacheState(this._stateManager.getSessionSummary(this.sessionUri.toString())?._meta); + if (this._launchPlan.kind === 'resume') { + await this._refreshSessionUsageMetrics(); + if (this._store.isDisposed) { + throw new CancellationError(); + } + } // Advertise the agent host's server tools for this session so clients // see them as server-provided. Execution happens in-process via the SDK @@ -1713,8 +1721,17 @@ export class CopilotAgentSession extends Disposable { this._serverToolHost?.advertise(this._storageUri.toString()); } - private _setPromptCacheState(promptCache: { readonly modelId: string; readonly cacheExpiresAt: string } | undefined): void { - const currentMeta = this._stateManager.getSessionSummary(this.sessionUri.toString())?._meta; + private _setPromptCacheState(promptCache: ISessionPromptCacheState | undefined): void { + const currentSummary = this._stateManager.getSessionSummary(this.sessionUri.toString()); + const currentMeta = currentSummary?._meta; + // Concurrent sessions can share `sessionUri`, so the persisted metadata — not this + // instance's cached value — is authoritative whenever a summary is available. + const currentPromptCache = currentSummary ? readSessionPromptCacheState(currentMeta) : this._promptCacheState; + this._promptCacheState = currentPromptCache; + if (currentPromptCache?.modelId === promptCache?.modelId && currentPromptCache?.cacheExpiresAt === promptCache?.cacheExpiresAt) { + return; + } + this._promptCacheState = promptCache; this._stateManager.setSessionMeta(this.sessionUri.toString(), withSessionPromptCacheState(currentMeta, promptCache)); } @@ -1940,7 +1957,7 @@ export class CopilotAgentSession extends Disposable { // `session.compaction_complete` has already folded the summarization call's // cost into the turn by the time this RPC resolves; refresh the session total // so the report carries both. - await this._refreshSessionTotalNanoAiu(); + await this._refreshSessionUsageMetrics(); const copilotUsage = this._parentCopilotUsageMeta(); this._emitAction({ type: ActionType.ChatUsage, @@ -4238,8 +4255,13 @@ export class CopilotAgentSession extends Disposable { // child session (via `parentToolCallId`) for the subagent tool to show // its own cost. const parentToolCallId = this._parentToolCallIdForSubagentEvent(e); - if (!parentToolCallId && !e.agentId && !e.data.parentToolCallId && e.data.model) { - this._setPromptCacheState(e.data.cacheExpiresAt ? { modelId: e.data.model, cacheExpiresAt: e.data.cacheExpiresAt } : undefined); + if (!parentToolCallId && !e.agentId && !e.data.parentToolCallId) { + this._promptCacheRefreshGeneration++; + if (e.data.model && e.data.cacheExpiresAt) { + this._setPromptCacheState({ modelId: e.data.model, cacheExpiresAt: e.data.cacheExpiresAt }); + } else if (e.data.model && this._promptCacheState?.modelId !== e.data.model) { + this._setPromptCacheState(undefined); + } } // `copilotUsage` is marked `asInternal` in the SDK schema so it is not exposed on the generated // `AssistantUsageData` type, but it is present at runtime. Read it dynamically. @@ -4358,7 +4380,7 @@ export class CopilotAgentSession extends Disposable { model: e.data.model, cacheReadTokens: e.data.cacheReadTokens, }; - await this._refreshSessionTotalNanoAiu(); + await this._refreshSessionUsageMetrics(); const attribution = isSubagentEvent ? undefined : await this._readContextAttribution(); if (!turnId) { return; @@ -4442,7 +4464,7 @@ export class CopilotAgentSession extends Disposable { // Then pick up the session-wide total, which also covers a compaction billed // while no turn was active, and re-emit so the widget reflects it. const turnIdBeforeRefresh = this._turnId; - if (await this._refreshSessionTotalNanoAiu() && turnIdBeforeRefresh === this._turnId) { + if (await this._refreshSessionUsageMetrics() && turnIdBeforeRefresh === this._turnId) { emitParentUsage(); } })); @@ -4959,6 +4981,13 @@ export class CopilotAgentSession extends Disposable { this._register(wrapper.onSessionModelChange(e => { this._logService.trace(`[Copilot:${sessionId}] Model changed: ${e.data.previousModel ?? '(none)'} -> ${e.data.newModel}`); + if (!e.agentId) { + this._promptCacheRefreshGeneration++; + if (e.data.previousModel !== e.data.newModel) { + this._setPromptCacheState(undefined); + } + void this._refreshSessionUsageMetrics(); + } })); this._register(wrapper.onManagedSettingsResolved(e => { diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index 8eb7710d096..a9ac6b9c166 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -30,7 +30,7 @@ import { readToolCallMeta } from '../../common/meta/agentToolCallMeta.js'; import { IDiffComputeService } from '../../common/diffComputeService.js'; import { ISessionDataService, type ISessionDatabase } from '../../common/sessionDataService.js'; import { ActionType, type ChatDeltaAction, type ChatErrorAction, type ChatInputRequestedAction, type ChatResponsePartAction, type ChatToolCallCompleteAction, type ChatToolCallDeltaAction, type ChatToolCallReadyAction, type ChatToolCallStartAction, type ChatTurnCompleteAction, type ChatUsageAction, type SessionAction, type StateAction } from '../../common/state/sessionActions.js'; -import { MessageAttachmentKind, MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildChatUri, buildDefaultChatUri, createSessionState, mergeSessionWithDefaultChat, readSessionPromptCacheState, readUsageInfoMeta, SessionStatus, type ToolDefinition, type ToolResultContent, type ToolResultFileEditContent, type ToolResultTerminalContent, type UsageInfoMeta } from '../../common/state/sessionState.js'; +import { MessageAttachmentKind, MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildChatUri, buildDefaultChatUri, createSessionState, mergeSessionWithDefaultChat, readSessionPromptCacheState, readUsageInfoMeta, SessionStatus, withSessionPromptCacheState, type ToolDefinition, type ToolResultContent, type ToolResultFileEditContent, type ToolResultTerminalContent, type UsageInfoMeta } from '../../common/state/sessionState.js'; import { TerminalClaimKind } from '../../common/state/protocol/state.js'; import { STREAMING_TOOL_DISPLAY_INTERVAL_MS } from '../../common/streamingToolCallDisplay.js'; import { CustomizationType, McpAuthRequiredReason, McpServerStatus, type Customization } from '../../common/state/protocol/channels-session/state.js'; @@ -104,7 +104,7 @@ class MockCopilotSession { totalNanoAiu: 0, sessionStartTime: new Date().toISOString(), codeChanges: { linesAdded: 0, linesRemoved: 0, filesModifiedCount: 0, filesModified: [] }, - modelMetrics: {}, + modelMetrics: {} as Record<string, { cacheExpiresAt?: string }>, currentModel: undefined as string | undefined, lastCallInputTokens: 0, lastCallOutputTokens: 0, @@ -169,6 +169,21 @@ class MockCopilotSession { * into the session-wide total that `usage.getMetrics` reports. */ private _accumulateUsageMetrics(type: SessionEventType, data: unknown): void { + if (type === 'session.model_change') { + const modelChange = data as { newModel?: string }; + if (modelChange.newModel) { + this.usageMetricsResult.currentModel = modelChange.newModel; + } + } + if (type === 'assistant.usage') { + const usage = data as { model?: string; cacheExpiresAt?: string; parentToolCallId?: string }; + if (!usage.parentToolCallId && usage.model) { + this.usageMetricsResult.currentModel = usage.model; + if (usage.cacheExpiresAt) { + this.usageMetricsResult.modelMetrics[usage.model] = { cacheExpiresAt: usage.cacheExpiresAt }; + } + } + } const billed = type === 'assistant.usage' ? data : type === 'session.compaction_complete' @@ -478,6 +493,7 @@ async function createAgentSession(disposables: DisposableStore, options?: { /** Configure the mock session before {@link CopilotAgentSession.initializeSession} runs. */ configureMockSession?: (session: MockCopilotSession) => void; sessionCustomizations?: () => readonly Customization[]; + initialSessionMeta?: Record<string, unknown>; sessionUri?: URI; chatChannelUri?: URI; resolveMcpChildId?: (serverName: string) => string | undefined; @@ -494,6 +510,7 @@ async function createAgentSession(disposables: DisposableStore, options?: { isLaunchTokenCurrent?: () => boolean; onTurnEnded?: () => void; modelId?: string; + resume?: boolean; }): Promise<{ session: CopilotAgentSession; runtime: ICopilotSessionRuntime; @@ -539,8 +556,7 @@ async function createAgentSession(disposables: DisposableStore, options?: { const mockSession = new MockCopilotSession(); options?.configureMockSession?.(mockSession); - const launchPlan: CopilotSessionLaunchPlan = { - kind: 'create', + const launchPlanBase = { client: { createSession: async () => mockSession as unknown as CopilotSession, resumeSession: async () => mockSession as unknown as CopilotSession, @@ -552,8 +568,20 @@ async function createAgentSession(disposables: DisposableStore, options?: { snapshot: options?.clientSnapshot ?? { tools: [], plugins: [], mcpServers: {} }, shellManager: undefined, githubToken: options?.githubToken, - model: options?.modelId ? { id: options.modelId } : undefined, }; + const model = options?.modelId ? { id: options.modelId } : undefined; + const launchPlan: CopilotSessionLaunchPlan = options?.resume + ? { + ...launchPlanBase, + kind: 'resume', + workingDirectory: options.workingDirectory ?? URI.file('/workspace'), + fallback: { model }, + } + : { + ...launchPlanBase, + kind: 'create', + model, + }; let launchedRuntime: ICopilotSessionRuntime | undefined; const sessionLauncher: ICopilotSessionLauncher = { launch: async (_plan, runtime) => { @@ -631,7 +659,7 @@ async function createAgentSession(disposables: DisposableStore, options?: { super.dispatchServerAction(channel, action); } override getSessionState(session: string) { - if (!options?.sessionCustomizations || session !== sessionUri.toString()) { + if ((!options?.sessionCustomizations && !options?.initialSessionMeta) || session !== sessionUri.toString()) { return undefined; } const state = createSessionState({ @@ -642,7 +670,26 @@ async function createAgentSession(disposables: DisposableStore, options?: { createdAt: new Date().toISOString(), modifiedAt: new Date().toISOString(), }); - return mergeSessionWithDefaultChat({ ...state, customizations: [...options.sessionCustomizations()] }, undefined); + return mergeSessionWithDefaultChat({ + ...state, + ...(options.initialSessionMeta ? { _meta: options.initialSessionMeta } : {}), + ...(options.sessionCustomizations ? { customizations: [...options.sessionCustomizations()] } : {}), + }, undefined); + } + override getSessionSummary(session: string) { + if (options?.initialSessionMeta && session === sessionUri.toString()) { + const now = new Date().toISOString(); + return { + resource: session, + provider: 'copilot', + title: 'Test session', + status: SessionStatus.Idle, + createdAt: now, + modifiedAt: now, + _meta: options.initialSessionMeta, + }; + } + return super.getSessionSummary(session); } }(new NullLogService())); services.set(IAgentHostStateManager, stateManager); @@ -1890,6 +1937,26 @@ suite('CopilotAgentSession', () => { }); }); + test('restores non-Opus prompt cache expiration from usage metrics on initialize', async () => { + const cacheExpiresAt = '2026-07-24T12:00:00.000Z'; + const { dispatchedActions } = await createAgentSession(disposables, { + resume: true, + configureMockSession: session => { + session.usageMetricsResult.currentModel = 'gpt-5.4'; + session.usageMetricsResult.modelMetrics['gpt-5.4'] = { cacheExpiresAt }; + }, + }); + + const promptCaches = dispatchedActions + .filter(action => action.type === ActionType.SessionMetaChanged) + .map(action => readSessionPromptCacheState(action._meta)) + .filter(cache => cache !== undefined); + assert.deepStrictEqual(promptCaches, [{ + modelId: 'gpt-5.4', + cacheExpiresAt, + }]); + }); + test('updates prompt cache expiration from main-agent usage only', async () => { const { mockSession, dispatchedActions } = await createAgentSession(disposables); mockSession.fire('assistant.usage', { @@ -1905,6 +1972,7 @@ suite('CopilotAgentSession', () => { cacheExpiresAt: '2026-07-24T12:10:00.000Z', parentToolCallId: 'subagent-tool-call', }); + await timeout(0); const promptCaches = dispatchedActions .filter(action => action.type === ActionType.SessionMetaChanged) @@ -1916,7 +1984,56 @@ suite('CopilotAgentSession', () => { }]); }); - test('clears prompt cache expiration when the main agent model does not report one', async () => { + test('preserves prompt cache expiration when later usage omits a cache update', async () => { + const { mockSession, dispatchedActions } = await createAgentSession(disposables); + mockSession.fire('assistant.usage', { + model: 'claude-sonnet-4.6', + inputTokens: 100, + outputTokens: 10, + cacheExpiresAt: '2026-07-24T12:00:00.000Z', + }); + await timeout(0); + mockSession.fire('assistant.usage', { + model: 'claude-sonnet-4.6', + inputTokens: 50, + outputTokens: 5, + }); + await timeout(0); + + const promptCaches = dispatchedActions + .filter(action => action.type === ActionType.SessionMetaChanged) + .map(action => readSessionPromptCacheState(action._meta)) + .filter(cache => cache !== undefined); + assert.deepStrictEqual(promptCaches, [{ + modelId: 'claude-sonnet-4.6', + cacheExpiresAt: '2026-07-24T12:00:00.000Z', + }]); + }); + + test('preserves restored prompt cache metadata after a resume metrics failure', async () => { + const cacheExpiresAt = '2026-07-24T12:00:00.000Z'; + const initialSessionMeta = withSessionPromptCacheState(undefined, { modelId: 'claude-sonnet-4.6', cacheExpiresAt }); + assert.ok(initialSessionMeta); + const { mockSession, dispatchedActions } = await createAgentSession(disposables, { + resume: true, + initialSessionMeta, + configureMockSession: session => { + session.usageMetricsResult.currentModel = 'claude-sonnet-4.6'; + session.usageMetricsResult.modelMetrics['claude-sonnet-4.6'] = { cacheExpiresAt }; + session.usageMetricsError = new Error('rpc unavailable'); + }, + }); + mockSession.fire('assistant.usage', { + model: 'claude-sonnet-4.6', + inputTokens: 50, + outputTokens: 5, + }); + await timeout(0); + + assert.deepStrictEqual(dispatchedActions.filter(action => action.type === ActionType.SessionMetaChanged), []); + }); + + test('clears prompt cache expiration when switching to a model without cached state', async () => { const { mockSession, dispatchedActions } = await createAgentSession(disposables); mockSession.fire('assistant.usage', { model: 'claude-opus-4.8', @@ -1924,11 +2041,62 @@ suite('CopilotAgentSession', () => { outputTokens: 10, cacheExpiresAt: '2026-07-24T12:00:00.000Z', }); + mockSession.fire('session.model_change', { + previousModel: 'claude-opus-4.8', + newModel: 'gpt-5.4', + }); + await timeout(0); + + const promptCaches = dispatchedActions + .filter(action => action.type === ActionType.SessionMetaChanged) + .map(action => readSessionPromptCacheState(action._meta)); + assert.deepStrictEqual(promptCaches, [{ + modelId: 'claude-opus-4.8', + cacheExpiresAt: '2026-07-24T12:00:00.000Z', + }, undefined]); + }); + + test('preserves prompt cache expiration for same-model configuration changes', async () => { + const { mockSession, dispatchedActions } = await createAgentSession(disposables); + mockSession.fire('assistant.usage', { + model: 'claude-opus-4.8', + inputTokens: 100, + outputTokens: 10, + cacheExpiresAt: '2026-07-24T12:00:00.000Z', + }); + await timeout(0); + mockSession.fire('session.model_change', { + previousModel: 'claude-opus-4.8', + newModel: 'claude-opus-4.8', + reasoningEffort: 'high', + }); + await timeout(0); + + const promptCaches = dispatchedActions + .filter(action => action.type === ActionType.SessionMetaChanged) + .map(action => readSessionPromptCacheState(action._meta)); + assert.deepStrictEqual(promptCaches, [{ + modelId: 'claude-opus-4.8', + cacheExpiresAt: '2026-07-24T12:00:00.000Z', + }]); + }); + + test('clears another model cache when a usage metrics refresh fails', async () => { + const { mockSession, dispatchedActions } = await createAgentSession(disposables); + mockSession.fire('assistant.usage', { + model: 'claude-opus-4.8', + inputTokens: 100, + outputTokens: 10, + cacheExpiresAt: '2026-07-24T12:00:00.000Z', + }); + await timeout(0); + mockSession.usageMetricsError = new Error('rpc unavailable'); mockSession.fire('assistant.usage', { model: 'gpt-5.4', inputTokens: 50, outputTokens: 5, }); + await timeout(0); const promptCaches = dispatchedActions .filter(action => action.type === ActionType.SessionMetaChanged) diff --git a/src/vs/platform/agentHost/test/node/e2e/providers/__snapshots__/Agent_Host_E2E___Copilot__Copilot-specific__client_tool_reaches_ready_after_start_and_completes.traffic.ahp.yaml b/src/vs/platform/agentHost/test/node/e2e/providers/__snapshots__/Agent_Host_E2E___Copilot__Copilot-specific__client_tool_reaches_ready_after_start_and_completes.traffic.ahp.yaml index a82a43c2b4d..a105f202a42 100644 --- a/src/vs/platform/agentHost/test/node/e2e/providers/__snapshots__/Agent_Host_E2E___Copilot__Copilot-specific__client_tool_reaches_ready_after_start_and_completes.traffic.ahp.yaml +++ b/src/vs/platform/agentHost/test/node/e2e/providers/__snapshots__/Agent_Host_E2E___Copilot__Copilot-specific__client_tool_reaches_ready_after_start_and_completes.traffic.ahp.yaml @@ -67,9 +67,6 @@ rounds: - channel: ${session_0} action: type: session/changesetsChanged - - channel: ${session_0} - action: - type: session/metaChanged - channel: ${chat_0} action: type: chat/usage @@ -169,9 +166,6 @@ rounds: part: kind: markdown content: 'The magic word is: **XYLOPHONE**' - - channel: ${session_0} - action: - type: session/metaChanged - channel: ${chat_0} action: type: chat/usage diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostPromptCacheNotification.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostPromptCacheNotification.ts index 1fe1763af12..49427d123f2 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostPromptCacheNotification.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostPromptCacheNotification.ts @@ -17,7 +17,6 @@ import { IWorkbenchAssignmentService } from '../../../../../services/assignment/ import { ChatInputNotificationActionKind, ChatInputNotificationSeverity, IChatInputNotificationService } from '../../widget/input/chatInputNotificationService.js'; const PROMPT_CACHE_EXPIRATION_NOTIFICATION_EXPERIMENT = 'copilotchat.promptCacheExpirationNotification'; -const PROMPT_CACHE_EXPIRATION_GRACE_PERIOD_MS = 10 * 60 * 1000; const PROMPT_CACHE_EXPIRATION_DISABLED_STORAGE_KEY = 'chat.promptCacheExpirationNotification.disabled'; const DISABLE_PROMPT_CACHE_EXPIRATION_NOTIFICATION_COMMAND = 'workbench.action.chat.disablePromptCacheExpirationNotification'; const PROMPT_CACHE_EXPIRATION_LEARN_MORE_URL = 'https://code.visualstudio.com/docs/agents/agent-troubleshooting/cache-explorer#_why-prompt-caching-matters'; @@ -74,9 +73,9 @@ export class AgentHostPromptCacheNotification extends Disposable { this._cacheExpirations.set(sessionResource, promptCache.cacheExpiresAt); const expirationTime = Date.parse(promptCache.cacheExpiresAt); if (Number.isFinite(expirationTime)) { - const remainingTime = expirationTime + PROMPT_CACHE_EXPIRATION_GRACE_PERIOD_MS - Date.now(); - if (remainingTime >= 0) { - expirationScheduler.schedule(remainingTime + 1); + const remainingTime = expirationTime - Date.now(); + if (remainingTime > 0) { + expirationScheduler.schedule(remainingTime); } } } else { @@ -99,7 +98,7 @@ export class AgentHostPromptCacheNotification extends Disposable { const cacheExpiresAt = this._cacheExpirations.get(sessionResource); const expirationTime = cacheExpiresAt ? Date.parse(cacheExpiresAt) : Number.NaN; const disabled = this._storageService.getBoolean(PROMPT_CACHE_EXPIRATION_DISABLED_STORAGE_KEY, StorageScope.PROFILE, false); - if (!this._experimentEnabled || disabled || !Number.isFinite(expirationTime) || Date.now() <= expirationTime + PROMPT_CACHE_EXPIRATION_GRACE_PERIOD_MS) { + if (!this._experimentEnabled || disabled || !Number.isFinite(expirationTime) || Date.now() < expirationTime) { this._notificationService.deleteNotification(this._notificationId(sessionResource)); return; } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostPromptCacheNotification.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostPromptCacheNotification.test.ts index 128321b08b4..d1bc1381b78 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostPromptCacheNotification.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostPromptCacheNotification.test.ts @@ -51,7 +51,7 @@ suite('AgentHostPromptCacheNotification', () => { clock.restore(); }); - test('does not show before ten minutes after expiration', async () => { + test('does not show before expiration', async () => { const clock = sinon.useFakeTimers({ now: new Date('2026-07-24T12:00:00.000Z') }); const notificationService = new TestNotificationService(); const contribution = store.add(new AgentHostPromptCacheNotification( @@ -66,12 +66,12 @@ suite('AgentHostPromptCacheNotification', () => { await Promise.resolve(); assert.strictEqual(notificationService.notifications.size, 0); - subscription.setValue(createState('2026-07-24T11:49:59.999Z')); + subscription.setValue(createState('2026-07-24T11:59:59.999Z')); assert.strictEqual(notificationService.notifications.size, 1); clock.restore(); }); - test('shows immediately after the ten-minute boundary', async () => { + test('shows at the expiration boundary', async () => { const clock = sinon.useFakeTimers({ now: new Date('2026-07-24T12:00:00.000Z') }); const notificationService = new TestNotificationService(); const contribution = store.add(new AgentHostPromptCacheNotification( @@ -85,7 +85,7 @@ suite('AgentHostPromptCacheNotification', () => { store.add(contribution.trackSession(sessionResource, subscription)); await Promise.resolve(); - await clock.tickAsync(11 * 60 * 1000); + await clock.tickAsync(60 * 1000 - 1); assert.strictEqual(notificationService.notifications.size, 0); await clock.tickAsync(1); assert.strictEqual(notificationService.notifications.size, 1); From 71e54e81597d5dbde34d0035329c9a6668b8f568 Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci <39117631+Giuspepe@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:16:51 +0200 Subject: [PATCH 67/86] sessions: don't lose pins and groups when an agent can't list its sessions (#328408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent host aggregates one `listSessions` result across all of its agents. An agent that isn't ready yet returned an empty list rather than failing, so a startup auth race produced a listing that looked complete but was missing whole agents. `_refreshSessions` then evicted every cached session absent from it and fired `removed`, which `SessionGroupsService` and `SessionsListModelService` treated as a deletion — permanently erasing the user's group membership, pins and manual sort keys. The sessions returned on the next launch, but ungrouped and unpinned. Three changes: - Durable UI state is discarded only on a definitive delete (`onDidDeleteSession`) or archive, never on `onDidChangeSessions.removed`, which is an eviction and can be transient. - `CodexAgent.listSessions` rejects with `AHP_AUTH_REQUIRED` instead of returning `[]` while its GitHub token is still landing. The workbench already handles a rejected listing correctly: no eviction, the cache is not marked authoritative, and a backoff retry heals it. This also fixes a latent bug where an `openai` usage source could never list sessions. - `_refreshSessions` no longer evicts an agent's sessions when that agent contributed no rows while others did — that namespace is unknown, not empty. A wholly empty listing still prunes, preserving the existing stale-hydrated-entry cleanup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/sessions/SKILL.md | 4 + .../agentHost/node/codex/codexAgent.ts | 9 ++- src/vs/sessions/SESSIONS_LIST.md | 2 + .../browser/baseAgentHostSessionsProvider.ts | 19 +++++ .../localAgentHostSessionsProvider.test.ts | 81 +++++++++++++++++++ .../sessions/browser/sessionGroupsService.ts | 14 +++- .../browser/sessionsListModelService.ts | 11 ++- .../test/browser/sessionGroupsService.test.ts | 25 +++++- .../browser/sessionsListModelService.test.ts | 38 ++++++--- 9 files changed, 180 insertions(+), 23 deletions(-) diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index 07553d89e79..ee7fc07040d 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -110,6 +110,10 @@ Whenever the user flags a wrong pattern, rejects an approach, or gives design/ru - **Definitive session deletion and temporary list eviction are different operations**: deletion clears durable provenance and pending state; filtering a still-existing session only removes its visible list entry. Keep the list-removal helper side-effect-free, and let each caller explicitly update its mutation generation instead of passing an "already incremented" boolean. +- **Durable user intent must never be discarded on `onDidChangeSessions.removed`**: pins, manual sort keys, and group membership (`SessionsListModelService`, `SessionGroupsService`) are cleared only on `ISessionsManagementService.onDidDeleteSession` (or archive), never on the provider's `removed` delta. `removed` is an *eviction*, not a deletion: `BaseAgentHostSessionsProvider._refreshSessions` reconciles against one listing that the host aggregates across all its agents, and an agent that cannot answer yet returns `[]` instead of failing (`CodexAgent.listSessions` returns `[]` for a missing `_githubToken`, a not-yet-downloaded SDK, or a failed `thread/list`; `ClaudeAgent.listSessions` does the same). Persisting the removal turned a ~300 ms startup race into permanent loss of the user's pins and groups. Runtime-only consumers of `removed` (terminals, grid slots, layout) are fine as-is — only *persisted* state needs the delete event. + +- **`_refreshSessions` must not evict a cached session whose agent contributed no rows**: a listing with zero rows for an agent means "unknown", not "empty", so scope eviction to `listedAgentProviders` (the set of `AgentSession.provider(...)` schemes actually present in the response) and compare against `adapter.agentProvider`. Real deletions still arrive through `deleteSessions` and the `sessionRemoved` notification; the only cost is that an agent's *last* session, deleted elsewhere, lingers until it lists something again. + - **Keep session-list refresh filtering linear**: when retention pruning needs the complete backend key set, collect those keys while filtering entries in the original loop, then reconcile last-seen/pruning afterward. Do not introduce a candidate-map/filter/map pipeline when one loop plus one reconciliation call expresses the lifecycle more clearly. - **Centralize session workspace filtering behind a semantic predicate**: refresh, add-notification, and summary-update paths should call one `_isSessionInWorkspace(entry)`-style helper. Keep key construction, working-directory parsing, pending-local lookup, and provenance checks out of each caller so the high-level list flow stays readable and all paths apply identical rules. diff --git a/src/vs/platform/agentHost/node/codex/codexAgent.ts b/src/vs/platform/agentHost/node/codex/codexAgent.ts index 3efdb32e4a0..a57baa3e6e3 100644 --- a/src/vs/platform/agentHost/node/codex/codexAgent.ts +++ b/src/vs/platform/agentHost/node/codex/codexAgent.ts @@ -3858,9 +3858,12 @@ export class CodexAgent extends Disposable implements IAgent { } async listSessions(): Promise<IAgentSessionMetadata[]> { - if (!this._githubToken) { - return []; - } + // Reject rather than reporting an empty list while the GitHub token is + // still landing: the workbench treats a successful listing as the + // authoritative session set and would evict — and permanently unpin and + // ungroup — every Codex session. A rejection instead leaves the cached + // list intact and self-heals through the caller's backoff retry. + this._ensureAuthenticated(); // 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 diff --git a/src/vs/sessions/SESSIONS_LIST.md b/src/vs/sessions/SESSIONS_LIST.md index 95693e620be..dcfdc18120e 100644 --- a/src/vs/sessions/SESSIONS_LIST.md +++ b/src/vs/sessions/SESSIONS_LIST.md @@ -66,6 +66,8 @@ User groups are **fully user-managed**: their order is owned by `ISessionSection Archived sessions always go to the "Done" section regardless of grouping mode. Archive wins over pin — an archived session is never shown in Pinned — and archiving removes the session from any user-created group. This cleanup also applies when an archived session is added by a provider and when persisted group state loads. Restoring the session does not restore its former group membership. +Group membership, pin state, and manual sort keys are **durable user intent**: they are discarded only when a session is *definitively deleted* (`ISessionsManagementService.onDidDeleteSession`) or archived — never when a session merely drops out of a provider's list. A provider can evict sessions transiently (an agent host aggregates one listing across several agents, and an agent whose auth token or SDK is still loading contributes an empty list), so treating `onDidChangeSessions.removed` as a deletion would permanently destroy grouping and pins for sessions that return on the next refresh. The trade-off is that a session deleted from another window leaves a stale membership/pin entry behind; those entries match no session and are inert. + The experimental `chat.experimental.sessionArchiveActionWording` setting keeps archive actions consistent across the regular workbench and Agents window. The `archive` variant uses **Archive**, **Archive All**, **Unarchive**, and **Unarchive All** with `Codicon.archive`/`Codicon.unarchive`; the `done` variant uses **Mark as Done**, **Mark All as Done**, **Restore**, and **Restore All** with `Codicon.check`/`Codicon.checkAll`/`Codicon.redo`. Confirmation copy follows the selected vocabulary, while the underlying archived state and the "Done" section remain unchanged. ### Sorting diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts index b2edd275664..8f19e424974 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -4422,6 +4422,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement this._cacheInitialized = true; this._sessionRefreshRetryDelay = BaseAgentHostSessionsProvider.SESSION_REFRESH_RETRY_MIN_MS; const currentKeys = new Set<string>(); + const listedAgentProviders = new Set<string>(); const added: ISession[] = []; const changed: ISession[] = []; @@ -4429,6 +4430,10 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement const meta = this._adoptSessionMeta(rawMeta); const rawId = AgentSession.id(meta.session); currentKeys.add(rawId); + const agentProvider = AgentSession.provider(meta.session); + if (agentProvider) { + listedAgentProviders.add(agentProvider); + } const existing = this._sessionCache.get(rawId); if (existing) { @@ -4449,11 +4454,25 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement // Some hosts briefly omit the just-sent eager session from listSessions. // Keep the pending session visible until sendRequest graduates it. const pendingRawId = this._pendingSession?.resource.path.replace(/^\//, ''); + // The host aggregates one listing across all of its agents, and an + // agent that cannot enumerate yet (its SDK is not downloaded) can + // contribute an empty list rather than failing. When other agents + // did answer, a namespace with no row at all is therefore *unknown* + // rather than empty, and evicting it would be a silent data loss — + // `removed` discards the user's pins and group membership. A wholly + // empty listing keeps the authoritative-empty contract, since an + // agent that cannot answer at all rejects (and we never get here). + // Real deletions still arrive through `deleteSessions` and the + // `sessionRemoved` notification. + const evictUnlistedAgents = listedAgentProviders.size === 0; for (const [key, cached] of this._sessionCache) { if (!currentKeys.has(key)) { if (key === pendingRawId) { continue; } + if (!evictUnlistedAgents && !listedAgentProviders.has(cached.agentProvider)) { + continue; + } this._sessionCache.delete(key); this._runningSessionConfigs.delete(cached.sessionId); this._runningSessionConfigResolveSeq.delete(cached.sessionId); 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 973a4497d46..4c9e3d8f5d8 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 @@ -204,6 +204,18 @@ class MockAgentHostService extends mock<IAgentHostService>() { this._sessions.set(AgentSession.id(meta.session), meta); } + /** + * Drop a session from what `listSessions()` reports, without going through + * `disposeSession`. Simulates an agent that cannot enumerate its sessions + * yet (auth token or SDK still loading) and so contributes nothing to the + * host's aggregated listing. + */ + stopListingSessions(...ids: string[]): void { + for (const id of ids) { + this._sessions.delete(id); + } + } + // ---- Session-state subscriptions --------------------------------------- private readonly _sessionStateEmitters = new Map<string, Emitter<SubscriptionState>>(); @@ -1093,6 +1105,75 @@ suite('LocalAgentHostSessionsProvider', () => { }); })); + test('a session whose agent reports nothing survives the refresh', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => { + // The host aggregates one listing across all of its agents, and an + // agent that cannot enumerate yet (SDK not downloaded) contributes an + // empty list instead of failing. Codex going quiet must not evict its + // sessions: `removed` is treated as a definitive deletion downstream + // and would discard the user's pins and groups. + agentHost.setAgents([ + { provider: 'copilotcli', displayName: 'Copilot', description: '', models: [] } as AgentInfo, + { provider: 'codex', displayName: 'Codex', description: '', models: [] } as AgentInfo, + ]); + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration(AgentHostCodexAgentEnabledSettingId, true); + agentHost.addSession(createSession('codex-1', { provider: 'codex', summary: 'Codex One' })); + agentHost.addSession(createSession('cli-1', { provider: 'copilotcli', summary: 'CLI One' })); + + const provider = createProvider(disposables, agentHost, undefined, { configurationService }); + await timeout(0); + + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions(e => changes.push(e))); + + agentHost.stopListingSessions('codex-1'); + agentHost.fireAction({ + channel: buildDefaultChatUri(AgentSession.uri('copilotcli', 'cli-1').toString()), + action: { type: ActionType.ChatTurnComplete }, + serverSeq: 1, + origin: undefined, + } as ActionEnvelope); + await timeout(0); + + assert.deepStrictEqual({ + removed: changes.flatMap(c => c.removed.map(s => s.title.get())), + cachedTitles: provider.getSessions().map(s => s.title.get()).sort(), + }, { + removed: [], + cachedTitles: ['CLI One', 'Codex One'], + }); + })); + + test('a session missing while its agent still reports others is evicted', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => { + // The agent answered and listed a sibling session, so its namespace is + // known: the missing session really is gone and must be evicted. + agentHost.addSession(createSession('cli-gone', { provider: 'copilotcli', summary: 'Gone' })); + agentHost.addSession(createSession('cli-kept', { provider: 'copilotcli', summary: 'Kept' })); + + const provider = createProvider(disposables, agentHost); + await timeout(0); + + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions(e => changes.push(e))); + + agentHost.stopListingSessions('cli-gone'); + agentHost.fireAction({ + channel: buildDefaultChatUri(AgentSession.uri('copilotcli', 'cli-kept').toString()), + action: { type: ActionType.ChatTurnComplete }, + serverSeq: 1, + origin: undefined, + } as ActionEnvelope); + await timeout(0); + + assert.deepStrictEqual({ + removed: changes.flatMap(c => c.removed.map(s => s.title.get())), + cachedTitles: provider.getSessions().map(s => s.title.get()).sort(), + }, { + removed: ['Gone'], + cachedTitles: ['Kept'], + }); + })); + test('a successful empty listSessions arms no retry', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => { // No sessions on the host: listSessions() succeeds with []. This is a // valid result, not a failure — the cache should be marked initialized diff --git a/src/vs/sessions/services/sessions/browser/sessionGroupsService.ts b/src/vs/sessions/services/sessions/browser/sessionGroupsService.ts index 390d002ac96..da12752602e 100644 --- a/src/vs/sessions/services/sessions/browser/sessionGroupsService.ts +++ b/src/vs/sessions/services/sessions/browser/sessionGroupsService.ts @@ -147,14 +147,16 @@ export class SessionGroupsService extends Disposable implements ISessionGroupsSe this.save(); } + // A session dropping out of the provider's list is an eviction, not a + // deletion — an agent that cannot answer `listSessions` yet reports no + // sessions, so its sessions disappear until the next refresh. Clearing + // membership here would turn that transient gap into a permanent, + // unrecoverable loss of the user's grouping. this._register(this.sessionsManagementService.onDidChangeSessions(e => { - const changed = new Set<string>(); for (const session of e.removed) { this._inFlightSessionGroups.delete(session.sessionId); - if (this._membership.delete(session.sessionId)) { - changed.add(session.sessionId); - } } + const changed = new Set<string>(); this.removeArchivedMembership(e.added, changed); this.removeArchivedMembership(e.changed, changed); if (changed.size > 0) { @@ -163,6 +165,10 @@ export class SessionGroupsService extends Disposable implements ISessionGroupsSe } })); + this._register(this.sessionsManagementService.onDidDeleteSession(session => { + this.removeFromGroup(session.sessionId); + })); + this._register(this.sessionsManagementService.onDidArchiveSession(session => { this.removeFromGroup(session.sessionId); })); diff --git a/src/vs/sessions/services/sessions/browser/sessionsListModelService.ts b/src/vs/sessions/services/sessions/browser/sessionsListModelService.ts index 315152efb48..0c9f07baa86 100644 --- a/src/vs/sessions/services/sessions/browser/sessionsListModelService.ts +++ b/src/vs/sessions/services/sessions/browser/sessionsListModelService.ts @@ -128,10 +128,13 @@ export class SessionsListModelService extends Disposable implements ISessionsLis this._legacyReadSessionIds = legacyRead.size > 0 ? legacyRead : undefined; this._migratedReadSessionIds = this.loadSet(SessionsListModelService.READ_MIGRATION_DONE_KEY); - this._register(this.sessionsManagementService.onDidChangeSessions(e => { - for (const session of e.removed) { - this.deleteSession(session); - } + // Only a definitive deletion discards pin and sort state. A session + // merely dropping out of the provider's list is an eviction (e.g. an + // agent that cannot answer `listSessions` yet reports no sessions), and + // discarding state there would permanently unpin sessions that come + // back on the next refresh. + this._register(this.sessionsManagementService.onDidDeleteSession(session => { + this.deleteSession(session); })); } diff --git a/src/vs/sessions/services/sessions/test/browser/sessionGroupsService.test.ts b/src/vs/sessions/services/sessions/test/browser/sessionGroupsService.test.ts index ee7ae1885eb..79dc1928d21 100644 --- a/src/vs/sessions/services/sessions/test/browser/sessionGroupsService.test.ts +++ b/src/vs/sessions/services/sessions/test/browser/sessionGroupsService.test.ts @@ -53,6 +53,7 @@ suite('SessionGroupsService', () => { let sessionStartedEmitter: Emitter<ISession>; let sessionArchivedEmitter: Emitter<ISession>; let sessionUnarchivedEmitter: Emitter<ISession>; + let sessionDeletedEmitter: Emitter<ISession>; let sessionReplacedEmitter: Emitter<{ readonly from: ISession; readonly to: ISession }>; let newSessionDiscardedEmitter: Emitter<ISession>; let instantiationService: TestInstantiationService; @@ -76,6 +77,7 @@ suite('SessionGroupsService', () => { sessionStartedEmitter = disposables.add(new Emitter<ISession>()); sessionArchivedEmitter = disposables.add(new Emitter<ISession>()); sessionUnarchivedEmitter = disposables.add(new Emitter<ISession>()); + sessionDeletedEmitter = disposables.add(new Emitter<ISession>()); sessionReplacedEmitter = disposables.add(new Emitter<{ readonly from: ISession; readonly to: ISession }>()); newSessionDiscardedEmitter = disposables.add(new Emitter<ISession>()); sessions = []; @@ -87,6 +89,7 @@ suite('SessionGroupsService', () => { onDidStartSession: sessionStartedEmitter.event, onDidArchiveSession: sessionArchivedEmitter.event, onDidUnarchiveSession: sessionUnarchivedEmitter.event, + onDidDeleteSession: sessionDeletedEmitter.event, onDidReplaceSession: sessionReplacedEmitter.event, onDidDiscardNewSession: newSessionDiscardedEmitter.event, }); @@ -159,10 +162,10 @@ suite('SessionGroupsService', () => { assert.strictEqual(service.getGroupOfSession('s2'), undefined); }); - test('membership is cleaned up when a session is removed', () => { + test('membership is cleaned up when a session is deleted', () => { const a = service.createGroup('A', ['s1', 's2']); const session = createSession('s1'); - sessionsChangedEmitter.fire({ added: [], removed: [session], changed: [] }); + sessionDeletedEmitter.fire(session); assert.deepStrictEqual({ groupName: service.getGroup(a.id)?.name, @@ -175,6 +178,24 @@ suite('SessionGroupsService', () => { }); }); + test('membership survives a session being evicted from the provider list', () => { + const a = service.createGroup('A', ['s1', 's2']); + const session = createSession('s1'); + + // An agent that cannot answer `listSessions` yet reports no sessions, + // so the list evicts them until the next refresh. That must not drop + // the user's grouping. + sessionsChangedEmitter.fire({ added: [], removed: [session], changed: [] }); + + assert.deepStrictEqual({ + membership: service.getGroupOfSession('s1'), + remainingMembers: service.getSessionIdsInGroup(a.id).sort(), + }, { + membership: a.id, + remainingMembers: ['s1', 's2'], + }); + }); + test('archiving the last member leaves an empty group', () => { const a = service.createGroup('A', ['s1']); diff --git a/src/vs/sessions/services/sessions/test/browser/sessionsListModelService.test.ts b/src/vs/sessions/services/sessions/test/browser/sessionsListModelService.test.ts index 5c541b59d72..11d4ca4360c 100644 --- a/src/vs/sessions/services/sessions/test/browser/sessionsListModelService.test.ts +++ b/src/vs/sessions/services/sessions/test/browser/sessionsListModelService.test.ts @@ -48,14 +48,17 @@ suite('SessionsListModelService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let service: SessionsListModelService; let sessionsChangedEmitter: Emitter<ISessionsChangeEvent>; + let sessionDeletedEmitter: Emitter<ISession>; setup(() => { const instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(IStorageService, disposables.add(new InMemoryStorageService())); sessionsChangedEmitter = disposables.add(new Emitter<ISessionsChangeEvent>()); + sessionDeletedEmitter = disposables.add(new Emitter<ISession>()); instantiationService.stub(ISessionsManagementService, { ...mock<ISessionsManagementService>(), onDidChangeSessions: sessionsChangedEmitter.event, + onDidDeleteSession: sessionDeletedEmitter.event, }); service = disposables.add(instantiationService.createInstance(SessionsListModelService)); }); @@ -157,14 +160,14 @@ suite('SessionsListModelService', () => { // -- Cleanup -- - test('cleans up state when session is removed', () => { + test('cleans up state when session is deleted', () => { const session = createSession('s1'); service.pinSession(session); const events: ISessionListModelChangeEvent[] = []; disposables.add(service.onDidChange(e => events.push(e))); - sessionsChangedEmitter.fire({ added: [], removed: [session], changed: [] }); + sessionDeletedEmitter.fire(session); assert.strictEqual(service.isSessionPinned(session), false); assert.deepStrictEqual(events, [ @@ -172,23 +175,38 @@ suite('SessionsListModelService', () => { ]); }); - test('removal does not fire when session has no state', () => { + test('pin survives a session being evicted from the provider list', () => { + const session = createSession('s1'); + service.pinSession(session); + + let changeCount = 0; + disposables.add(service.onDidChange(() => changeCount++)); + + // An agent that cannot answer `listSessions` yet reports no sessions, + // so the list evicts them until the next refresh. That must not unpin. + sessionsChangedEmitter.fire({ added: [], removed: [session], changed: [] }); + + assert.strictEqual(service.isSessionPinned(session), true); + assert.strictEqual(changeCount, 0); + }); + + test('deletion does not fire when session has no state', () => { const session = createSession('s1'); let changeCount = 0; disposables.add(service.onDidChange(() => changeCount++)); - sessionsChangedEmitter.fire({ added: [], removed: [session], changed: [] }); + sessionDeletedEmitter.fire(session); assert.strictEqual(changeCount, 0); }); - test('removal does not affect other sessions', () => { + test('deletion does not affect other sessions', () => { const s1 = createSession('s1'); const s2 = createSession('s2'); service.pinSession(s1); service.pinSession(s2); - sessionsChangedEmitter.fire({ added: [], removed: [s1], changed: [] }); + sessionDeletedEmitter.fire(s1); assert.strictEqual(service.isSessionPinned(s1), false); assert.strictEqual(service.isSessionPinned(s2), true); @@ -204,7 +222,7 @@ suite('SessionsListModelService', () => { const instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(IStorageService, storageService); - instantiationService.stub(ISessionsManagementService, { ...mock<ISessionsManagementService>(), onDidChangeSessions: disposables.add(new Emitter<ISessionsChangeEvent>()).event }); + instantiationService.stub(ISessionsManagementService, { ...mock<ISessionsManagementService>(), onDidDeleteSession: disposables.add(new Emitter<ISession>()).event }); const loadedService = disposables.add(instantiationService.createInstance(SessionsListModelService)); assert.strictEqual(loadedService.isSessionPinned(createSession('s1')), true); @@ -217,7 +235,7 @@ suite('SessionsListModelService', () => { const instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(IStorageService, storageService); - instantiationService.stub(ISessionsManagementService, { ...mock<ISessionsManagementService>(), onDidChangeSessions: disposables.add(new Emitter<ISessionsChangeEvent>()).event }); + instantiationService.stub(ISessionsManagementService, { ...mock<ISessionsManagementService>(), onDidDeleteSession: disposables.add(new Emitter<ISession>()).event }); const loadedService = disposables.add(instantiationService.createInstance(SessionsListModelService)); // Should not throw and should return empty state @@ -244,7 +262,7 @@ suite('SessionsListModelService', () => { instantiationService.stub(IStorageService, storage); instantiationService.stub(ISessionsManagementService, { ...mock<ISessionsManagementService>(), - onDidChangeSessions: disposables.add(new Emitter<ISessionsChangeEvent>()).event, + onDidDeleteSession: disposables.add(new Emitter<ISession>()).event, markRead: async (session: ISession) => { readMarks.push(session.sessionId); }, markUnread: async (session: ISession) => { unreadMarks.push(session.sessionId); }, }); @@ -299,7 +317,7 @@ suite('SessionsListModelService', () => { instantiationService.stub(IStorageService, storage); instantiationService.stub(ISessionsManagementService, { ...mock<ISessionsManagementService>(), - onDidChangeSessions: disposables.add(new Emitter<ISessionsChangeEvent>()).event, + onDidDeleteSession: disposables.add(new Emitter<ISession>()).event, markRead: async (session: ISession) => { readMarks.push(session.sessionId); }, markUnread: async (session: ISession) => { unreadMarks.push(session.sessionId); }, }); From 3d1fee0cc033905b0ab38fb26e6864801d6e4ecf Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev <ulugbekna@gmail.com> Date: Fri, 31 Jul 2026 18:18:31 +0500 Subject: [PATCH 68/86] nes: preserve ghost-text state for speculative reuse (#328406) nes: fix: preserve ghost-text state for speculative reuse Keep speculative next-edit results linked to their stable cache entry so suggestions shown as ghost text remain suppressible when reused in another view kind. Add coverage for speculative reuse, rebasing, and cache replacement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 93fb295d-2415-4079-b354-784417ad06f7 --- .../inlineEdits/node/nextEditProvider.ts | 2 +- .../test/node/nextEditCacheRebase.spec.ts | 59 +++++++++++++++++++ .../node/nextEditProviderSpeculative.spec.ts | 40 +++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts b/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts index d38deaa1e2a..6b0b20e9e7a 100644 --- a/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts @@ -701,7 +701,7 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne telemetryBuilder.setStatelessNextEditTelemetry(nextEditResult.telemetry); if (speculativeRequest) { const firstEdit = await requestToReuse.firstEdit.p; - return firstEdit.map(val => ({ ...val, isFromSpeculativeRequest: true })); + return firstEdit.map(val => ({ ...val, isFromSpeculativeRequest: true, baseCacheEntry: val.baseCacheEntry ?? val })); } return nextEditResult.nextEdit.isError() ? nextEditResult.nextEdit : requestToReuse.firstEdit.p; } else { diff --git a/extensions/copilot/src/extension/inlineEdits/test/node/nextEditCacheRebase.spec.ts b/extensions/copilot/src/extension/inlineEdits/test/node/nextEditCacheRebase.spec.ts index 0ad096870de..d0c8867b8dc 100644 --- a/extensions/copilot/src/extension/inlineEdits/test/node/nextEditCacheRebase.spec.ts +++ b/extensions/copilot/src/extension/inlineEdits/test/node/nextEditCacheRebase.spec.ts @@ -170,6 +170,7 @@ describe('NextEditCache rebase — Fibonacci scenario', () => { assert(cachedEdit !== undefined, 'setKthNextEdit should return the cached edit'); assert(cachedEdit.userEditSince !== undefined, 'userEditSince should be set'); + cachedEdit.wasRenderedAsInlineSuggestion = true; const rebaseResult = cache.tryRebaseCacheEntry( cachedEdit, @@ -180,6 +181,64 @@ describe('NextEditCache rebase — Fibonacci scenario', () => { assert(rebaseResult.edit !== undefined, 'should rebase successfully'); assert(rebaseResult.edit.rebasedEdit !== undefined, 'should have a rebased edit for the class body'); assert.strictEqual(rebaseResult.edit.modelTelemetry, testModelTelemetry, 'should preserve model attribution on the rebased edit'); + const baseCacheEntry = rebaseResult.edit.baseCacheEntry; + assert(baseCacheEntry, 'should reference the stable cache entry'); + assert.strictEqual(baseCacheEntry, cachedEdit); + assert.strictEqual(baseCacheEntry.wasRenderedAsInlineSuggestion, true, 'should preserve inline-rendered state on the stable cache entry'); + }); +}); + +describe('NextEditCache ghost-text presentation state', () => { + + const document = new StringText('const value = 1;\n'); + const docId = DocumentId.create(URI.file('/test/cache-presentation-state.ts').toString()); + + function makeSource(): NextEditFetchRequest { + const logContext = new InlineEditRequestLogContext('test', 0, undefined); + return new NextEditFetchRequest(generateUuid(), logContext, undefined, false); + } + + it('does not carry inline-rendered state to a replacement cache entry', () => { + const workspace = new MutableObservableWorkspace(); + workspace.addDocument({ id: docId, initialValue: document.value }); + const cache = new NextEditCache(workspace, new LogServiceImpl([]), new DefaultsOnlyConfigurationService(), new NullExperimentationService()); + + const first = cache.setKthNextEdit( + docId, + document, + undefined, + StringReplacement.insert(document.value.length, 'first'), + 0, + undefined, + undefined, + makeSource(), + { isFromCursorJump: false, modelTelemetry: testModelTelemetry }, + ); + assert(first); + first.wasRenderedAsInlineSuggestion = true; + + const replacement = cache.setKthNextEdit( + docId, + document, + undefined, + StringReplacement.insert(document.value.length, 'replacement'), + 0, + undefined, + undefined, + makeSource(), + { isFromCursorJump: false, modelTelemetry: testModelTelemetry }, + ); + const result = cache.lookupNextEdit(docId, document, [OffsetRange.emptyAt(document.value.length)]); + + assert.deepStrictEqual({ + isReplacementEntry: result === replacement, + newText: result?.edit?.newText, + wasRenderedAsInlineSuggestion: result?.wasRenderedAsInlineSuggestion, + }, { + isReplacementEntry: true, + newText: 'replacement', + wasRenderedAsInlineSuggestion: undefined, + }); }); }); diff --git a/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderSpeculative.spec.ts b/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderSpeculative.spec.ts index 6e72d67baa7..0a94294d73f 100644 --- a/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderSpeculative.spec.ts +++ b/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderSpeculative.spec.ts @@ -627,6 +627,46 @@ describe('NextEditProvider speculative requests', () => { await statelessProvider.calls[1].completed.p; }); + it('reused speculative request preserves inline-rendered state on the cached entry', async () => { + await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On); + + const statelessProvider = new TestStatelessNextEditProvider(); + statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') }); + const specContinue = new DeferredPromise<void>(); + statelessProvider.enqueueBehavior({ kind: 'yieldEditThenWait', edit: lineReplacement(2, 'console.log(value + 1);'), continueSignal: specContinue }); + const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider); + + const doc = workspace.addDocument({ + id: DocumentId.create(URI.file('/test/spec-cache-entry-identity.ts').toString()), + initialValue: 'const value = 1;\nconsole.log(value);', + }); + doc.setSelection([new OffsetRange(0, 0)], undefined); + + const firstSuggestion = await getNextEdit(nextEditProvider, doc.id); + assert(firstSuggestion.result?.edit); + nextEditProvider.handleShown(firstSuggestion); + await statelessProvider.waitForCall(2); + nextEditProvider.handleAcceptance(doc.id, firstSuggestion); + doc.applyEdit(firstSuggestion.result.edit.toEdit()); + + const speculativeSuggestion = await getNextEdit(nextEditProvider, doc.id); + assert(speculativeSuggestion.result?.cacheEntry); + speculativeSuggestion.result.cacheEntry.wasRenderedAsInlineSuggestion = true; + + specContinue.complete(); + await statelessProvider.calls[1].completed.p; + + const cachedSuggestion = await getNextEdit(nextEditProvider, doc.id); + assert(cachedSuggestion.result?.cacheEntry); + expect({ + isSameCacheEntry: cachedSuggestion.result.cacheEntry === speculativeSuggestion.result.cacheEntry, + wasRenderedAsInlineSuggestion: cachedSuggestion.result.cacheEntry.wasRenderedAsInlineSuggestion, + }).toEqual({ + isSameCacheEntry: true, + wasRenderedAsInlineSuggestion: true, + }); + }); + it('skips cache delay for edits from speculative requests even when enforceCacheDelay is true', async () => { const CACHE_DELAY_MS = 5_000; await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On); From c90abb2b382575318375dd47bd0d8c909674e19d Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev <ulugbekna@gmail.com> Date: Fri, 31 Jul 2026 18:27:08 +0500 Subject: [PATCH 69/86] sessions: fix: put agent-created sessions in correct repository (#328375) * agentHost: fix: group linked-worktree sessions by repository Resolve the primary Git worktree as the canonical repository identity while preserving the selected checkout as the source for ignored include files. Migrate persisted repository metadata for sessions created before the fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f3a1b843-be4c-447d-8aa6-8289d6aa760e * agentHost: fix: skip probes for missing session worktrees Use persisted repository metadata directly when cleanup has removed an archived session's working directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f3a1b843-be4c-447d-8aa6-8289d6aa760e --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f3a1b843-be4c-447d-8aa6-8289d6aa760e --- .../agentHost/common/agentHostGitService.ts | 51 ++++++++ .../platform/agentHost/node/agentService.ts | 54 +++++++-- .../node/copilot/copilotGitProject.ts | 7 +- .../node/shared/worktreeIsolation.ts | 36 +++++- .../agentHost/test/node/agentService.test.ts | 40 ++++++- .../test/node/copilotGitProject.test.ts | 28 ++++- .../node/shared/worktreeIsolation.test.ts | 110 +++++++++++++++++- 7 files changed, 302 insertions(+), 24 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHostGitService.ts b/src/vs/platform/agentHost/common/agentHostGitService.ts index 3e0f459fc5c..705b1334633 100644 --- a/src/vs/platform/agentHost/common/agentHostGitService.ts +++ b/src/vs/platform/agentHost/common/agentHostGitService.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Sequencer } from '../../../base/common/async.js'; import { VSBuffer } from '../../../base/common/buffer.js'; +import { LRUCache } from '../../../base/common/map.js'; import { URI } from '../../../base/common/uri.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { ISessionFileDiff, ISessionGitState } from './state/sessionState.js'; @@ -91,6 +93,54 @@ export interface IPullOptions { export const IAgentHostGitService = createDecorator<IAgentHostGitService>('agentHostGitService'); +/** + * Resolves linked checkouts to their primary worktree and caches successful mappings for every worktree reported by Git. + * Resolution is serialized so concurrent requests across linked checkouts share one probe, while empty results remain retryable. + */ +class PrimaryWorktreeRootResolver { + private readonly _roots = new LRUCache<string, URI>(100); + private readonly _sequencer = new Sequencer(); + + constructor(private readonly _gitService: IAgentHostGitService) { } + + async resolve(checkoutRoot: URI): Promise<URI | undefined> { + const key = checkoutRoot.toString(); + const cached = this._roots.get(key); + if (cached) { + return cached; + } + return this._sequencer.queue(async () => { + const cached = this._roots.get(key); + if (cached) { + return cached; + } + const roots = await this._gitService.getWorktreeRoots(checkoutRoot); + const primaryRoot = roots[0]; + if (!primaryRoot) { + return undefined; + } + this._roots.set(key, primaryRoot); + for (const root of roots) { + this._roots.set(root.toString(), primaryRoot); + } + return primaryRoot; + }); + } +} + +/** Resolver lifetime follows the injected Git service; each resolver owns a bounded path cache. */ +const primaryWorktreeRootResolvers = new WeakMap<IAgentHostGitService, PrimaryWorktreeRootResolver>(); + +/** Resolves the primary worktree root when Git reports a worktree listing. */ +export function tryResolvePrimaryWorktreeRoot(gitService: IAgentHostGitService, checkoutRoot: URI): Promise<URI | undefined> { + let resolver = primaryWorktreeRootResolvers.get(gitService); + if (!resolver) { + resolver = new PrimaryWorktreeRootResolver(gitService); + primaryWorktreeRootResolvers.set(gitService, resolver); + } + return resolver.resolve(checkoutRoot); +} + export interface IRefQuery { readonly count?: number; readonly pattern?: string | string[]; @@ -156,6 +206,7 @@ export interface IAgentHostGitService { getBranches(workingDirectory: URI, query?: IRefQuery): Promise<Branch[]>; getBranch(workingDirectory: URI, name: string): Promise<Branch | undefined>; getRepositoryRoot(workingDirectory: URI): Promise<URI | undefined>; + /** Returns worktree roots in Git's porcelain order, with the primary worktree first. */ getWorktreeRoots(workingDirectory: URI): Promise<URI[]>; /** * Creates a worktree for a new branch. `onProgress` receives every checkout diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 5ee95e9fd1e..2a0a0f3a1ce 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -9,7 +9,7 @@ import { DeferredPromise, disposableTimeout, ResourceQueue } from '../../../base import { toErrorMessage } from '../../../base/common/errorMessage.js'; import { Emitter } from '../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableResourceMap, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../base/common/map.js'; +import { LRUCache, ResourceMap } from '../../../base/common/map.js'; import { getExtensionForMimeType, getMediaMime } from '../../../base/common/mime.js'; import { Schemas } from '../../../base/common/network.js'; import { IObservable, observableValue } from '../../../base/common/observable.js'; @@ -23,7 +23,7 @@ import { InstantiationService } from '../../instantiation/common/instantiationSe import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; import { ILogService } from '../../log/common/log.js'; import { AgentProvider, AgentSession, AgentSignal, AgentHostSessionReleaseGraceMsEnvVar, IAgent, IAgentChatDataChange, IAgentCreateChatOptions, IAgentCreateChatResult, IAgentCreateChatSideChatSelection, IAgentCreateChatSideChatSource, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentHostAuthTokenRequest, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkEndpoint, IAgentHostNetworkFetchResult, IAgentMaterializeSessionEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSpawnChatEvent, AuthenticateParams, AuthenticateResult, IMcpNotification, IRestoredSubagentSession, SubagentChatSignal } from '../common/agentService.js'; -import { ISessionDataService, SESSION_ATTACHMENTS_DIRNAME } from '../common/sessionDataService.js'; +import { type ISessionDatabase, ISessionDataService, SESSION_ATTACHMENTS_DIRNAME } from '../common/sessionDataService.js'; import { IAgentEditAttributionService, ICancelEditAttributionFlushParams, ICommitEditAttributionFlushParams, IEditAttributionFlushResult, IPrepareEditAttributionFlushParams, IPreparedEditAttributionFlush, parseEditAttributionResource } from '../common/fileEditAttribution.js'; import { SessionConfigKey } from '../common/sessionConfigKeys.js'; import type { IAgentCustomizationSettingsRegistration } from '../common/agentCustomizationSettings.js'; @@ -43,7 +43,7 @@ import { AgentHostTerminalManager, IAgentHostTerminalManager } from './agentHost import { ISessionDbUriFields, parseSessionDbUri } from '../common/sessionDbUri.js'; import { IGitBlobUriFields, parseGitBlobUri } from './gitDiffContent.js'; import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js'; -import { IAgentHostGitService } from '../common/agentHostGitService.js'; +import { IAgentHostGitService, tryResolvePrimaryWorktreeRoot } from '../common/agentHostGitService.js'; import { AgentSideEffects } from './agentSideEffects.js'; import { AgentHostLocalTurns } from './agentHostLocalTurns.js'; import { AgentServerToolHost } from './shared/agentServerToolHost.js'; @@ -301,6 +301,8 @@ export class AgentService extends Disposable implements IAgentService { * agents stay unaware of the folder-vs-worktree distinction. */ private _worktree: WorktreeIsolation | undefined; + /** Successful list-time repository-root resolutions; eviction only causes safe re-resolution. */ + private readonly _normalizedWorktreeRepositoryRoots = new LRUCache<string, URI>(100); /** Single source of truth for GitHub (Enterprise) endpoints and protected resources. */ private readonly _gitHubEndpointService: IAgentHostGitHubEndpointService; /** Pluggable completion item providers (e.g. workspace file completions, agent-specific @-mentions). */ @@ -831,6 +833,41 @@ export class AgentService extends Disposable implements IAgentService { }; } + /** + * Repairs repository roots written by older builds that treated a parent linked checkout as the repository. + * Listing performs this migration because archived sessions may never resume through WorktreeIsolation's metadata reader. + */ + private async _normalizeListedWorktreeRepositoryRoot(session: IAgentSessionMetadata, database: ISessionDatabase, repositoryRootRaw: string): Promise<string> { + const storedRepositoryRootRaw = repositoryRootRaw; + const persistedRoot = URI.parse(repositoryRootRaw); + const sessionStr = session.session.toString(); + let primaryRoot = this._normalizedWorktreeRepositoryRoots.get(sessionStr); + if (!primaryRoot) { + const workingDirectory = session.workingDirectories?.[0]; + const checkoutRoot = workingDirectory && await this._fileExistsSafe(workingDirectory) ? workingDirectory : persistedRoot; + try { + primaryRoot = await tryResolvePrimaryWorktreeRoot(this._gitService, checkoutRoot) + ?? (checkoutRoot.toString() !== persistedRoot.toString() ? await tryResolvePrimaryWorktreeRoot(this._gitService, persistedRoot) : undefined); + if (primaryRoot) { + this._normalizedWorktreeRepositoryRoots.set(sessionStr, primaryRoot); + } + } catch (error) { + this._logService.warn(`[AgentService][listSessions] Failed to resolve primary worktree for ${session.session}`, error); + } + } + if (primaryRoot) { + repositoryRootRaw = primaryRoot.toString(); + } + if (repositoryRootRaw !== storedRepositoryRootRaw) { + try { + await database.setMetadata(WORKTREE_META_REPOSITORY_ROOT, repositoryRootRaw); + } catch (error) { + this._logService.warn(`[AgentService][listSessions] Failed to normalize worktree repository metadata for ${session.session}`, error); + } + } + return repositoryRootRaw; + } + async listSessions(): Promise<IAgentSessionMetadata[]> { this._logService.trace('[AgentService] listSessions called'); const results = await Promise.all( @@ -899,12 +936,11 @@ export class AgentService extends Disposable implements IAgentService { updated = { ...updated, _meta: withSessionWorkspaceless(updated._meta, m[AH_META_WORKSPACELESS_DB_KEY] === 'true') }; } - // Worktree-isolated sessions run out of `<repo>.worktrees/<name>` but - // must group under the repository in the sessions UI. Merge the repo - // project persisted alongside the worktree metadata so a list refresh - // doesn't revert the workspace name to the worktree directory. No-op - // for folder sessions (key absent). - const worktreeProject = worktreeProjectFromRepositoryRoot(m[WORKTREE_META_REPOSITORY_ROOT]); + let repositoryRootRaw = m[WORKTREE_META_REPOSITORY_ROOT]; + if (repositoryRootRaw) { + repositoryRootRaw = await this._normalizeListedWorktreeRepositoryRoot(updated, ref.object, repositoryRootRaw); + } + const worktreeProject = worktreeProjectFromRepositoryRoot(repositoryRootRaw); if (worktreeProject) { updated = { ...updated, project: worktreeProject }; } diff --git a/src/vs/platform/agentHost/node/copilot/copilotGitProject.ts b/src/vs/platform/agentHost/node/copilot/copilotGitProject.ts index 5f08a309877..74553a7fa00 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotGitProject.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotGitProject.ts @@ -7,7 +7,7 @@ import { Schemas } from '../../../../base/common/network.js'; import { basename } from '../../../../base/common/path.js'; import { URI } from '../../../../base/common/uri.js'; import type { IAgentSessionProjectInfo } from '../../common/agentService.js'; -import type { IAgentHostGitService } from '../../common/agentHostGitService.js'; +import { tryResolvePrimaryWorktreeRoot, type IAgentHostGitService } from '../../common/agentHostGitService.js'; export interface ICopilotSessionContext { readonly cwd?: string; @@ -25,10 +25,7 @@ export async function resolveGitProject(workingDirectory: URI | undefined, gitSe return undefined; } - const uri = (await gitService.getWorktreeRoots(workingDirectory))[0] ?? repositoryRoot; - if (!uri) { - return undefined; - } + const uri = await tryResolvePrimaryWorktreeRoot(gitService, repositoryRoot) ?? repositoryRoot; return { uri, displayName: basename(uri.fsPath) || uri.toString() }; } diff --git a/src/vs/platform/agentHost/node/shared/worktreeIsolation.ts b/src/vs/platform/agentHost/node/shared/worktreeIsolation.ts index 544fe5a5e21..22bdef3015a 100644 --- a/src/vs/platform/agentHost/node/shared/worktreeIsolation.ts +++ b/src/vs/platform/agentHost/node/shared/worktreeIsolation.ts @@ -14,7 +14,7 @@ import { generateUuid } from '../../../../base/common/uuid.js'; import { localize } from '../../../../nls.js'; import { ILogService } from '../../../log/common/log.js'; import { IAgentSessionProjectInfo } from '../../common/agentService.js'; -import { getBranchCompletions, IAgentHostGitService, IDefaultBranch, IWorktreeFileProgress, META_DIFF_BASE_BRANCH } from '../../common/agentHostGitService.js'; +import { getBranchCompletions, IAgentHostGitService, IDefaultBranch, IWorktreeFileProgress, META_DIFF_BASE_BRANCH, tryResolvePrimaryWorktreeRoot } from '../../common/agentHostGitService.js'; import { ISchemaProperty, schemaProperty } from '../../common/agentHostSchema.js'; import { ISessionDataService } from '../../common/sessionDataService.js'; import { SessionConfigKey } from '../../common/sessionConfigKeys.js'; @@ -518,11 +518,12 @@ export class WorktreeIsolation extends Disposable { onProgress?.(buildWorktreeProgressText(WorktreeCreationPhase.Starting)); - const repositoryRoot = await this._gitService.getRepositoryRoot(workingDirectory); - if (!repositoryRoot) { + const checkoutRoot = await this._gitService.getRepositoryRoot(workingDirectory); + if (!checkoutRoot) { return workingDirectory; } + const repositoryRoot = await this._resolvePrimaryWorktreeRoot(checkoutRoot, checkoutRoot); const worktreesRoot = getWorktreesRoot(repositoryRoot); // Prefix (e.g. the user's `git.branchPrefix`) the client forwards for // worktree-isolated sessions. Prepended ahead of the built-in `agents/` @@ -568,7 +569,7 @@ export class WorktreeIsolation extends Disposable { try { onProgress?.(buildWorktreeProgressText(WorktreeCreationPhase.CopyingIncludeFiles)); await withPercentProgress(WorktreeCreationPhase.CopyingIncludeFiles, onProgress, progress => - this._gitService.copyWorktreeIncludeFiles(repositoryRoot, worktree, worktreeIncludeFiles, progress)); + this._gitService.copyWorktreeIncludeFiles(checkoutRoot, worktree, worktreeIncludeFiles, progress)); } catch (error) { this._logService.warn(`[${this._logLabel}:${sessionId}] Failed to copy worktree include files: ${errorMessage(error)}`); } @@ -818,6 +819,15 @@ export class WorktreeIsolation extends Disposable { return meta?.repositoryRoot ? projectFromRepositoryRoot(meta.repositoryRoot) : undefined; } + private async _resolvePrimaryWorktreeRoot(checkoutRoot: URI, fallbackRoot: URI): Promise<URI> { + try { + return await tryResolvePrimaryWorktreeRoot(this._gitService, checkoutRoot) ?? fallbackRoot; + } catch (error) { + this._logService.warn(`[${this._logLabel}] Failed to resolve primary worktree for '${checkoutRoot.fsPath}': ${errorMessage(error)}`); + return fallbackRoot; + } + } + /** * Synchronous companion to {@link resolveWorktreeProject} for the * materialize-event path: the repository project for a worktree this agent @@ -872,6 +882,10 @@ export class WorktreeIsolation extends Disposable { } } + /** + * Reads worktree metadata and migrates repository roots written before linked checkouts were canonicalized. + * It probes an existing worktree when available and otherwise falls back to the persisted root for archived sessions. + */ private async _readWorktreeMetadata(sessionUri: URI): Promise<{ branchName: string; worktreePath?: URI; repositoryRoot?: URI } | undefined> { const ref = await this._sessionDataService.tryOpenDatabase(sessionUri); if (!ref) { @@ -887,7 +901,19 @@ export class WorktreeIsolation extends Disposable { return undefined; } const worktreePath = worktreePathRaw ? URI.parse(worktreePathRaw) : undefined; - const repositoryRoot = repositoryRootRaw ? URI.parse(repositoryRootRaw) : undefined; + let repositoryRoot = repositoryRootRaw ? URI.parse(repositoryRootRaw) : undefined; + if (repositoryRoot) { + const checkoutRoot = worktreePath && await fileExists(worktreePath.fsPath) ? worktreePath : repositoryRoot; + const primaryRoot = await this._resolvePrimaryWorktreeRoot(checkoutRoot, repositoryRoot); + if (primaryRoot.toString() !== repositoryRoot.toString()) { + repositoryRoot = primaryRoot; + try { + await ref.object.setMetadata(WORKTREE_META_REPOSITORY_ROOT, primaryRoot.toString()); + } catch (error) { + this._logService.warn(`[${this._logLabel}] Failed to normalize worktree repository metadata for '${sessionUri.toString()}': ${errorMessage(error)}`); + } + } + } return { branchName, worktreePath, repositoryRoot }; } finally { ref.dispose(); diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index 44469852b41..06a6951907c 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -38,7 +38,7 @@ import { type ISessionEvent } from './copilotTestEvents.js'; import { createNoopGitService, createSessionDataService, TestSessionDatabase } from '../common/sessionTestHelpers.js'; import { buildSessionChangesetUri, buildUncommittedChangesetUri } from '../../common/changesetUri.js'; import { type ICopilotApiService, type ICopilotApiServiceRequestOptions, type ICopilotUtilityChatCompletionRequest } from '../../node/shared/copilotApiService.js'; -import { WorktreeIsolation } from '../../node/shared/worktreeIsolation.js'; +import { WorktreeIsolation, WORKTREE_META_REPOSITORY_ROOT } from '../../node/shared/worktreeIsolation.js'; import { AhpErrorCodes, JSON_RPC_INTERNAL_ERROR, ProtocolError } from '../../common/state/sessionProtocol.js'; import type { INetworkDiagnosticsService } from '../../node/networkDiagnosticsService.js'; @@ -1319,6 +1319,44 @@ suite('AgentService (node dispatcher)', () => { assert.deepStrictEqual(sessions[0]._meta, { workspaceless: true }); }); + test('listSessions normalizes a persisted linked-worktree project without probing a missing session worktree', async () => { + const db = disposables.add(new TestSessionDatabase()); + const primaryRoot = URI.file('/workspace/vscode'); + const linkedCheckout = URI.file('/workspace/vscode.worktrees/parent'); + const sessionWorktree = URI.file('/workspace/vscode.worktrees/parent.worktrees/child'); + await db.setMetadata(WORKTREE_META_REPOSITORY_ROOT, linkedCheckout.toString()); + const sessionId = 'test-session-linked-worktree'; + const sessionUri = AgentSession.uri('copilot', sessionId); + const agent = new MockAgent('copilot'); + disposables.add(toDisposable(() => agent.dispose())); + agent.sessionMetadataOverrides = { + workingDirectories: [sessionWorktree], + project: { uri: linkedCheckout, displayName: 'parent' }, + }; + (agent as unknown as { _sessions: Map<string, URI> })._sessions.set(sessionId, sessionUri); + const gitService = createNoopGitService(); + const resolvedFrom: URI[] = []; + gitService.getWorktreeRoots = async workingDirectory => { + resolvedFrom.push(workingDirectory); + return [primaryRoot, linkedCheckout, sessionWorktree]; + }; + const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, gitService)); + svc.registerProvider(agent); + + const sessions = await svc.listSessions(); + await svc.listSessions(); + + assert.deepStrictEqual({ + resolvedFrom: resolvedFrom.map(uri => uri.toString()), + project: sessions[0].project && { uri: sessions[0].project.uri.toString(), displayName: sessions[0].project.displayName }, + persistedRepositoryRoot: await db.getMetadata(WORKTREE_META_REPOSITORY_ROOT), + }, { + resolvedFrom: [linkedCheckout.toString()], + project: { uri: primaryRoot.toString(), displayName: 'vscode' }, + persistedRepositoryRoot: primaryRoot.toString(), + }); + }); + test('listSessions uses SDK title when no custom title exists', async () => { service.registerProvider(copilotAgent); copilotAgent.sessionMetadataOverrides = { summary: 'Auto-generated Title' }; diff --git a/src/vs/platform/agentHost/test/node/copilotGitProject.test.ts b/src/vs/platform/agentHost/test/node/copilotGitProject.test.ts index d319a570f34..d0d85dd7526 100644 --- a/src/vs/platform/agentHost/test/node/copilotGitProject.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotGitProject.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 type { IAgentHostGitService, IBranch, IDefaultBranch } from '../../common/agentHostGitService.js'; +import { tryResolvePrimaryWorktreeRoot, type IAgentHostGitService, type IBranch, type IDefaultBranch } from '../../common/agentHostGitService.js'; import { projectFromCopilotContext, projectFromRepository, resolveGitProject } from '../../node/copilot/copilotGitProject.js'; class TestAgentHostGitService implements IAgentHostGitService { @@ -14,6 +14,7 @@ class TestAgentHostGitService implements IAgentHostGitService { repositoryRoot: URI | undefined; worktreeRoots: URI[] = []; + worktreeRootCalls = 0; async getCurrentBranch(): Promise<string | undefined> { return undefined; } async getDefaultBranch(): Promise<IDefaultBranch | undefined> { return undefined; } @@ -21,7 +22,10 @@ class TestAgentHostGitService implements IAgentHostGitService { async getRefs(): Promise<IBranch[]> { return []; } async getBranches(): Promise<IBranch[]> { return []; } async getRepositoryRoot(): Promise<URI | undefined> { return this.repositoryRoot; } - async getWorktreeRoots(): Promise<URI[]> { return this.worktreeRoots; } + async getWorktreeRoots(): Promise<URI[]> { + this.worktreeRootCalls++; + return this.worktreeRoots; + } async addWorktree(): Promise<void> { } async copyWorktreeIncludeFiles(): Promise<void> { } async addExistingWorktree(): Promise<void> { } @@ -89,6 +93,26 @@ suite('Copilot Git Project', () => { }); }); + test('deduplicates concurrent resolution across linked worktrees', async () => { + const primaryRoot = URI.file('/workspace/source-repo'); + const checkoutA = URI.file('/workspace/source-repo.worktrees/a'); + const checkoutB = URI.file('/workspace/source-repo.worktrees/b'); + gitService.worktreeRoots = [primaryRoot, checkoutA, checkoutB]; + + const roots = await Promise.all([ + tryResolvePrimaryWorktreeRoot(gitService, checkoutA), + tryResolvePrimaryWorktreeRoot(gitService, checkoutB), + ]); + + assert.deepStrictEqual({ + worktreeRootCalls: gitService.worktreeRootCalls, + roots: roots.map(root => root?.toString()), + }, { + worktreeRootCalls: 1, + roots: [primaryRoot.toString(), primaryRoot.toString()], + }); + }); + test('returns undefined outside a git working tree', async () => { assert.strictEqual(await resolveGitProject(URI.file('/workspace/plain-folder'), gitService), undefined); }); diff --git a/src/vs/platform/agentHost/test/node/shared/worktreeIsolation.test.ts b/src/vs/platform/agentHost/test/node/shared/worktreeIsolation.test.ts index 1dd0124c50f..e126d5f6660 100644 --- a/src/vs/platform/agentHost/test/node/shared/worktreeIsolation.test.ts +++ b/src/vs/platform/agentHost/test/node/shared/worktreeIsolation.test.ts @@ -240,6 +240,73 @@ suite('WorktreeIsolation', () => { }); }); + test('resolveWorkingDirectory creates from the primary worktree while copying include files from the selected checkout', async () => { + const checkoutRoot = URI.joinPath(repoRoot, 'linked-checkout'); + const gitService = createGitService(); + let addWorktreeRoot: URI | undefined; + gitService.getRepositoryRoot = async () => checkoutRoot; + gitService.getWorktreeRoots = async () => [repoRoot, checkoutRoot]; + gitService.addWorktree = async (repositoryRoot, worktree, branch, startPoint, track) => { + addWorktreeRoot = repositoryRoot; + addWorktreeCalls.push({ worktree, branchName: branch, startPoint, track }); + mkdirSync(worktree.fsPath, { recursive: true }); + }; + const isolation = createIsolation(disposables, { gitService }); + const includeFiles = ['.env']; + + const worktree = await isolation.resolveWorkingDirectory({ + sessionUri, + sessionId, + workingDirectory: checkoutRoot, + config: { + [SessionConfigKey.Isolation]: 'worktree', + [SessionConfigKey.Branch]: 'main', + [SessionConfigKey.WorktreeIncludeFiles]: includeFiles, + }, + }); + const meta = await isolation.readWorktreeMetadata(sessionUri); + const project = isolation.createdWorktreeProject(sessionId); + + assert.deepStrictEqual({ + worktree: worktree?.toString(), + addWorktreeRoot: addWorktreeRoot?.toString(), + includeFileRoot: copyIncludeCalls[0]?.repositoryRoot.toString(), + metaRepositoryRoot: meta?.repositoryRoot?.toString(), + project: project && { uri: project.uri.toString(), displayName: project.displayName }, + }, { + worktree: URI.joinPath(worktreesRoot, getWorktreeName(branchName)).toString(), + addWorktreeRoot: repoRoot.toString(), + includeFileRoot: checkoutRoot.toString(), + metaRepositoryRoot: repoRoot.toString(), + project: { uri: repoRoot.toString(), displayName: basename(repoRoot) }, + }); + }); + + test('resolveWorkingDirectory falls back to the selected checkout when primary worktree resolution fails', async () => { + const checkoutRoot = URI.joinPath(repoRoot, 'linked-checkout'); + const gitService = createGitService(); + gitService.getRepositoryRoot = async () => checkoutRoot; + gitService.getWorktreeRoots = async () => { throw new Error('worktree enumeration failed'); }; + const isolation = createIsolation(disposables, { gitService }); + + const worktree = await isolation.resolveWorkingDirectory({ + sessionUri, + sessionId, + workingDirectory: checkoutRoot, + config: { [SessionConfigKey.Isolation]: 'worktree', [SessionConfigKey.Branch]: 'main' }, + }); + const meta = await isolation.readWorktreeMetadata(sessionUri); + const fallbackWorktreesRoot = getWorktreesRoot(checkoutRoot); + + assert.deepStrictEqual({ + worktree: worktree?.toString(), + metaRepositoryRoot: meta?.repositoryRoot?.toString(), + }, { + worktree: URI.joinPath(fallbackWorktreesRoot, getWorktreeName(branchName)).toString(), + metaRepositoryRoot: checkoutRoot.toString(), + }); + }); + test('resolveWorkingDirectory names each creation phase, rounding percentages down and debouncing updates', async () => { const gitService = createGitService(); gitService.addWorktree = async (_root, worktree, branch, startPoint, track, onProgress) => { @@ -346,9 +413,13 @@ suite('WorktreeIsolation', () => { test('resolveWorkingDirectory serializes concurrent creation in the same repository', async () => { const gitService = createGitService(); + const checkoutRootA = URI.joinPath(repoRoot, 'linked-checkout-a'); + const checkoutRootB = URI.joinPath(repoRoot, 'linked-checkout-b'); const existingBranches = new Set<string>(); let activeAddWorktrees = 0; let maxActiveAddWorktrees = 0; + gitService.getRepositoryRoot = async workingDirectory => workingDirectory; + gitService.getWorktreeRoots = async () => [repoRoot, checkoutRootA, checkoutRootB]; gitService.branchExists = async (_repositoryRoot, candidate) => existingBranches.has(candidate); gitService.addWorktree = async (_repositoryRoot, worktree, candidate, startPoint, track) => { activeAddWorktrees++; @@ -366,8 +437,8 @@ suite('WorktreeIsolation', () => { const config = { [SessionConfigKey.Isolation]: 'worktree', [SessionConfigKey.Branch]: 'main' }; const worktrees = await Promise.all([ - isolation.resolveWorkingDirectory({ sessionUri: URI.parse('agent-session://test/12345678-aaaa-bbbb-cccc-123456789abc'), sessionId: '12345678-aaaa-bbbb-cccc-123456789abc', workingDirectory: repoRoot, config, prompt: 'Add feature' }), - isolation.resolveWorkingDirectory({ sessionUri: URI.parse('agent-session://test/87654321-aaaa-bbbb-cccc-123456789abc'), sessionId: '87654321-aaaa-bbbb-cccc-123456789abc', workingDirectory: repoRoot, config, prompt: 'Add feature' }), + isolation.resolveWorkingDirectory({ sessionUri: URI.parse('agent-session://test/12345678-aaaa-bbbb-cccc-123456789abc'), sessionId: '12345678-aaaa-bbbb-cccc-123456789abc', workingDirectory: checkoutRootA, config, prompt: 'Add feature' }), + isolation.resolveWorkingDirectory({ sessionUri: URI.parse('agent-session://test/87654321-aaaa-bbbb-cccc-123456789abc'), sessionId: '87654321-aaaa-bbbb-cccc-123456789abc', workingDirectory: checkoutRootB, config, prompt: 'Add feature' }), ]); assert.deepStrictEqual({ @@ -573,6 +644,41 @@ suite('WorktreeIsolation', () => { }); }); + test('resolveWorktreeProject normalizes persisted linked-checkout metadata', async () => { + const checkoutRoot = URI.joinPath(repoRoot, 'linked-checkout'); + const existingWorktree = URI.joinPath(repoRoot, 'existing-worktree'); + mkdirSync(existingWorktree.fsPath, { recursive: true }); + await Promise.all([ + db.setMetadata('copilot.worktree.branchName', 'feature/x'), + db.setMetadata('copilot.worktree.path', existingWorktree.toString()), + db.setMetadata('copilot.worktree.repositoryRoot', checkoutRoot.toString()), + ]); + const gitService = createGitService(); + let resolvedFrom: URI | undefined; + let resolutionCount = 0; + gitService.getWorktreeRoots = async workingDirectory => { + resolvedFrom = workingDirectory; + resolutionCount++; + return [repoRoot, checkoutRoot, existingWorktree]; + }; + const isolation = createIsolation(disposables, { gitService }); + + const project = await isolation.resolveWorktreeProject(sessionUri); + await isolation.resolveWorktreeProject(sessionUri); + + assert.deepStrictEqual({ + resolutionCount, + resolvedFrom: resolvedFrom?.toString(), + project: project && { uri: project.uri.toString(), displayName: project.displayName }, + persistedRepositoryRoot: await db.getMetadata('copilot.worktree.repositoryRoot'), + }, { + resolutionCount: 1, + resolvedFrom: existingWorktree.toString(), + project: { uri: repoRoot.toString(), displayName: basename(repoRoot) }, + persistedRepositoryRoot: repoRoot.toString(), + }); + }); + test('applyRestoreAnnouncement prepends a markdown part when worktree metadata exists', async () => { const isolation = createIsolation(disposables); const turn: Turn = { From 3e4479f418c031a4aaab2bb822b172a7600ba5d1 Mon Sep 17 00:00:00 2001 From: Logan Ramos <lramos15@gmail.com> Date: Fri, 31 Jul 2026 09:34:35 -0400 Subject: [PATCH 70/86] Fix input box warnings not adjusting on resize (#328412) --- src/vs/base/browser/ui/inputbox/inputBox.ts | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index d8c041c95ac..465101843c4 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -117,6 +117,7 @@ export class InputBox extends Widget { private maxHeight: number = Number.POSITIVE_INFINITY; private scrollableElement: ScrollableElement | undefined; private readonly hover: MutableDisposable<IDisposable> = this._register(new MutableDisposable()); + private readonly messageResizeObserver: MutableDisposable<IDisposable> = this._register(new MutableDisposable()); private _onDidChange = this._register(new Emitter<string>()); public get onDidChange(): Event<string> { return this._onDidChange.event; } @@ -527,10 +528,13 @@ export class InputBox extends Widget { }, onHide: () => { this.state = 'closed'; + this.messageResizeObserver.clear(); }, layout: layout }); + this.observeElementResize(); + // ARIA Support let alertText: string; if (this.message.type === MessageType.ERROR) { @@ -555,9 +559,27 @@ export class InputBox extends Widget { this.contextViewProvider.hideContextView(); } + this.messageResizeObserver.clear(); this.state = 'idle'; } + /** + * Keeps the validation message sized and anchored to the input while the + * message is showing and the input itself is resized, e.g. because the + * containing view was resized. + */ + private observeElementResize(): void { + const observer = new dom.DisposableResizeObserver('InputBox.validationMessage', () => { + // Ignore notifications for a hidden or detached input, laying out + // against a degenerate anchor would move the message to the corner. + if (this.element.isConnected && dom.getTotalWidth(this.element) > 0) { + this.layoutMessage(); + } + }, dom.getWindow(this.element)); + observer.observe(this.element); + this.messageResizeObserver.value = observer; + } + private layoutMessage(): void { if (this.state === 'open' && this.contextViewProvider) { this.contextViewProvider.layout(); From ac673890c28dbb7364538ee2fb4d8e69af1ff6a0 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:39:46 +0200 Subject: [PATCH 71/86] Respect editor associations for chat file pills (#328415) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/sessions/SKILL.md | 1 + .../chat/browser/sessionChatInputToolbar.ts | 8 +--- .../browser/widget/chatArtifactsWidget.ts | 2 +- .../chatInlineAnchorWidget.ts | 18 +------- .../chatContentParts/chatTurnPillsPart.ts | 27 +++++------ .../browser/widget/chatEditorAssociations.ts | 23 ++++++++++ .../chat/browser/widget/chatTurnPills.ts | 42 ++++++++--------- .../test/browser/widget/chatTurnPills.test.ts | 45 +++++++++++++++++++ 8 files changed, 103 insertions(+), 63 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/chatEditorAssociations.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/widget/chatTurnPills.test.ts diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index ee7fc07040d..69bdc2b009b 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -97,6 +97,7 @@ Then read the relevant spec for the area you are changing (see table below). If - **A reused new-session composer must re-seed its workspace draft when it swaps out of quick-chat mode**: the session-type picker hides itself when it has no folder types (`sessionTypePicker` `_folderSessionTypes.length === 0`), which is the case whenever the composer has **no active session** (`refresh(undefined)` clears the types). A *freshly opened* new-session composer avoids this by seeding a workspace draft from the restored folder in its constructor — but the same `NewChatWidget` instance is **reused** across the quick-chat→new-session transition (`sessionView.ts` keeps `kind==='newSession'`), and Cmd+N's `openNewSession` discard branch only `_activate(undefined)`, leaving the reused composer session-less → picker hidden. Fix by re-running the constructor's seed (`_seedWorkspaceDraft()`) from an autorun when `_isQuickChatComposer` flips **true→false with no active session**, so the reused composer matches a fresh one (folder + visible picker). Don't assume the constructor-time restore covers a reused composer. - **Every untitled-session-title fallback must be quick-chat aware**: an untitled session's title observable is `''`, so a hardcoded `localize(…, "New Session")` fallback shows "New Session" even for a quick chat (whose composer says "New Chat"). Route **all** such fallbacks through the shared `getUntitledSessionTitle(isQuickChat)` helper (`services/sessions/common/session.ts`, boolean param so each caller controls reader-tracked `.read(reader)` vs `.get()`). There are ≥5 sites — titlebar (`sessionsTitleBarWidget`), session header (×2: title + rename placeholder), list-row hover (`sessionHoverContent`), sessions picker (`sessionsActions`) — keep them on the helper; never hardcode "New Session". (The Cmd+N *action* title stays "New Session" — that action creates a session, unrelated to a session's own title.) - **`NeedsInput` is still an active turn for live turn UI**: agent-host tool and input confirmations intentionally transition a running chat from `InProgress` to `NeedsInput` without ending `activeTurn`. Live status surfaces such as the chat input pills must use `isActiveSessionStatus` so they do not disappear until the next output returns the chat to `InProgress`. +- **Chat file pills must not hardcode an editor**: open their resource through the shared `chat.editorAssociations` resolution path so both completed-response pills and the session input toolbar respect the configured editor. Do not invoke `markdown.showPreview` directly. - **Agent-host-only exclusions for built-in client tools belong in `ClientToolSetsContribution`, not the global tool registration**: `AgentHostActiveClientService.getClientTools` advertises enabled members of every non-deprecated tool set, including extension-contributed sets. Omit an unsupported built-in tool from the client tool sets so normal Copilot chat can continue using it; do not treat this contribution as the sole Agent Host allowlist. - **Non-interactive MCP authentication probes must not create dynamic authentication providers**: Provider creation can prompt for manual client registration when dynamic registration is unsupported. With `allowInteraction: false`, only inspect existing providers and sessions; defer metadata discovery and provider creation until the user invokes the `mcpAuthenticationRequired` action. - **Use structured maps for the state that is actually multi-keyed, not for an incidental cache**: If MCP tracking is addressed by session + server, model that source of truth directly with `NKeyMap`. Do not add a separate `NKeyMap` that merely caches serialized storage keys while leaving the real tracking state in nested or synchronized maps. diff --git a/src/vs/sessions/contrib/chat/browser/sessionChatInputToolbar.ts b/src/vs/sessions/contrib/chat/browser/sessionChatInputToolbar.ts index df7ece01e19..58b44d5b739 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionChatInputToolbar.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionChatInputToolbar.ts @@ -8,15 +8,13 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun, derived, derivedOpts, IObservable, IReader, observableValue } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.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 { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { localize } from '../../../../nls.js'; import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; -import { ChatTurnPillsWidget, diffStatsEqual, EMPTY_DIFF_STATS, IChatTurnPillsModel, IDiffStats, IPreviewFile, observeTurnStatusPillsEnabled, openChatPreviewFile, previewFilesEqual, previewKind } from '../../../../workbench/contrib/chat/browser/widget/chatTurnPills.js'; +import { ChatTurnPillsWidget, diffStatsEqual, EMPTY_DIFF_STATS, IChatTurnPillsModel, IDiffStats, IPreviewFile, observeTurnStatusPillsEnabled, openChatTurnFile, previewFilesEqual, previewKind } from '../../../../workbench/contrib/chat/browser/widget/chatTurnPills.js'; import { isAgentHostProviderId } from '../../../common/agentHostSessionsProvider.js'; import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; import { IChat, isActiveSessionStatus } from '../../../services/sessions/common/session.js'; @@ -135,10 +133,8 @@ export class SessionChatInputToolbar extends Disposable { }); constructor( - @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IOpenerService private readonly _openerService: IOpenerService, - @ILogService private readonly _logService: ILogService, @ISessionsService private readonly _sessionsService: ISessionsService, @IEditorService private readonly _editorService: IEditorService, @IInstantiationService instantiationService: IInstantiationService, @@ -154,7 +150,7 @@ export class SessionChatInputToolbar extends Disposable { changesEnabled: derived(reader => this._debugData.read(reader) !== undefined || this._active.read(reader) && turnStatusPillsEnabled.read(reader)), previewEnabled: derived(reader => this._debugData.read(reader) !== undefined || this._active.read(reader) && turnStatusPillsEnabled.read(reader)), openChanges: () => this._debugData.get() ? undefined : this._openChanges(), - openPreviewFile: file => this._debugData.get() ? undefined : openChatPreviewFile(file, this._commandService, this._openerService, this._logService), + openFile: file => this._debugData.get() ? undefined : openChatTurnFile(file, this._openerService, this._configurationService), }; const pills = this._register(instantiationService.createInstance(ChatTurnPillsWidget, model)); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatArtifactsWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatArtifactsWidget.ts index ce96986e5f4..6ba4e9feeb6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatArtifactsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatArtifactsWidget.ts @@ -27,7 +27,7 @@ import { ChatConfiguration } from '../../common/constants.js'; import { ChatMemoryFileResource } from '../../common/chatArtifactExtraction.js'; import { IChatArtifact, IChatArtifactsService, IArtifactSourceGroup, ArtifactSource } from '../../common/tools/chatArtifactsService.js'; import { IChatImageCarouselService } from '../chatImageCarouselService.js'; -import { getEditorOverrideForChatResource } from './chatContentParts/chatInlineAnchorWidget.js'; +import { getEditorOverrideForChatResource } from './chatEditorAssociations.js'; const ARTIFACT_TYPE_ICONS: Record<string, ThemeIcon> = { devServer: Codicon.globe, diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts index dc95821bad2..73fdd530396 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts @@ -38,7 +38,6 @@ import { FolderThemeIcon, IThemeService } from '../../../../../../platform/theme import { fillEditorsDragData } from '../../../../../browser/dnd.js'; import { StaticResourceContextKey } from '../../../../../common/contextkeys.js'; import { IEditorService, SIDE_GROUP } from '../../../../../services/editor/common/editorService.js'; -import { globMatchesResource } from '../../../../../services/editor/common/editorResolverService.js'; import { INotebookDocumentService } from '../../../../../services/notebook/common/notebookDocumentService.js'; import { ExplorerFolderContext } from '../../../../files/common/files.js'; import { IWorkspaceSymbol } from '../../../../search/common/search.js'; @@ -54,22 +53,7 @@ import { Schemas } from '../../../../../../base/common/network.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { BrowserEditorInput } from '../../../../browserView/common/browserEditorInput.js'; - -/** - * Returns the editor ID to use when opening a resource from chat pills (inline anchors), based on the - * `chat.editorAssociations` setting. Returns undefined if no association matches. - */ -export function getEditorOverrideForChatResource(resource: URI, configurationService: IConfigurationService): string | undefined { - const associations = configurationService.getValue<Record<string, string>>(ChatConfiguration.EditorAssociations) ?? {}; - // Sort patterns by length (longer patterns are more specific) - const sortedPatterns = Object.keys(associations).sort((a, b) => b.length - a.length); - for (const pattern of sortedPatterns) { - if (globMatchesResource(pattern, resource)) { - return associations[pattern]; - } - } - return undefined; -} +import { getEditorOverrideForChatResource } from '../chatEditorAssociations.js'; type ContentRefData = | { readonly kind: 'symbol'; readonly symbol: IWorkspaceSymbol } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTurnPillsPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTurnPillsPart.ts index fdb91d334e2..9b1b57ac62f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTurnPillsPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTurnPillsPart.ts @@ -13,13 +13,11 @@ import { basename, getComparisonKey, isEqual } from '../../../../../../base/comm import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { URI } from '../../../../../../base/common/uri.js'; import { localize, localize2 } from '../../../../../../nls.js'; -import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { FileKind } from '../../../../../../platform/files/common/files.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from '../../../../../browser/labels.js'; @@ -31,7 +29,7 @@ import { IEditSessionEntryDiff } from '../../../common/editing/chatEditingServic import { IChatRendererContent, IChatTurnPillsPart } from '../../../common/model/chatViewModel.js'; import { ChatTreeItem } from '../../chat.js'; import { IChatResponseFileChangesService } from '../../chatResponseFileChangesService.js'; -import { diffStatsEqual, EMPTY_DIFF_STATS, IDiffStats, IPreviewFile, observeTurnStatusPillsEnabled, openChatPreviewFile, previewFilesEqual, previewKind } from '../chatTurnPills.js'; +import { diffStatsEqual, EMPTY_DIFF_STATS, IDiffStats, IPreviewFile, observeTurnStatusPillsEnabled, openChatTurnFile, previewFilesEqual, previewKind } from '../chatTurnPills.js'; import { renderChangesSummaryFileList } from './chatChangesSummaryPart.js'; import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; @@ -54,12 +52,10 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent private readonly _content: IChatTurnPillsPart, _context: IChatContentPartRenderContext, @IChatResponseFileChangesService chatResponseFileChangesService: IChatResponseFileChangesService, - @ICommandService private readonly _commandService: ICommandService, @IOpenerService private readonly _openerService: IOpenerService, - @ILogService private readonly _logService: ILogService, @IHoverService private readonly _hoverService: IHoverService, @IEditorService private readonly _editorService: IEditorService, - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILabelService private readonly _labelService: ILabelService, @@ -112,7 +108,7 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent return [...created, ...edited]; }); - const turnStatusPillsEnabled = observeTurnStatusPillsEnabled(configurationService); + const turnStatusPillsEnabled = observeTurnStatusPillsEnabled(this._configurationService); const changesEnabled = derived(this, reader => turnStatusPillsEnabled.read(reader)); const previewEnabled = derived(this, reader => turnStatusPillsEnabled.read(reader)); const showChanges = derived(this, reader => changesEnabled.read(reader) && stats.read(reader).files > 0); @@ -139,10 +135,9 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent })); // Only feed diffs into the list when the changes summary is shown, so the - // disclosure stays empty when just the preview action is enabled. Each - // previewable row gets a "Preview" action that opens the file's preview. + // disclosure stays empty when just the preview action is enabled. const listDiffs = derived(this, reader => showChanges.read(reader) ? this._diffs.read(reader) : []); - this._register(renderChangesSummaryFileList(details, listDiffs, this._instantiationService, this._editorService, configurationService, { + this._register(renderChangesSummaryFileList(details, listDiffs, this._instantiationService, this._editorService, this._configurationService, { getRowActions: diff => this._getRowActions(diff), })); @@ -218,10 +213,10 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent { resource: primaryFile.uri, name }, { fileKind: FileKind.FILE, - title: localize('chat.turnPreview.tooltip', "{0} • Open Preview", this._labelService.getUriLabel(primaryFile.uri)), + title: localize('chat.turnPreview.tooltip', "{0} • Open File", this._labelService.getUriLabel(primaryFile.uri)), }, ); - button.setAttribute('aria-label', localize('chat.turnPreview.ariaLabel', "Open Preview: {0}", name)); + button.setAttribute('aria-label', localize('chat.turnPreview.ariaLabel', "Open File: {0}", name)); } container.classList.toggle('hidden', !showPreview.read(reader)); })); @@ -264,13 +259,13 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent private _openPrimaryPreview(files: readonly IPreviewFile[]): void { const primaryFile = files.at(0); if (primaryFile) { - openChatPreviewFile(primaryFile, this._commandService, this._openerService, this._logService); + openChatTurnFile(primaryFile, this._openerService, this._configurationService); } } /** - * Row actions for the changed-files list: markdown files get a labelless- - * icon-free "Preview" action that opens the file as a markdown preview. + * Row actions for the changed-files list: markdown files get a labelless, + * icon-free action that opens the file. */ private _getRowActions(diff: IEditSessionEntryDiff): IAction[] { const kind = previewKind(diff.modifiedURI); @@ -281,7 +276,7 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent return [toAction({ id: 'chat.turnChanges.previewFile', label: localize('chat.turnChanges.preview', "Preview"), - run: () => openChatPreviewFile(file, this._commandService, this._openerService, this._logService), + run: () => openChatTurnFile(file, this._openerService, this._configurationService), })]; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatEditorAssociations.ts b/src/vs/workbench/contrib/chat/browser/widget/chatEditorAssociations.ts new file mode 100644 index 00000000000..ff515b53439 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatEditorAssociations.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../base/common/uri.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { globMatchesResource } from '../../../../services/editor/common/editorResolverService.js'; +import { ChatConfiguration } from '../../common/constants.js'; + +/** + * Returns the editor configured for a resource opened from chat, if one matches. + */ +export function getEditorOverrideForChatResource(resource: URI, configurationService: IConfigurationService): string | undefined { + const associations = configurationService.getValue<Record<string, string>>(ChatConfiguration.EditorAssociations) ?? {}; + const sortedPatterns = Object.keys(associations).sort((a, b) => b.length - a.length); + for (const pattern of sortedPatterns) { + if (globMatchesResource(pattern, resource)) { + return associations[pattern]; + } + } + return undefined; +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatTurnPills.ts b/src/vs/workbench/contrib/chat/browser/widget/chatTurnPills.ts index 8da26f7b4ed..445041d3c39 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatTurnPills.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatTurnPills.ts @@ -16,18 +16,17 @@ import { basename, isEqual } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { FileKind } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { AnimatedCounterWidget } from '../../../../browser/animatedCounterWidget.js'; import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from '../../../../browser/labels.js'; import { ChatConfiguration } from '../../common/constants.js'; +import { getEditorOverrideForChatResource } from './chatEditorAssociations.js'; import '../media/chatTurnPills.css'; const CHANGES_PILL_ACTION_ID = 'chat.turnPills.changes'; @@ -91,17 +90,14 @@ export function previewFilesEqual(a: readonly IPreviewFile[], b: readonly IPrevi return true; } -/** - * Open a previewable file: markdown files open as a markdown preview, falling - * back to the default opener when it is not available (e.g. web). - */ -export async function openChatPreviewFile(file: IPreviewFile, commandService: ICommandService, openerService: IOpenerService, logService: ILogService): Promise<void> { - try { - await commandService.executeCommand('markdown.showPreview', file.uri); - } catch (err) { - logService.trace('[ChatTurnPills] Falling back to default opener for preview', err); - await openerService.open(file.uri); - } +/** Opens a turn file with the editor configured for resources opened from chat. */ +export async function openChatTurnFile(file: IPreviewFile, openerService: IOpenerService, configurationService: IConfigurationService): Promise<void> { + await openerService.open(file.uri, { + fromUserGesture: true, + editorOptions: { + override: getEditorOverrideForChatResource(file.uri, configurationService), + }, + }); } /** The data and interactions a {@link ChatTurnPillsWidget} reflects. */ @@ -113,7 +109,7 @@ export interface IChatTurnPillsModel { /** When `false` the preview pill stays hidden regardless of the data. */ readonly previewEnabled: IObservable<boolean>; openChanges(): void; - openPreviewFile(file: IPreviewFile): void; + openFile(file: IPreviewFile): void; } /** The former per-pill setting shape, retained for existing user settings. */ @@ -216,7 +212,7 @@ class ChangesPillActionViewItem extends BaseActionViewItem { * The preview pill: renders the primary previewable file as a resource label * (file icon + name). When more than one previewable file exists, a separator * and a dropdown chevron are shown; the chevron lists every previewable file. - * Activating the label opens the primary file's preview. + * Activating the label opens the primary file. */ class PreviewPillActionViewItem extends BaseActionViewItem { @@ -292,7 +288,7 @@ class PreviewPillActionViewItem extends BaseActionViewItem { * changes. * - **Preview** — shown when the turn created or edited a markdown file. * Rendered as a resource label for the primary file. Activating it opens that - * file as a markdown preview; when several exist, a dropdown lists them all. + * file; when several exist, a dropdown lists them all. * The data and the open actions are supplied by the {@link IChatTurnPillsModel} * so the same widget serves surfaces with different data sources. */ @@ -325,7 +321,7 @@ export class ChatTurnPillsWidget extends Disposable { this._resourceLabels = this._register(this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER)); this._changesAction = this._register(new Action(CHANGES_PILL_ACTION_ID, localize('chatTurnPills.changes.tooltip', "View Current Turn Changes"), undefined, true, async () => this._model.openChanges())); - this._previewAction = this._register(new Action(PREVIEW_PILL_ACTION_ID, localize('chatTurnPills.preview.label', "Open Preview"), undefined, true, async () => this._openPrimaryPreview())); + this._previewAction = this._register(new Action(PREVIEW_PILL_ACTION_ID, localize('chatTurnPills.preview.label', "Open Preview"), undefined, true, async () => this._openPrimaryFile())); this._toolbar = this._register(new ToolBar(this.element, this._contextMenuService, { orientation: ActionsOrientation.HORIZONTAL, @@ -335,7 +331,7 @@ export class ChatTurnPillsWidget extends Disposable { return new ChangesPillActionViewItem(action, options, this._model.stats, this._instantiationService); } if (action.id === PREVIEW_PILL_ACTION_ID) { - return new PreviewPillActionViewItem(action, options, this._model.previewFiles, this._resourceLabels, file => this._model.openPreviewFile(file), anchor => this._showAllPreviews(anchor)); + return new PreviewPillActionViewItem(action, options, this._model.previewFiles, this._resourceLabels, file => this._model.openFile(file), anchor => this._showAllFiles(anchor)); } return undefined; }, @@ -372,14 +368,14 @@ export class ChatTurnPillsWidget extends Disposable { this.element.classList.toggle('hidden', actions.length === 0); } - private _openPrimaryPreview(): void { + private _openPrimaryFile(): void { const primaryFile = this._model.previewFiles.get().at(0); if (primaryFile) { - this._model.openPreviewFile(primaryFile); + this._model.openFile(primaryFile); } } - private _showAllPreviews(anchor: HTMLElement): void { + private _showAllFiles(anchor: HTMLElement): void { const files = this._model.previewFiles.get(); if (files.length === 0) { return; @@ -389,8 +385,8 @@ export class ChatTurnPillsWidget extends Disposable { getActions: () => files.map(file => toAction({ id: `${PREVIEW_PILL_ACTION_ID}.${file.uri.toString()}`, label: basename(file.uri), - class: ThemeIcon.asClassName(Codicon.openPreview), - run: () => this._model.openPreviewFile(file), + class: ThemeIcon.asClassName(Codicon.goToFile), + run: () => this._model.openFile(file), })), }); } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatTurnPills.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatTurnPills.test.ts new file mode 100644 index 00000000000..42fa90b8fe8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatTurnPills.test.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { mock } from '../../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IOpenerService, OpenExternalOptions, OpenInternalOptions } from '../../../../../../platform/opener/common/opener.js'; +import { openChatTurnFile } from '../../../browser/widget/chatTurnPills.js'; +import { ChatConfiguration } from '../../../common/constants.js'; + +suite('ChatTurnPills', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('opens a markdown resource with its configured chat editor association', async () => { + const resource = URI.file('/workspace/README.md'); + let opened: { resource: string; options: OpenInternalOptions | OpenExternalOptions | undefined } | undefined; + const openerService = new class extends mock<IOpenerService>() { + override async open(resource: string | URI, options?: OpenInternalOptions | OpenExternalOptions): Promise<boolean> { + opened = { resource: resource.toString(), options }; + return true; + } + }; + const configurationService = new TestConfigurationService({ + [ChatConfiguration.EditorAssociations]: { + '*.md': 'vscode.markdown.editor', + }, + }); + + await openChatTurnFile({ uri: resource, kind: 'markdown', created: true }, openerService, configurationService); + + assert.deepStrictEqual(opened, { + resource: resource.toString(), + options: { + fromUserGesture: true, + editorOptions: { + override: 'vscode.markdown.editor', + }, + }, + }); + }); +}); From 4b2897c7cb6c64e193664d4a19d62a01c00857c8 Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci <39117631+Giuspepe@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:02:52 +0200 Subject: [PATCH 72/86] Add xAI model provider icon (#328413) * Add xAI model provider icon * signing commit --- package-lock.json | 8 ++++---- package.json | 2 +- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- src/vs/base/common/codiconsLibrary.ts | 1 + .../widget/input/modelPicker/modelProviderIcons.ts | 9 +++++++-- .../widget/input/modelPicker/modelProviderIcons.test.ts | 8 ++++++++ 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index e47d53e34cf..20eff3d3fa0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@microsoft/mxc-sdk": "0.6.1", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.46-27", + "@vscode/codicons": "^0.0.46-28", "@vscode/copilot-api": "^0.4.2", "@vscode/deviceid": "^0.1.1", "@vscode/diff": "0.0.2-7", @@ -4171,9 +4171,9 @@ } }, "node_modules/@vscode/codicons": { - "version": "0.0.46-27", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.46-27.tgz", - "integrity": "sha512-R6lEiJzbDrcrIT+pjM0aauVFjGXVHj4K9ClMzI4aOQWZH/fSswOJljypunppD81Mjwia1RzxA5LiBAlBJUI5PA==", + "version": "0.0.46-28", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.46-28.tgz", + "integrity": "sha512-Rj3yNS72a7N0FN/JeT/muXRCBzNNvnbQ99B++bCDyaOZkbHAfP/3DS7YoiAxa4z+ZiG5ZowJ5b9ncB40YGe1ig==", "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { diff --git a/package.json b/package.json index 6603d8bc5a0..4616d7a71da 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "@microsoft/mxc-sdk": "0.6.1", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.46-27", + "@vscode/codicons": "^0.0.46-28", "@vscode/copilot-api": "^0.4.2", "@vscode/deviceid": "^0.1.1", "@vscode/diff": "0.0.2-7", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 619c300ac42..423c55ef873 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.46-27", + "@vscode/codicons": "^0.0.46-28", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.1", "@vscode/vscode-languagedetection": "1.0.23", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.46-27", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.46-27.tgz", - "integrity": "sha512-R6lEiJzbDrcrIT+pjM0aauVFjGXVHj4K9ClMzI4aOQWZH/fSswOJljypunppD81Mjwia1RzxA5LiBAlBJUI5PA==", + "version": "0.0.46-28", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.46-28.tgz", + "integrity": "sha512-Rj3yNS72a7N0FN/JeT/muXRCBzNNvnbQ99B++bCDyaOZkbHAfP/3DS7YoiAxa4z+ZiG5ZowJ5b9ncB40YGe1ig==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index 06348620438..393d4dc09e5 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.46-27", + "@vscode/codicons": "^0.0.46-28", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.1", "@vscode/vscode-languagedetection": "1.0.23", diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index cbd904ba4ed..06e709dd15a 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -759,4 +759,5 @@ export const codiconsLibrary = { cloudUploadCompact: register('cloud-upload-compact', 0xece9), micCompact: register('mic-compact', 0xecea), arrowUpCompact: register('arrow-up-compact', 0xeceb), + xai: register('xai', 0xecec), } as const; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelProviderIcons.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelProviderIcons.ts index d2a8c602a69..4a8624bcc30 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelProviderIcons.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPicker/modelProviderIcons.ts @@ -15,16 +15,20 @@ const claudeModelProviderIcon = registerIcon('chat-model-provider-claude', Codic const geminiModelProviderIcon = registerIcon('chat-model-provider-gemini', Codicon.googleGemini, localize('chatModelProviderGeminiIcon', "Icon for Gemini models.")); const kimiModelProviderIcon = registerIcon('chat-model-provider-kimi', Codicon.kimi, localize('chatModelProviderKimiIcon', "Icon for Kimi models.")); const microsoftModelProviderIcon = registerIcon('chat-model-provider-microsoft', Codicon.microsoft, localize('chatModelProviderMicrosoftIcon', "Icon for Microsoft models.")); +const xAIModelProviderIcon = registerIcon('chat-model-provider-xai', Codicon.xai, localize('chatModelProviderXAIIcon', "Icon for xAI models.")); const genericModelProviderIcon = registerIcon('chat-model-provider-generic', Codicon.sparkle, localize('chatModelProviderGenericIcon', "Icon for other model providers.")); export function getModelProviderIcon(model: ILanguageModelChatMetadataAndIdentifier): ThemeIcon { + const identity = `${model.metadata.vendor} ${model.metadata.family} ${model.metadata.id} ${model.metadata.name}`.toLowerCase(); + if (identity.includes('grok') || identity.includes('xai')) { + return xAIModelProviderIcon; + } if (model.metadata.isBYOK) { return genericModelProviderIcon; } if (isAutoLanguageModel(model)) { return copilotModelProviderIcon; } - const identity = `${model.metadata.vendor} ${model.metadata.family} ${model.metadata.id} ${model.metadata.name}`.toLowerCase(); if (identity.includes('claude') || identity.includes('anthropic')) { return claudeModelProviderIcon; } @@ -40,7 +44,8 @@ export function getModelProviderIcon(model: ILanguageModelChatMetadataAndIdentif if (identity.includes('openai') || identity.includes('gpt') || identity.includes('codex') || /\bo[134]\b/.test(identity)) { return openAIModelProviderIcon; } - if (identity.includes('copilot')) { + const modelIdentity = `${model.metadata.id} ${model.metadata.name}`.toLowerCase(); + if (modelIdentity.includes('copilot')) { return copilotModelProviderIcon; } return genericModelProviderIcon; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/modelPicker/modelProviderIcons.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/modelPicker/modelProviderIcons.test.ts index 36f1721e6f2..6281ce23216 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/modelPicker/modelProviderIcons.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/modelPicker/modelProviderIcons.test.ts @@ -36,7 +36,11 @@ suite('ModelProviderIcons', () => { getModelProviderIcon(createModel('claude-sonnet-5', 'Claude Sonnet 5')).id, getModelProviderIcon(createModel('gemini-3.1-pro', 'Gemini 3.1 Pro')).id, getModelProviderIcon(createModel('kimi-k2.5', 'Kimi K2.5')).id, + getModelProviderIcon(createModel('grok-4.5', 'Grok 4.5')).id, + getModelProviderIcon(createModel('grok-code-fast-1', 'Grok Code Fast 1', 'agent-host-copilot')).id, + getModelProviderIcon(createModel('grok-4', 'Grok 4', 'xai', { isBYOK: true })).id, getModelProviderIcon(createModel('mai-ds-r1', 'MAI-DS-R1')).id, + getModelProviderIcon(createModel('deepseek-v4-pro', 'DeepSeek V4 Pro')).id, getModelProviderIcon(createModel('auto', 'Auto')).id, getModelProviderIcon(createModel('auto', 'Auto', 'anthropic')).id, getModelProviderIcon(createModel('custom', 'Custom Model', 'third-party')).id, @@ -48,7 +52,11 @@ suite('ModelProviderIcons', () => { 'chat-model-provider-claude', 'chat-model-provider-gemini', 'chat-model-provider-kimi', + 'chat-model-provider-xai', + 'chat-model-provider-xai', + 'chat-model-provider-xai', 'chat-model-provider-microsoft', + 'chat-model-provider-generic', 'chat-model-provider-copilot', 'chat-model-provider-copilot', 'chat-model-provider-generic', From d2485131f06a1aa512fb666b1359725d13d4e8a6 Mon Sep 17 00:00:00 2001 From: vritant24 <vrbhardw@microsoft.com> Date: Fri, 31 Jul 2026 09:07:08 -0700 Subject: [PATCH 73/86] Agent Host changes for agents/issue-328336-fix-planning --- .../agentHost/common/agentHostByokLm.ts | 4 ++ .../agentHost/node/byokLmBridgeRegistry.ts | 13 +++- .../agentHost/node/copilot/copilotAgent.ts | 6 +- .../node/copilot/copilotSessionLauncher.ts | 8 +-- .../test/node/byokLmBridgeRegistry.test.ts | 49 +++++++++++++++ .../agentHost/test/node/copilotAgent.test.ts | 61 +++++++++++++++++-- .../agentHost/agentHostByokLmHandler.ts | 5 ++ .../agentHostByokLmHandler.test.ts | 51 ++++++++++++++++ 8 files changed, 187 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHostByokLm.ts b/src/vs/platform/agentHost/common/agentHostByokLm.ts index 772b6e5db88..0323e351059 100644 --- a/src/vs/platform/agentHost/common/agentHostByokLm.ts +++ b/src/vs/platform/agentHost/common/agentHostByokLm.ts @@ -143,6 +143,10 @@ export interface IByokLmModelInfo { readonly maxContextWindowTokens?: number; /** Whether the model accepts image inputs, when known. */ readonly supportsVision?: boolean; + /** Reasoning effort values advertised by the renderer model, when known. */ + readonly supportedReasoningEfforts?: readonly string[]; + /** Default reasoning effort advertised by the renderer model, when known. */ + readonly defaultReasoningEffort?: string; } export const IAgentHostByokLmHandler = createDecorator<IAgentHostByokLmHandler>('agentHostByokLmHandler'); diff --git a/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts b/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts index 3729e98674b..41d7903a439 100644 --- a/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts +++ b/src/vs/platform/agentHost/node/byokLmBridgeRegistry.ts @@ -162,10 +162,21 @@ function modelsEqual(a: readonly IByokLmModelInfo[], b: readonly IByokLmModelInf } return a.every((m, i) => { const n = b[i]; - return m.vendor === n.vendor && m.id === n.id && m.name === n.name && m.modelIdentifier === n.modelIdentifier && m.maxContextWindowTokens === n.maxContextWindowTokens && m.supportsVision === n.supportsVision; + return m.vendor === n.vendor + && m.id === n.id + && m.name === n.name + && m.modelIdentifier === n.modelIdentifier + && m.maxContextWindowTokens === n.maxContextWindowTokens + && m.supportsVision === n.supportsVision + && m.defaultReasoningEffort === n.defaultReasoningEffort + && arraysEqual(m.supportedReasoningEfforts, n.supportedReasoningEfforts); }); } +function arraysEqual(a: readonly string[] | undefined, b: readonly string[] | undefined): boolean { + return a === b || (a !== undefined && b !== undefined && a.length === b.length && a.every((value, index) => value === b[index])); +} + /** * No-op {@link IByokLmBridgeRegistry} for agent host entrypoints that do not * support BYOK — e.g. the remote agent host, where no extension host runs diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 7e92ba0a548..7e8227e7bd0 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -76,7 +76,7 @@ import { CopilotAgentSession, type CopilotSdkMode } from './copilotAgentSession. import { ICopilotSessionContext, projectFromCopilotContext } from './copilotGitProject.js'; import { parsedPluginsEqual, toChildCustomizations } from './copilotPluginConverters.js'; import { CopilotGitHubTelemetryForwarder } from './copilotGitHubTelemetryForwarder.js'; -import { CopilotSessionLauncher, ContextSizeConfigKey, ThinkingLevelConfigKey, getCopilotContextTier, resolveCopilotReasoningEffort, type CopilotSessionLaunchPlan, type IActiveClientSnapshot } from './copilotSessionLauncher.js'; +import { CopilotSessionLauncher, ContextSizeConfigKey, ThinkingLevelConfigKey, getCopilotContextTier, isCopilotReasoningEffort, resolveCopilotReasoningEffort, type CopilotSessionLaunchPlan, type IActiveClientSnapshot } from './copilotSessionLauncher.js'; import { ShellManager } from './copilotShellTools.js'; import { isAgentHostTelemetryService } from '../agentHostTelemetryService.js'; import { ICopilotApiService, type IRestrictedTelemetryContext } from '../shared/copilotApiService.js'; @@ -1092,12 +1092,16 @@ export class CopilotAgent extends Disposable implements IAgent { } this._byokModels = this._byokBridgeRegistry.getModels().map((m): IAgentModelInfo => { const byokMeta = createAgentModelByokMeta(m.modelIdentifier); + const supportedReasoningEfforts = m.supportedReasoningEfforts?.filter(isCopilotReasoningEffort); + const defaultReasoningEffort = supportedReasoningEfforts?.find(effort => effort === m.defaultReasoningEffort) ?? supportedReasoningEfforts?.[0]; + const thinkingLevel = this._createThinkingLevelConfigSchemaProperty(supportedReasoningEfforts, defaultReasoningEffort); return { provider: this.id, id: `${m.vendor}/${m.id}`, name: m.name ?? m.id, maxContextWindow: m.maxContextWindowTokens, supportsVision: m.supportsVision ?? false, + ...(thinkingLevel ? { configSchema: { type: 'object', properties: { [ThinkingLevelConfigKey]: thinkingLevel } } satisfies ConfigSchema } : {}), ...(byokMeta && { _meta: byokMeta }), }; }); diff --git a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts index ef7ab36fa3d..ecaeed7ffdb 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts @@ -175,7 +175,7 @@ export interface ICopilotResumeSessionLaunchPlan extends ICopilotSessionLaunchBa export type CopilotSessionLaunchPlan = ICopilotCreateSessionLaunchPlan | ICopilotResumeSessionLaunchPlan; -function isReasoningEffort(value: unknown): value is ReasoningEffort { +export function isCopilotReasoningEffort(value: unknown): value is ReasoningEffort { return ReasoningEfforts.some(reasoningEffort => reasoningEffort === value); } @@ -238,11 +238,11 @@ function isCustomAgentNotFoundError(err: unknown): boolean { * caller/operator is responsible for choosing a level the model supports. */ export function getCopilotReasoningEffort(model: ModelSelection | undefined, effortOverride?: string): SessionConfig['reasoningEffort'] { - if (isReasoningEffort(effortOverride)) { + if (isCopilotReasoningEffort(effortOverride)) { return effortOverride; } const thinkingLevel = model?.config?.[ThinkingLevelConfigKey]; - return isReasoningEffort(thinkingLevel) ? thinkingLevel : undefined; + return isCopilotReasoningEffort(thinkingLevel) ? thinkingLevel : undefined; } /** @@ -255,7 +255,7 @@ export function resolveCopilotReasoningEffort(model: ModelSelection | undefined, // '' is the schema's unset marker, so an unset override reads as `undefined`. const override = rawOverride ? rawOverride : undefined; if (override !== undefined) { - if (isReasoningEffort(override)) { + if (isCopilotReasoningEffort(override)) { logService.info(`[Copilot:${sessionId}] Applying reasoning-effort override '${override}'`); } else { logService.warn(`[Copilot:${sessionId}] Ignoring invalid reasoning-effort override '${override}'; expected one of [${ReasoningEfforts.join(', ')}]`); diff --git a/src/vs/platform/agentHost/test/node/byokLmBridgeRegistry.test.ts b/src/vs/platform/agentHost/test/node/byokLmBridgeRegistry.test.ts index da7cec180fb..f461b01d40e 100644 --- a/src/vs/platform/agentHost/test/node/byokLmBridgeRegistry.test.ts +++ b/src/vs/platform/agentHost/test/node/byokLmBridgeRegistry.test.ts @@ -161,4 +161,53 @@ suite('ByokLmBridgeRegistry', () => { reg.dispose(); }); + + test('compares reasoning effort metadata structurally', () => { + const registry = new ByokLmBridgeRegistry(); + const conn = pushable(); + const reg = store.add(registry.register('client-a', conn.connection)); + conn.push([{ + vendor: 'acme', + id: 'reasoning', + supportedReasoningEfforts: ['low', 'high'], + defaultReasoningEffort: 'low', + }]); + + let changes = 0; + store.add(registry.onDidChangeModels(() => { changes++; })); + + conn.push([{ + vendor: 'acme', + id: 'reasoning', + supportedReasoningEfforts: ['low', 'high'], + defaultReasoningEffort: 'low', + }]); + conn.push([{ + vendor: 'acme', + id: 'reasoning', + supportedReasoningEfforts: ['low', 'high'], + defaultReasoningEffort: 'high', + }]); + conn.push([{ + vendor: 'acme', + id: 'reasoning', + supportedReasoningEfforts: ['low', 'medium', 'high'], + defaultReasoningEffort: 'high', + }]); + + assert.deepStrictEqual({ + changes, + models: registry.getModels(), + }, { + changes: 2, + models: [{ + vendor: 'acme', + id: 'reasoning', + supportedReasoningEfforts: ['low', 'medium', 'high'], + defaultReasoningEffort: 'high', + }], + }); + + reg.dispose(); + }); }); diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index a00156384ca..1446cb072c5 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -13,7 +13,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; import { DeferredPromise, timeout } from '../../../../base/common/async.js'; import { CancellationError, 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'; +import { Emitter, Event } from '../../../../base/common/event.js'; import { Schemas } from '../../../../base/common/network.js'; import { waitForState } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; @@ -28,6 +28,7 @@ import { ServiceCollection } from '../../../instantiation/common/serviceCollecti import { ILogService, LogLevel, NullLogService } from '../../../log/common/log.js'; import { IAgentHostProxyResolver } from '../../node/agentHostProxyResolver.js'; import type { IAgentHostClientProxyConnection } from '../../common/agentHostClientProxyChannel.js'; +import type { IByokLmBridgeConnection, IByokLmModelInfo } from '../../common/agentHostByokLm.js'; import { ITelemetryService } from '../../../telemetry/common/telemetry.js'; import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; import { AgentHostTelemetryService } from '../../node/agentHostTelemetryService.js'; @@ -596,7 +597,7 @@ function getCreatedClientOptions(agent: CopilotAgent): readonly CopilotClientOpt return agent.createdClientOptions; } -function createTestAgentContext(disposables: Pick<DisposableStore, 'add'>, options?: { sessionDataService?: ISessionDataService; copilotClient?: ITestCopilotClient; useRealResumePath?: boolean; gitService?: TestAgentHostGitService; environmentServiceRegistration?: 'native' | 'none'; pluginManager?: IAgentPluginManager; fileService?: FileService; copilotApiService?: ICopilotApiService; gitHubEndpointService?: IAgentHostGitHubEndpointService; telemetryService?: ITelemetryService; userHome?: URI; logService?: ILogService; proxyResolver?: IAgentHostProxyResolver }): { agent: CopilotAgent; instantiationService: IInstantiationService; configurationService: IAgentConfigurationService; fileService: FileService; stateManager: AgentHostStateManager } { +function createTestAgentContext(disposables: Pick<DisposableStore, 'add'>, options?: { sessionDataService?: ISessionDataService; copilotClient?: ITestCopilotClient; useRealResumePath?: boolean; gitService?: TestAgentHostGitService; environmentServiceRegistration?: 'native' | 'none'; pluginManager?: IAgentPluginManager; fileService?: FileService; copilotApiService?: ICopilotApiService; gitHubEndpointService?: IAgentHostGitHubEndpointService; telemetryService?: ITelemetryService; userHome?: URI; logService?: ILogService; proxyResolver?: IAgentHostProxyResolver; byokBridgeRegistry?: IByokLmBridgeRegistry }): { agent: CopilotAgent; instantiationService: IInstantiationService; configurationService: IAgentConfigurationService; fileService: FileService; stateManager: AgentHostStateManager } { const services = new ServiceCollection(); const logService = options?.logService ?? new NullLogService(); const fileService = options?.fileService ?? disposables.add(new FileService(logService)); @@ -621,7 +622,7 @@ function createTestAgentContext(disposables: Pick<DisposableStore, 'add'>, optio }); services.set(IAgentHostCompletions, disposables.add(new AgentHostCompletions(logService))); services.set(IAgentHostProxyResolver, options?.proxyResolver ?? new TestProxyResolver()); - services.set(IByokLmBridgeRegistry, new ByokLmBridgeRegistry()); + services.set(IByokLmBridgeRegistry, options?.byokBridgeRegistry ?? new ByokLmBridgeRegistry()); const copilotApiService = options?.copilotApiService ?? new TestCopilotApiService(); services.set(ICopilotApiService, copilotApiService); services.set(ITelemetryService, options?.telemetryService ?? NullTelemetryService); @@ -641,7 +642,7 @@ function createTestAgentContext(disposables: Pick<DisposableStore, 'add'>, optio return { agent, instantiationService, configurationService: configService, fileService, stateManager }; } -function createTestAgent(disposables: Pick<DisposableStore, 'add'>, options?: { sessionDataService?: ISessionDataService; copilotClient?: ITestCopilotClient; useRealResumePath?: boolean; gitService?: TestAgentHostGitService; environmentServiceRegistration?: 'native' | 'none'; pluginManager?: IAgentPluginManager; fileService?: FileService; copilotApiService?: ICopilotApiService; gitHubEndpointService?: IAgentHostGitHubEndpointService; telemetryService?: ITelemetryService; userHome?: URI; logService?: ILogService }): CopilotAgent { +function createTestAgent(disposables: Pick<DisposableStore, 'add'>, options?: { sessionDataService?: ISessionDataService; copilotClient?: ITestCopilotClient; useRealResumePath?: boolean; gitService?: TestAgentHostGitService; environmentServiceRegistration?: 'native' | 'none'; pluginManager?: IAgentPluginManager; fileService?: FileService; copilotApiService?: ICopilotApiService; gitHubEndpointService?: IAgentHostGitHubEndpointService; telemetryService?: ITelemetryService; userHome?: URI; logService?: ILogService; byokBridgeRegistry?: IByokLmBridgeRegistry }): CopilotAgent { return createTestAgentContext(disposables, options).agent; } @@ -2137,6 +2138,58 @@ suite('CopilotAgent', () => { } }); + test('BYOK model configSchema exposes only Copilot-supported reasoning efforts', async () => { + const byokBridgeRegistry = new ByokLmBridgeRegistry(); + const agent = createTestAgent(disposables, { byokBridgeRegistry }); + const modelSnapshots = disposables.add(new Emitter<IByokLmModelInfo[]>()); + const connection: IByokLmBridgeConnection = { + chat: async () => ({ output: [] }), + onDidChangeModels: modelSnapshots.event, + }; + disposables.add(byokBridgeRegistry.register('renderer', connection)); + + try { + modelSnapshots.fire([ + { + vendor: 'acme', + id: 'fallback-default', + name: 'Fallback Default', + supportedReasoningEfforts: ['minimal', 'low', 'high'], + defaultReasoningEffort: 'minimal', + }, + { + vendor: 'acme', + id: 'valid-default', + name: 'Valid Default', + supportedReasoningEfforts: ['low', 'medium', 'high'], + defaultReasoningEffort: 'medium', + }, + { + vendor: 'acme', + id: 'unsupported-only', + name: 'Unsupported Only', + supportedReasoningEfforts: ['minimal'], + defaultReasoningEffort: 'minimal', + }, + ]); + const models = await waitForState(agent.models, models => models.length === 3); + + assert.deepStrictEqual(models.map(model => ({ + id: model.id, + thinkingLevel: model.configSchema?.properties.thinkingLevel && { + enum: model.configSchema.properties.thinkingLevel.enum, + default: model.configSchema.properties.thinkingLevel.default, + }, + })), [ + { id: 'acme/fallback-default', thinkingLevel: { enum: ['low', 'high'], default: 'low' } }, + { id: 'acme/valid-default', thinkingLevel: { enum: ['low', 'medium', 'high'], default: 'medium' } }, + { id: 'acme/unsupported-only', thinkingLevel: undefined }, + ]); + } finally { + await disposeAgent(agent); + } + }); + test('configSchema emits a numeric contextSize property when long_context tier exceeds default', async () => { const agent = createTestAgent(disposables, { copilotClient: new TestCopilotClient([], [{ 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 79c85a3c4a9..27a22293686 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.ts @@ -137,6 +137,9 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok // 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) { + const reasoningEffortSchema = metadata.configurationSchema?.properties?.reasoningEffort; + const supportedReasoningEfforts = reasoningEffortSchema?.enum?.filter((value): value is string => typeof value === 'string'); + const defaultReasoningEffort = typeof reasoningEffortSchema?.default === 'string' ? reasoningEffortSchema.default : undefined; models.push({ vendor: metadata.vendor, id: metadata.id, @@ -144,6 +147,8 @@ export class AgentHostByokLmHandler extends Disposable implements IAgentHostByok modelIdentifier: identifier, maxContextWindowTokens: metadata.maxInputTokens + metadata.maxOutputTokens, supportsVision: !!metadata.capabilities?.vision, + ...(supportedReasoningEfforts?.length ? { supportedReasoningEfforts } : {}), + ...(defaultReasoningEffort !== undefined ? { defaultReasoningEffort } : {}), }); } } 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 72293a054df..d9ed95f9857 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 @@ -129,6 +129,57 @@ suite('AgentHostByokLmHandler', () => { ]); }); + test('listModels carries string reasoning effort metadata from renderer BYOK schemas', async () => { + const service = new TestLanguageModelsService( + new Map<string, ILanguageModelChatMetadata>([ + ['id-reasoning', { + ...byokModel('acme', 'reasoning'), + configurationSchema: { + properties: { + reasoningEffort: { + type: 'string', + enum: ['minimal', 'low', 1, 'high'], + default: 'high', + }, + }, + }, + }], + ['id-malformed', { + ...byokModel('acme', 'malformed'), + configurationSchema: { + properties: { + reasoningEffort: { + type: 'string', + enum: [1, false], + default: 1, + }, + }, + }, + }], + ['id-plain', byokModel('acme', 'plain')], + ]), + () => responseOf([]), + ); + const handler = createHandler(service); + + const models = await handler.listModels(CancellationToken.None); + + assert.deepStrictEqual(models, [ + { + vendor: 'acme', + id: 'reasoning', + name: 'acme reasoning', + modelIdentifier: 'id-reasoning', + maxContextWindowTokens: 2000, + supportsVision: false, + supportedReasoningEfforts: ['minimal', 'low', 'high'], + defaultReasoningEffort: 'high', + }, + { vendor: 'acme', id: 'malformed', name: 'acme malformed', modelIdentifier: 'id-malformed', maxContextWindowTokens: 2000, supportsVision: false }, + { vendor: 'acme', id: 'plain', name: 'acme plain', modelIdentifier: 'id-plain', maxContextWindowTokens: 2000, supportsVision: false }, + ]); + }); + test('buffers ordered thinking, text, tool calls, continuation and usage', async () => { const service = new TestLanguageModelsService( new Map([['id-acme-claude', byokModel('acme', 'claude')]]), From 82705b922d896afda0b07ddb23f77ff9a1bd6d40 Mon Sep 17 00:00:00 2001 From: Hawk Ticehurst <39639992+hawkticehurst@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:23:46 -0400 Subject: [PATCH 74/86] Modern UI tabs redesign and bug bash (#328111) * Redesign modern UI tabs * Address PR feedback * Many more updates * Tweaks and bug fixes * More changes --- .../lib/stylelint/vscode-known-variables.json | 4 + .../browser/parts/editor/editorTabsControl.ts | 2 +- .../browser/media/activityBar.css | 39 +- .../styleOverrides/browser/media/tabs.css | 466 +++++++++++++----- 4 files changed, 378 insertions(+), 133 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 52db3d2c065..694aaf18069 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1022,6 +1022,10 @@ "--part-border-color", "--pane-header-size", "--model-hover-surface-background", + "--modern-ui-editor-tab-action-active-background", + "--modern-ui-editor-tab-action-hover-background", + "--modern-ui-tab-active-background", + "--modern-ui-tab-hover-background", "--scroll-shadow-surface", "--vscode-chat-list-background", "--vscode-editorCodeLens-fontFamily", diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 1072ac32cb9..43cc2e5a0e4 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -104,7 +104,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC compact: 22 as const, // Style-override (Modern UI) multi-tab mode adds 4px top + 4px bottom padding to // the tabs-and-actions-container (tabs.css), so the total title-bar height is the - // --editor-group-tab-height CSS value (24px / 14px) plus that 8px padding. + // --editor-group-tab-height CSS value (24px / 20px) plus that 8px padding. styleOverride: 32 as const, // 24px tab + 4px top + 4px bottom padding styleOverrideCompact: 28 as const, // 20px tab + 4px top + 4px bottom padding (20px = minimum to fit 16px icon + 2px padding) }; diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/activityBar.css b/src/vs/workbench/contrib/styleOverrides/browser/media/activityBar.css index 01ea5973ba7..545cd679c66 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/media/activityBar.css +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/activityBar.css @@ -99,6 +99,24 @@ height: calc(var(--activity-bar-action-height, 28px) - 4px); } +:is(.hc-black, .hc-light).style-override .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked .active-item-indicator { + border-radius: var(--vscode-cornerRadius-small); + background-color: transparent; + outline: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); + outline-offset: calc(-1 * var(--vscode-strokeThickness)); +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator { + display: block; +} + +:is(.hc-black, .hc-light).style-override .activitybar > .content:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) :not(.monaco-menu) > .monaco-action-bar .action-item:not(.checked):hover::before { + border-radius: var(--vscode-cornerRadius-small); + background-color: transparent; + outline: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); + outline-offset: calc(-1 * var(--vscode-strokeThickness)); +} + /* * Horizontal Activity Bar — top / bottom position on the primary sidebar, panel * (bottom) and auxiliary bar. When the activity bar is moved to the top or @@ -145,15 +163,16 @@ } /* Active item: inset, rounded background box behind the icon. */ -.style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon.checked .active-item-indicator, -.style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon.checked .active-item-indicator { +.style-override .pane-composite-part > .title > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon.checked:not(:active) .active-item-indicator, +.style-override .pane-composite-part > .header-or-footer > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon.checked:not(:active) .active-item-indicator { z-index: 0; - top: 4px; + top: 50%; left: 0; width: 24px; height: 24px; border-radius: var(--vscode-cornerRadius-small); background-color: var(--vscode-activityBar-activeBackground, var(--vscode-list-inactiveSelectionBackground)); + transform: translateY(-50%); } /* @@ -161,15 +180,16 @@ * shows the box) and while dragging (the action-item `::before`/`::after` are * reused for the drop-line indicators). */ -.style-override .pane-composite-part > .title > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon:not(.checked):hover .active-item-indicator, -.style-override .pane-composite-part > .header-or-footer > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon:not(.checked):hover .active-item-indicator { +.style-override .pane-composite-part > .title > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon:not(.checked):not(:active):hover .active-item-indicator, +.style-override .pane-composite-part > .header-or-footer > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon:not(.checked):not(:active):hover .active-item-indicator { z-index: 0; - top: 4px; + top: 50%; left: 0; width: 24px; height: 24px; border-radius: var(--vscode-cornerRadius-small); background-color: var(--vscode-list-hoverBackground); + transform: translateY(-50%); } /* @@ -264,6 +284,7 @@ .style-override.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon, .style-override.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon { padding: 0 4px; + border-radius: var(--vscode-cornerRadius-small); } /* @@ -282,15 +303,11 @@ .style-override.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label::before, .style-override.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label::before { + position: relative; left: 0px; top: 0px; } -.style-override.monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, -.style-override.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { - position: relative; -} - /* * Auxiliary bar only: give its composite icon items a shorter 24px height so the * single Chat item reads as a compact, balanced chip. Scoped to the auxiliary diff --git a/src/vs/workbench/contrib/styleOverrides/browser/media/tabs.css b/src/vs/workbench/contrib/styleOverrides/browser/media/tabs.css index 882529256d6..dea920b0e60 100644 --- a/src/vs/workbench/contrib/styleOverrides/browser/media/tabs.css +++ b/src/vs/workbench/contrib/styleOverrides/browser/media/tabs.css @@ -16,26 +16,41 @@ * src/vs/sessions/browser/media/style.css. */ -.style-override .part.editor .title.tabs { +.style-override.monaco-workbench { + --modern-ui-tab-active-background: color-mix(in srgb, var(--vscode-foreground) 22%, transparent); + --modern-ui-tab-hover-background: color-mix(in srgb, var(--vscode-foreground) 8%, transparent); + --modern-ui-editor-tab-action-active-background: color-mix(in srgb, var(--vscode-foreground) 22%, var(--vscode-editor-background)); + --modern-ui-editor-tab-action-hover-background: color-mix(in srgb, var(--vscode-foreground) 8%, var(--vscode-editor-background)); +} + +.style-override.monaco-workbench.vs { + --modern-ui-tab-active-background: color-mix(in srgb, var(--vscode-foreground) 16%, transparent); + --modern-ui-tab-hover-background: color-mix(in srgb, var(--vscode-foreground) 6%, transparent); + --modern-ui-editor-tab-action-active-background: color-mix(in srgb, var(--vscode-foreground) 16%, var(--vscode-editor-background)); + --modern-ui-editor-tab-action-hover-background: color-mix(in srgb, var(--vscode-foreground) 6%, var(--vscode-editor-background)); +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs { background-color: transparent !important; + cursor: default; --editor-group-tab-height: 24px !important; } /* Compact tab height: 20px tab + 4px top + 4px bottom padding = 28px total. * 20px is the minimum to fit the tab action icons (16px codicon + 2px padding on each side). */ -.style-override .part.editor .title.tabs.compact-height { +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs.compact-height { --editor-group-tab-height: 20px !important; } .style-override .part.editor .tabs-container > .tab { - background-color: color-mix(in srgb, var(--vscode-foreground) 5%, transparent) !important; + background-color: transparent !important; border-right: none !important; border-radius: var(--vscode-cornerRadius-small); font-size: var(--vscode-fontSize-body1) !important; font-weight: var(--vscode-fontWeight-regular); box-shadow: none !important; margin-right: var(--vscode-spacing-size40) !important; - padding: 0 0 0 4px !important; + padding: 0 var(--vscode-spacing-size40) !important; --tab-border-top-color: transparent !important; } @@ -43,6 +58,10 @@ margin-right: calc(var(--last-tab-margin-right) + var(--vscode-spacing-size40)) !important; } +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab { + border-bottom: none; +} + .style-override .part.editor .tabs-and-actions-container.wrapping .tabs-container { row-gap: var(--vscode-spacing-size40); } @@ -86,11 +105,23 @@ } .style-override .part.editor .tabs-container > .tab.tab-actions-left:not(.sticky-compact) { - padding: 0 10px 0 0 !important; + flex-direction: row; + padding: 0 var(--vscode-spacing-size40) !important; } -.style-override .part.editor .tabs-container > .tab.close-action-off:not(.dirty):not(.sticky-compact) { - padding: 0 8px 0 4px !important; +.style-override .part.editor .tabs-container > .tab.close-action-off:not(.sticky-compact) { + padding: 0 var(--vscode-spacing-size40) !important; +} + +.style-override .part.editor .tabs-container > .tab.dirty:not(.sticky-compact):not(.tab-actions-left):not(.close-action-off.dirty-border-top), +.style-override .part.editor .tabs-container > .tab.sticky:not(.sticky-compact):not(.pinned-action-off):not(.tab-actions-left) { + padding-right: var(--vscode-spacing-size240) !important; +} + +.style-override .part.editor .tabs-container > .tab.dirty.tab-actions-left:not(.sticky-compact), +.style-override .part.editor .tabs-container > .tab.sticky.tab-actions-left:not(.sticky-compact):not(.pinned-action-off) { + padding-left: var(--vscode-spacing-size240) !important; + padding-right: var(--vscode-spacing-size40) !important; } .style-override .part.editor .tabs-container > .tab.sizing-fit:not(.sticky-compact) { @@ -107,7 +138,11 @@ } .style-override .part.editor .tabs-container > .tab.active { - background-color: color-mix(in srgb, var(--vscode-foreground) 18%, transparent) !important; + background-color: var(--modern-ui-tab-active-background) !important; +} + +.style-override .part.editor .tabs-container > .tab .tab-border-bottom-container { + display: none !important; } .style-override .part.editor .tabs-container > .tab.selected:not(.active) { @@ -137,25 +172,8 @@ --tab-border-top-color: var(--vscode-tab-activeBorderTop, var(--vscode-tab-selectedBorderTop)) !important; } -.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):hover { - background-color: color-mix(in srgb, var(--vscode-foreground) 8%, transparent) !important; -} - -/* - * Light themes: the foreground is dark, so the same foreground-based mixes read - * much heavier on a light surface than on a dark one. Tone the tab backgrounds - * down so the pills stay subtle. - */ -.style-override.monaco-workbench.vs .part.editor .tabs-container > .tab { - background-color: color-mix(in srgb, var(--vscode-foreground) 4%, transparent) !important; -} - -.style-override.monaco-workbench.vs .part.editor .tabs-container > .tab.active { - background-color: color-mix(in srgb, var(--vscode-foreground) 10%, transparent) !important; -} - -.style-override.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):hover { - background-color: color-mix(in srgb, var(--vscode-foreground) 6%, transparent) !important; +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):not(.selected):hover { + background-color: var(--modern-ui-tab-hover-background) !important; } .style-override.monaco-workbench .part.editor .tabs-container > .tab:is(.sizing-shrink, .sizing-fixed) > .tab-label > .monaco-icon-label-container { @@ -168,9 +186,116 @@ display: none; } +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:is(.sizing-shrink, .sizing-fixed) > .tab-label { + padding-right: 0; +} + +.style-override.monaco-workbench .part.editor .tabs-container > .tab > .tab-fade-hider { + display: none; +} + +/* + * Overlay tab actions on the label instead of reserving a trailing/leading + * column. The action surface inherits the tab background while revealed so + * label text and icons beneath it do not compete with the action glyph. + */ +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions { + position: absolute; + z-index: 7; + top: 0; + right: 0; + bottom: 0; + margin: 0; + width: 24px; + overflow: visible; + border-radius: 0 var(--vscode-cornerRadius-small) var(--vscode-cornerRadius-small) 0; + pointer-events: none; +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions > .monaco-action-bar { + width: 24px; +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-left > .tab-actions { + right: auto; + left: 0; + border-radius: var(--vscode-cornerRadius-small) 0 0 var(--vscode-cornerRadius-small); +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-actions { + top: var(--vscode-spacing-size20); +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-actions { + top: 0; +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.close-action-off):hover > .tab-actions, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.close-action-off) > .tab-actions:focus-within { + background-color: var(--modern-ui-editor-tab-action-hover-background); + pointer-events: auto; +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:not(.close-action-off):hover > .tab-actions, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:not(.close-action-off) > .tab-actions:focus-within { + background-color: var(--modern-ui-editor-tab-action-active-background); +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected:not(.active):not(.close-action-off):hover > .tab-actions, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected:not(.active):not(.close-action-off) > .tab-actions:focus-within { + --tab-border-top-color: var(--vscode-tab-selectedBorderTop); + background-color: var(--vscode-tab-selectedBackground); +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.multi-selected:not(.close-action-off):hover > .tab-actions, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.multi-selected:not(.close-action-off) > .tab-actions:focus-within { + --tab-border-top-color: var(--vscode-tab-activeBorderTop, var(--vscode-tab-selectedBorderTop)); +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected:not(.active).tab-border-top:not(.close-action-off):hover > .tab-actions, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected:not(.active).tab-border-top:not(.close-action-off) > .tab-actions:focus-within, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.multi-selected.tab-border-top:not(.close-action-off):hover > .tab-actions, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.multi-selected.tab-border-top:not(.close-action-off) > .tab-actions:focus-within { + border-top: var(--vscode-strokeThickness) solid var(--tab-border-top-color); + border-right: var(--vscode-strokeThickness) solid var(--tab-border-top-color); + border-bottom: var(--vscode-strokeThickness) solid var(--tab-border-top-color); + box-sizing: border-box; +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected:not(.active).tab-border-top.tab-actions-left:not(.close-action-off):hover > .tab-actions, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected:not(.active).tab-border-top.tab-actions-left:not(.close-action-off) > .tab-actions:focus-within, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.multi-selected.tab-border-top.tab-actions-left:not(.close-action-off):hover > .tab-actions, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.multi-selected.tab-border-top.tab-actions-left:not(.close-action-off) > .tab-actions:focus-within { + border-right: 0; + border-left: var(--vscode-strokeThickness) solid var(--tab-border-top-color); +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.dirty):not(.sticky):not(:hover) > .tab-actions .action-label:not(:focus) { + opacity: 0; +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-actions .action-label, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.close-action-off):hover > .tab-actions .action-label, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .action-label:focus { + opacity: 1; +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty:not(.close-action-off):hover > .tab-actions .action-label.codicon-close::before, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-actions .action-label.codicon-close:focus::before { + content: var(--vscode-icon-close-content); + font-family: var(--vscode-icon-close-font-family); +} + +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty:not(.close-action-off):hover > .tab-actions .action-label.codicon-pinned::before, +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-actions .action-label.codicon-pinned:focus::before { + content: var(--vscode-icon-pinned-content); + font-family: var(--vscode-icon-pinned-font-family); +} + /* Keep scrolling tabs from showing through compact pinned pills and their spacing. */ .style-override .part.editor .tabs-container > .tab.sticky-compact { - background-color: color-mix(in srgb, var(--vscode-foreground) 5%, var(--vscode-editor-background)) !important; + background-color: transparent !important; } .style-override .part.editor .tabs-and-actions-container > .monaco-scrollable-element > .sticky-tabs-background { @@ -185,34 +310,35 @@ } .style-override.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar { - z-index: 6; + z-index: 11; } .style-override .part.editor .tabs-container > .tab.sticky-compact.active { - background-color: color-mix(in srgb, var(--vscode-foreground) 18%, var(--vscode-editor-background)) !important; + background-color: var(--modern-ui-tab-active-background) !important; } -.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.sticky-compact:not(.active):hover { - background-color: color-mix(in srgb, var(--vscode-foreground) 8%, var(--vscode-editor-background)) !important; +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.sticky-compact:not(.active):not(.selected):hover { + background-color: var(--modern-ui-tab-hover-background) !important; } -.style-override.monaco-workbench.vs .part.editor .tabs-container > .tab.sticky-compact { - background-color: color-mix(in srgb, var(--vscode-foreground) 4%, var(--vscode-editor-background)) !important; -} - -.style-override.monaco-workbench.vs .part.editor .tabs-container > .tab.sticky-compact.active { - background-color: color-mix(in srgb, var(--vscode-foreground) 10%, var(--vscode-editor-background)) !important; -} - -.style-override.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.sticky-compact:not(.active):hover { - background-color: color-mix(in srgb, var(--vscode-foreground) 6%, var(--vscode-editor-background)) !important; -} - -.style-override .part.editor .tabs-container > .tab .tab-border-top-container, -.style-override .part.editor .tabs-container > .tab .tab-border-bottom-container { +.style-override .part.editor .tabs-container > .tab .tab-border-top-container { display: none !important; } +.style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.dirty-border-top > .tab-border-top-container { + display: block !important; + position: absolute; + z-index: 8; + top: 0; + left: 0; + width: 100% !important; + height: 2px !important; + border: none; + border-radius: var(--vscode-cornerRadius-small) var(--vscode-cornerRadius-small) 0 0; + background-color: var(--tab-dirty-border-top-color) !important; + pointer-events: none; +} + .style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.drop-target-left:not(.last-in-row):not(:last-child)::after { display: none; } @@ -223,96 +349,194 @@ } /* - * Panel tabs (TERMINAL, PROBLEMS, OUTPUT, DEBUG CONSOLE ...). These are - * composite-bar action items rather than editor `.tab` elements, with an - * underline `.active-item-indicator`. Give them the same rounded-pill look as - * the editor tabs: normal case, Body 1 / 600 type, a subtle background on the - * active tab and no underline. + * Pane tabs (primary side bar, panel and auxiliary side bar) are text composite + * actions. Keep icon-only activity items on their existing treatment. */ -.style-override .part.panel > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { +.style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon), +.style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) { text-transform: none !important; padding: 0 8px; border-radius: var(--vscode-cornerRadius-small); } -/* - * The text lives in `.action-label`, which carries its own `font-size: 11px` - * from the base action bar styles. Setting the size on `.action-item` alone - * does not reach the label (an explicit font-size on the element wins over the - * inherited value), so the type must be set on the label itself. - */ -.style-override .part.panel > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { +.style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, +.style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label { font-size: var(--vscode-fontSize-body1); font-weight: var(--vscode-fontWeight-semiBold); line-height: 22px; /* keep consistent with other 22px title/control heights */ } -.style-override .part.panel > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked { - background-color: color-mix(in srgb, var(--vscode-foreground) 10%, transparent) !important; +.style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon).checked, +.style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon).checked, +.style-override.monaco-workbench:not(.hc-black):not(.hc-light) .part.auxiliarybar > .header-or-footer > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon.checked:not(:active), +.style-override.monaco-workbench:not(.hc-black):not(.hc-light) .pane-composite-part.basepanel > .title > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon.checked:not(:active) { + background-color: var(--modern-ui-tab-active-background) !important; +} + +.style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon):not(.checked):hover, +.style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon):not(.checked):hover { + background-color: var(--modern-ui-tab-hover-background); +} + +.style-override.monaco-workbench:not(.hc-black):not(.hc-light) .part.auxiliarybar > .header-or-footer > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon:not(.checked):not(:active):hover, +.style-override.monaco-workbench:not(.hc-black):not(.hc-light) .pane-composite-part.basepanel > .title > .composite-bar-container:not(.dragged-over):not(.dragged-over-head):not(.dragged-over-tail) > .composite-bar > .monaco-action-bar .action-item.icon:not(.checked):not(:active):hover { + background-color: var(--vscode-list-hoverBackground); +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab, +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.active, +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.selected, +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:hover { + background-color: transparent !important; + box-shadow: none !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:hover > .tab-actions, +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab > .tab-actions:focus-within { + background-color: var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-editor-background)) !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab > .tab-actions .action-label { + background-color: transparent !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:is(.active, .selected):hover > .tab-actions { + border-top: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); + border-right: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); + border-bottom: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); + box-sizing: border-box; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):not(.selected):hover > .tab-actions { + border-top: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); + border-right: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); + border-bottom: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); + box-sizing: border-box; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab > .tab-actions:focus-within { + border-top: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); + border-right: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); + border-bottom: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); + box-sizing: border-box; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:is(.active, .selected).tab-actions-left:hover > .tab-actions { + border-right: 0; + border-left: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):not(.selected).tab-actions-left:hover > .tab-actions { + border-right: 0; + border-left: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.tab-actions-left > .tab-actions:focus-within { + border-right: 0; + border-left: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:is(.active, .selected):not(:focus) { + outline: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder) !important; + outline-offset: calc(-1 * var(--vscode-strokeThickness)) !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):not(.selected):not(:focus):hover { + background-color: transparent !important; + outline: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder) !important; + outline-offset: calc(-1 * var(--vscode-strokeThickness)) !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:focus { + background-color: transparent !important; + outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder) !important; + outline-offset: calc(-1 * var(--vscode-strokeThickness)) !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):not(.selected):focus:hover { + background-color: transparent !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab .tab-label, +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab .tab-label a { + outline: none !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.active > .tab-border-bottom-container { + display: none; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:hover > .tab-border-bottom-container { + display: none; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.dirty):not(.sticky):not(:hover) > .tab-actions .action-label:not(:focus) { + opacity: 0 !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.dirty > .tab-actions .action-label, +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label, +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:hover > .tab-actions .action-label, +:is(.hc-black, .hc-light).style-override.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab > .tab-actions .action-label:focus { + opacity: 1 !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item, +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item, +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator, +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { + background-color: transparent !important; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon, +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon { + box-sizing: border-box; + flex: 0 0 24px; + width: 24px; + min-width: 24px; + max-width: 24px; + height: 24px; + min-height: 24px; + max-height: 24px; + padding: 0; +} + +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +:is(.hc-black, .hc-light).style-override.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { + outline: none !important; +} + +:is(.hc-black, .hc-light).style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator:before, +:is(.hc-black, .hc-light).style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator:before { + display: none !important; +} + +:is(.hc-black, .hc-light).style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus), +:is(.hc-black, .hc-light).style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) { + outline: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); + outline-offset: calc(-1 * var(--vscode-strokeThickness)); +} + +:is(.hc-black, .hc-light).style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):not(:focus):hover, +:is(.hc-black, .hc-light).style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):not(:focus):hover { + outline: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); + outline-offset: calc(-1 * var(--vscode-strokeThickness)); +} + +:is(.hc-black, .hc-light).style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus, +:is(.hc-black, .hc-light).style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus { + outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder) !important; + outline-offset: calc(-1 * var(--vscode-strokeThickness)); } /* - * Drop the underline active indicator in favour of the pill background. The - * default rules set the border with `!important`, so match their specificity - * (with the extra `.style-override` class winning) for the checked and - * focused states. + * Drop the checked underline in favour of the rounded background while retaining + * the base keyboard-focus indicator. */ -.style-override .part.panel > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, -.style-override .part.panel > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { - border-top-color: transparent !important; - border-top-width: 0 !important; - border-bottom-color: transparent !important; - border-bottom-width: 0 !important; -} - -/* - * Auxiliary bar (secondary side bar) composite tabs (e.g. CHAT, EXTENSIONS). - * Same composite-bar action items as the panel tabs, but the active view - * switcher can live under `.title` or the activity-bar `.header-or-footer` - * depending on the activity bar position, so both locations are covered. Give - * them the rounded-pill look: normal case, Body 1 / 600 type, a subtle background - * on the active tab and no underline indicator. - */ -.style-override .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item, -.style-override .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { - text-transform: none !important; - padding: 0 8px; - border-radius: var(--vscode-cornerRadius-small); -} - -/* - * The text lives in `.action-label`, which carries its own `font-size: 11px` - * from the base action bar styles, so the type must be set on the label itself. - */ -.style-override .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, -.style-override .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { - font-size: var(--vscode-fontSize-body1); - font-weight: var(--vscode-fontWeight-semiBold); - line-height: 22px; /* keep consistent with other 22px title/control heights */ -} - -.style-override .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, -.style-override .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { - font-size: var(--vscode-fontSize-body1); - font-weight: var(--vscode-fontWeight-semiBold); -} - -.style-override .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked, -.style-override .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked { - background-color: color-mix(in srgb, var(--vscode-foreground) 10%, transparent) !important; -} - -/* - * Drop the underline active indicator in favour of the pill background. The - * base rules (auxiliaryBarPart.css / paneCompositePart.css) paint the indicator - * with `!important` from `.part.auxiliarybar` selectors, so match their - * specificity (with the extra `.style-override` class winning) across the - * `.title` and `.header-or-footer` checked and focused states. - */ -.style-override .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, -.style-override .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, -.style-override .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, -.style-override .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.style-override .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon).checked:not(:focus) .active-item-indicator:before, +.style-override .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon).checked:not(:focus) .active-item-indicator:before { border-top-color: transparent !important; border-top-width: 0 !important; border-bottom-color: transparent !important; From 924d37b4594db1078129a60ed6cfa6d359f58aa5 Mon Sep 17 00:00:00 2001 From: Mir <mirimadahmed@outlook.com> Date: Sat, 1 Aug 2026 00:24:24 +0800 Subject: [PATCH 75/86] Make coding agent voice aware for better voice experience (#328217) * Add voice-aware progress to Copilot Agent mode Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Gate voice-aware agent progress Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Mir Imad Ahmed <mirimadahmed@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../copilotcli/node/copilotcliSession.ts | 1 + .../codeBlocks/node/codeBlockProcessor.ts | 1 + .../extension/intents/node/toolCallingLoop.ts | 334 ++- .../node/toolCallingLoopAutopilot.spec.ts | 254 +- .../common/responseStreamWithLinkification.ts | 6 +- .../node/defaultIntentRequestHandler.ts | 2 + .../pseudoStartStopConversationCallback.ts | 7 +- .../test/defaultIntentRequestHandler.spec.ts | 63 +- .../prompts/node/agent/agentPrompt.tsx | 6 + .../node/agent/test/agentPrompt.spec.tsx | 29 + ...seudoStartStopConversationCallback.spec.ts | 40 + .../src/util/common/chatResponseStreamImpl.ts | 8 +- .../src/util/common/test/shims/chatTypes.ts | 7 + .../util/common/test/shims/vscodeTypesShim.ts | 3 +- extensions/copilot/src/vscodeTypes.ts | 1 + .../workbench/api/common/extHost.api.impl.ts | 1 + .../api/common/extHostChatAgents2.ts | 7 + .../api/common/extHostTypeConverters.ts | 17 +- src/vs/workbench/api/common/extHostTypes.ts | 11 + .../test/common/extHostTypeConverters.test.ts | 11 +- .../browser/agentsVoice.contribution.ts | 8 + .../browser/actions/chatAccessibilityHelp.ts | 3 + src/vs/workbench/contrib/chat/browser/chat.ts | 1 + .../browser/voiceClient/voiceClientService.ts | 111 +- .../voiceClient/voiceSessionController.ts | 1109 ++++++-- .../browser/voiceClient/voiceTelemetry.ts | 5 +- .../voiceClient/voiceToolDispatchService.ts | 35 +- .../contrib/chat/browser/widget/chatWidget.ts | 1 + .../widgetHosts/viewPane/chatViewPane.ts | 7 +- .../chat/common/chatService/chatService.ts | 10 + .../common/chatService/chatServiceImpl.ts | 1 + .../contrib/chat/common/model/chatModel.ts | 15 +- .../common/model/chatSessionOperationLog.ts | 8 +- .../chat/common/participants/chatAgents.ts | 4 + .../common/voiceClient/voiceClientService.ts | 31 +- .../common/voiceClient/voiceConfirmation.ts | 58 + .../contrib/chat/common/widget/annotations.ts | 2 + .../chatAccessibilityHelp.test.ts | 14 + .../voiceClient/voiceClientService.test.ts | 229 +- .../voiceSessionController.test.ts | 2527 ++++++++++++++++- .../voiceToolDispatchService.test.ts | 74 +- .../common/chatService/chatService.test.ts | 20 + .../chat/test/common/model/chatModel.test.ts | 39 +- .../test/common/widget/annotations.test.ts | 10 + ...scode.proposed.chatParticipantPrivate.d.ts | 40 + 45 files changed, 4841 insertions(+), 330 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/voiceClient/voiceConfirmation.ts diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts index f90561a23ce..0b5a30fb7f4 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts @@ -107,6 +107,7 @@ class CopilotCLIResponseStreamRouter { push: (part: vscode.ExtendedChatResponsePart): void => { this._call('push', [part]); }, thinkingProgress: (thinkingDelta: vscode.ThinkingDelta): void => { this._call('thinkingProgress', [thinkingDelta]); }, hookProgress: (hookType: vscode.ChatHookType, stopReason?: string, systemMessage?: string): void => { this._call('hookProgress', [hookType, stopReason, systemMessage]); }, + voiceProgress: (id: string, value: string): void => { this._call('voiceProgress', [id, value]); }, textEdit: (target: vscode.Uri, editsOrDone: vscode.TextEdit | vscode.TextEdit[] | true): void => { this._call('textEdit', [target, editsOrDone]); }, notebookEdit: (target: vscode.Uri, editsOrDone: vscode.NotebookEdit | vscode.NotebookEdit[] | true): void => { this._call('notebookEdit', [target, editsOrDone]); }, workspaceEdit: (edits: vscode.ChatWorkspaceFileEdit[]): void => { this._call('workspaceEdit', [edits]); }, diff --git a/extensions/copilot/src/extension/codeBlocks/node/codeBlockProcessor.ts b/extensions/copilot/src/extension/codeBlocks/node/codeBlockProcessor.ts index daec78f3059..cfbeb0acb13 100644 --- a/extensions/copilot/src/extension/codeBlocks/node/codeBlockProcessor.ts +++ b/extensions/copilot/src/extension/codeBlocks/node/codeBlockProcessor.ts @@ -127,6 +127,7 @@ export class CodeBlockTrackingChatResponseStream implements ChatResponseStream { warning = this.forward(this._wrapped.warning.bind(this._wrapped)); info = this.forward(this._wrapped.info.bind(this._wrapped)); hookProgress = this.forward(this._wrapped.hookProgress.bind(this._wrapped)); + voiceProgress = this.forward(this._wrapped.voiceProgress.bind(this._wrapped)); reference2 = this.forward(this._wrapped.reference2.bind(this._wrapped)); codeCitation = this.forward(this._wrapped.codeCitation.bind(this._wrapped)); anchor = this.forward(this._wrapped.anchor.bind(this._wrapped)); diff --git a/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts b/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts index fbe2039c50e..ca068398332 100644 --- a/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts +++ b/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts @@ -35,12 +35,13 @@ import { DeferredPromise, timeout } from '../../../util/vs/base/common/async'; import { CancellationTokenSource } from '../../../util/vs/base/common/cancellation'; import { CancellationError, isCancellationError } from '../../../util/vs/base/common/errors'; import { Emitter } from '../../../util/vs/base/common/event'; +import { stringHash } from '../../../util/vs/base/common/hash'; import { Disposable, IDisposable } from '../../../util/vs/base/common/lifecycle'; import { Mutable } from '../../../util/vs/base/common/types'; import { URI } from '../../../util/vs/base/common/uri'; import { generateUuid } from '../../../util/vs/base/common/uuid'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; -import { ChatResponsePullRequestPart, LanguageModelDataPart2, LanguageModelPartAudience, LanguageModelToolResult2, MarkdownString } from '../../../vscodeTypes'; +import { ChatResponsePullRequestPart, LanguageModelDataPart2, LanguageModelPartAudience, LanguageModelTextPart, LanguageModelToolResult2, MarkdownString } from '../../../vscodeTypes'; import { InteractionOutcomeComputer } from '../../inlineChat/node/promptCraftingTypes'; import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection'; import { Conversation, IResultMetadata, ResponseStreamParticipant, TurnStatus, TurnTokenUsageMetadata } from '../../prompt/common/conversation'; @@ -52,7 +53,7 @@ import { PseudoStopStartResponseProcessor } from '../../prompt/node/pseudoStartS import { ResponseProcessorContext } from '../../prompt/node/responseProcessorContext'; import { SummarizedConversationHistoryMetadata } from '../../prompts/node/agent/summarizedConversationHistory'; import { ToolFailureEncountered, ToolResultMetadata } from '../../prompts/node/panel/toolCalling'; -import { ToolName } from '../../tools/common/toolNames'; +import { getToolName, ToolName } from '../../tools/common/toolNames'; import { IToolsService, ToolCallCancelledError } from '../../tools/common/toolsService'; import { ReadFileParams } from '../../tools/node/readFileTool'; import { isHookAbortError, processHookResults } from './hookResultProcessor'; @@ -86,6 +87,10 @@ export interface IToolCallingLoopOptions { * The current chat request */ request: ChatRequest; + /** + * Enables deterministic Voice Mode progress for the top-level Agent loop. + */ + enableVoiceProgress?: boolean; /** * A getter that returns true if VS Code has requested the extension to * gracefully yield. When set, it's likely that the editor will immediately @@ -145,6 +150,171 @@ interface SubagentStopHookResult { readonly reasons?: readonly string[]; } +type VoiceProgressPhase = 'investigating' | 'planning' | 'editing' | 'validating' | 'recovering'; + +interface VoiceProgressToolInput { + readonly stage: VoiceProgressPhase; + readonly summary: string; +} + +type VoiceProgressToolInputResult = { readonly input: VoiceProgressToolInput } | { readonly error: string }; + +const voiceProgressPhases = new Set<VoiceProgressPhase>(['investigating', 'planning', 'editing', 'validating', 'recovering']); +const voiceProgressSummaryMaxLength = 240; +const unsafeVoiceProgressSummaryPattern = /[`*_#\[\]<>]|(?:https?:\/\/|file:\/\/)|(?:^|\s)(?:\.{0,2}[\\/]|[A-Za-z]:\\)|\b[\w.-]+\/[\w./-]+\b|\b[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+\b|\b(?:gh[pousr]_|AKIA)[A-Za-z0-9_-]+|\b[0-9a-f]{8}-[0-9a-f-]{27,}\b|\b[0-9a-f]{32,}\b/i; + +const editingToolNames = new Set<string>([ + ToolName.ApplyPatch, + ToolName.CreateDirectory, + ToolName.CreateFile, + ToolName.CreateNewJupyterNotebook, + ToolName.EditFile, + ToolName.EditNotebook, + ToolName.MultiReplaceString, + ToolName.ReplaceString, +]); + +const validationToolNames = new Set<string>([ + ToolName.CoreCreateAndRunTask, + ToolName.CoreRunTask, + ToolName.CoreRunTest, + ToolName.GetErrors, + ToolName.RunNotebookCell, +]); + +const investigatingToolNames = new Set<string>([ + ToolName.Codebase, + ToolName.VSCodeAPI, + ToolName.FindFiles, + ToolName.FindTextInFiles, + ToolName.ReadFile, + ToolName.ViewImage, + ToolName.ListDirectory, + ToolName.GetScmChanges, + ToolName.ReadProjectStructure, + ToolName.SearchWorkspaceSymbols, + ToolName.GetNotebookSummary, + ToolName.ReadCellOutput, + ToolName.FetchWebPage, + ToolName.FindTestFiles, + ToolName.GithubSemanticRepoSearch, + ToolName.GithubTextSearch, + ToolName.SearchSubagent, + ToolName.ExploreSubagent, + ToolName.CoreRunSubagent, + ToolName.ToolSearch, + ToolName.CoreReadPage, + ToolName.CoreScreenshotPage, +]); + +const planningToolNames = new Set<string>([ + ToolName.CoreManageTodoList, + ToolName.CoreReviewPlan, + ToolName.CoreAskQuestions, +]); + +function isEditingTool(name: string): boolean { + return editingToolNames.has(getToolName(name)); +} + +function isInvestigatingTool(name: string): boolean { + const toolName = getToolName(name); + return investigatingToolNames.has(toolName) || /(?:^|_)(?:explore|find|grep|inspect|list|read|search)(?:_|$)/i.test(toolName); +} + +function isPlanningTool(name: string): boolean { + const toolName = getToolName(name); + return planningToolNames.has(toolName) || /(?:askQuestions|artifact|plan|todo)/i.test(toolName); +} + +function isValidationToolCall(call: IToolCall): boolean { + const name = getToolName(call.name); + if (validationToolNames.has(name)) { + return true; + } + return name === ToolName.CoreRunInTerminal && /\b(?:build|check|compile|lint|test|typecheck)\b/i.test(call.arguments); +} + +function isVoiceProgressPhase(value: string): value is VoiceProgressPhase { + return voiceProgressPhases.has(value as VoiceProgressPhase); +} + +function parseVoiceProgressToolInput(argumentsJson: string): VoiceProgressToolInputResult { + let value: unknown; + try { + value = JSON.parse(argumentsJson); + } catch { + return { error: 'the input must be valid JSON' }; + } + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return { error: 'the input must be an object' }; + } + const input = value as Record<string, unknown>; + if (Object.keys(input).some(key => key !== 'stage' && key !== 'summary')) { + return { error: 'only stage and summary are allowed' }; + } + if (typeof input.stage !== 'string' || !isVoiceProgressPhase(input.stage)) { + return { error: 'stage must be investigating, planning, editing, validating, or recovering' }; + } + if (typeof input.summary !== 'string') { + return { error: 'summary must be a string' }; + } + const summary = input.summary.replace(/\s+/g, ' ').trim(); + if (!summary) { + return { error: 'summary must not be empty' }; + } + if (summary.length > voiceProgressSummaryMaxLength) { + return { error: `summary must be at most ${voiceProgressSummaryMaxLength} characters` }; + } + if (unsafeVoiceProgressSummaryPattern.test(summary)) { + return { error: 'summary must use plain speech without markdown, paths, commands, identifiers, URLs, or secrets' }; + } + return { input: { stage: input.stage, summary } }; +} + +function getVoiceProgressMessage(phase: VoiceProgressPhase, requestId: string): string { + let variants: readonly string[]; + switch (phase) { + case 'investigating': + variants = [ + l10n.t("I'm tracing the relevant code now."), + l10n.t("I'm looking through the code to find the right path."), + l10n.t("I'm investigating how this fits together."), + ]; + break; + case 'planning': + variants = [ + l10n.t("I've got the context. I'm working out the approach."), + l10n.t("I'm mapping out the cleanest change now."), + l10n.t("I've found the path. I'm planning the update."), + ]; + break; + case 'editing': + variants = [ + l10n.t("Found the spot. I'm making the change now."), + l10n.t("There it is. I'm updating the code."), + l10n.t("I've got the change point. Making the edit now."), + ]; + break; + case 'validating': + variants = [ + l10n.t("Nice, that's in. I'm checking it now."), + l10n.t("The update's ready. I'm putting it through its checks."), + l10n.t("Good progress. I'm verifying everything now."), + ]; + break; + case 'recovering': + variants = [ + l10n.t("That hit a snag. I'm switching approaches."), + l10n.t("Small detour. I'm trying a better route."), + l10n.t("Not quite. I've got another angle to try."), + ]; + break; + } + const index = (stringHash(`${requestId}:${phase}`, 0) >>> 0) % variants.length; + return variants[index]; +} + /** * Formats a hook context message from blocking reasons. * @param reasons The reasons hooks blocked the agent from stopping @@ -168,6 +338,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = private static NextToolCallId = Date.now(); private static readonly TASK_COMPLETE_TOOL_NAME = 'task_complete'; + private static readonly VOICE_PROGRESS_TOOL_NAME = 'report_voice_progress'; private toolCallResults: Record<string, LanguageModelToolResult2> = Object.create(null); private toolCallRounds: IToolCallRound[] = []; @@ -179,6 +350,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = private toolsAvailableEmitted = false; private lastHeaderRequestId: string | undefined; private lastModelCallId: string | undefined; + private readonly reportedVoiceProgress = new Set<VoiceProgressPhase>(); /** * Running total of Copilot credits across every model call in the current @@ -654,6 +826,134 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = } } + private isVoiceProgressEnabled(): boolean { + return Boolean(this.options.enableVoiceProgress && this.options.request.isVoiceModeInput && !this.options.request.subAgentInvocationId); + } + + protected reportVoiceProgress(outputStream: ChatResponseStream | undefined, phase: VoiceProgressPhase, summary?: string): boolean { + if (!this.options.enableVoiceProgress || !this.options.request.isVoiceModeInput || this.options.request.subAgentInvocationId || this.reportedVoiceProgress.has(phase)) { + return false; + } + this.reportedVoiceProgress.add(phase); + outputStream?.voiceProgress(phase, summary ?? getVoiceProgressMessage(phase, this.options.request.id)); + this._logService.info(`[VoiceProgress] emitted request=${this.options.request.id} phase=${phase} source=${summary ? 'model' : 'fallback'} stream=${Boolean(outputStream)}`); + return true; + } + + protected getVoiceProgressFallbackPhase(round: IToolCallRound): VoiceProgressPhase | undefined { + const hasEditingTool = round.toolCalls.some(call => isEditingTool(call.name)); + const validationFailed = round.toolCalls.some(call => getToolName(call.name) === ToolName.CoreTestFailure); + if (validationFailed || (hasEditingTool && this.reportedVoiceProgress.has('validating'))) { + return 'recovering'; + } + if (!this.reportedVoiceProgress.has('editing') && hasEditingTool) { + return 'editing'; + } + if (round.toolCalls.some(isValidationToolCall)) { + return 'validating'; + } + if (round.toolCalls.some(call => isPlanningTool(call.name))) { + return 'planning'; + } + if (round.toolCalls.some(call => isInvestigatingTool(call.name))) { + return 'investigating'; + } + return undefined; + } + + protected reportVoiceProgressForRound(outputStream: ChatResponseStream | undefined, round: IToolCallRound): void { + const phase = this.getVoiceProgressFallbackPhase(round); + if (!phase) { + return; + } + const emitted = this.reportVoiceProgress(outputStream, phase); + this._logService.info(`[VoiceProgress] fallback request=${this.options.request.id} phase=${phase} emitted=${emitted} tools=${round.toolCalls.map(call => getToolName(call.name)).join(',')}`); + } + + protected ensureVoiceProgressTool(availableTools: LanguageModelToolInformation[]): LanguageModelToolInformation[] { + if (!this.isVoiceProgressEnabled() || availableTools.some(tool => tool.name === ToolCallingLoop.VOICE_PROGRESS_TOOL_NAME)) { + return availableTools; + } + this._logService.info(`[VoiceProgress] injected tool request=${this.options.request.id} availableTools=${availableTools.length + 1}`); + return [...availableTools, { + name: ToolCallingLoop.VOICE_PROGRESS_TOOL_NAME, + description: 'Report one concise factual spoken progress update to the user at a meaningful stage change. Call this in parallel with actual work when possible. Do not use it for acknowledgements, questions, confirmations, or the final response.', + inputSchema: { + type: 'object', + properties: { + stage: { + type: 'string', + enum: ['investigating', 'planning', 'editing', 'validating', 'recovering'], + description: 'The current work stage.', + }, + summary: { + type: 'string', + minLength: 1, + maxLength: voiceProgressSummaryMaxLength, + description: 'A concise user-facing factual update in plain speech, without markdown, paths, commands, identifiers, secrets, reasoning, or raw source and tool output.', + }, + }, + required: ['stage', 'summary'], + additionalProperties: false, + }, + tags: [], + source: undefined, + }]; + } + + protected processVoiceProgressToolCalls(outputStream: ChatResponseStream | undefined, toolCalls: readonly IToolCall[]): void { + if (!this.isVoiceProgressEnabled()) { + return; + } + for (const toolCall of toolCalls) { + if (toolCall.name !== ToolCallingLoop.VOICE_PROGRESS_TOOL_NAME) { + continue; + } + const parsed = parseVoiceProgressToolInput(toolCall.arguments); + let resultMessage: string; + if ('error' in parsed) { + resultMessage = `Voice progress was not reported because ${parsed.error}.`; + } else if (this.reportVoiceProgress(outputStream, parsed.input.stage, parsed.input.summary)) { + resultMessage = 'Voice progress reported.'; + } else { + resultMessage = `Voice progress for ${parsed.input.stage} was already reported.`; + } + this.toolCallResults[toolCall.id] = new LanguageModelToolResult2([new LanguageModelTextPart(resultMessage)]); + this._logService.info(`[VoiceProgress] processed model tool request=${this.options.request.id} call=${toolCall.id} valid=${!('error' in parsed)}`); + } + } + + protected hasProductiveToolCalls(round: IToolCallRound): boolean { + return round.toolCalls.some(toolCall => + toolCall.name !== ToolCallingLoop.TASK_COMPLETE_TOOL_NAME + && toolCall.name !== ToolCallingLoop.VOICE_PROGRESS_TOOL_NAME + ); + } + + protected getPersistableToolCallingState(): { toolCallRounds: IToolCallRound[]; toolCallResults: Record<string, LanguageModelToolResult2> } { + const toolCallRounds: IToolCallRound[] = []; + const toolCallResults: Record<string, LanguageModelToolResult2> = {}; + for (const round of this.toolCallRounds) { + const persistableRound = this.withoutVoiceProgressToolCalls(round); + if (!persistableRound.toolCalls.length && !persistableRound.response && !persistableRound.thinking && !persistableRound.statefulMarker && !persistableRound.compaction && !persistableRound.hookContext) { + continue; + } + toolCallRounds.push(persistableRound); + for (const toolCall of persistableRound.toolCalls) { + const result = this.toolCallResults[toolCall.id]; + if (result) { + toolCallResults[toolCall.id] = result; + } + } + } + return { toolCallRounds, toolCallResults }; + } + + private withoutVoiceProgressToolCalls(round: IToolCallRound): IToolCallRound { + const toolCalls = round.toolCalls.filter(toolCall => toolCall.name !== ToolCallingLoop.VOICE_PROGRESS_TOOL_NAME); + return toolCalls.length === round.toolCalls.length ? round : { ...round, toolCalls }; + } + /** * Ensures the `task_complete` tool is present in the available tools when running in * autopilot mode. If it's missing (e.g. filtered out by the tool picker), it's resolved @@ -1133,6 +1433,8 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = this.agentSpan = agentSpan; this.chatSessionIdForTools = chatSessionId; this.toolsAvailableEmitted = false; + this.reportedVoiceProgress.clear(); + this._logService.info(`[VoiceProgress] loop request=${this.options.request.id} configured=${Boolean(this.options.enableVoiceProgress)} voice=${Boolean(this.options.request.isVoiceModeInput)} subagent=${Boolean(this.options.request.subAgentInvocationId)} stream=${Boolean(outputStream)}`); while (true) { if (lastResult && i++ >= this.options.toolCallLimit) { @@ -1162,6 +1464,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = agentSpan?.addEvent('turn_start', { turnId, ...(chatSessionId ? { [CopilotChatAttr.CHAT_SESSION_ID]: chatSessionId } : {}) }); this.resolveAutopilotProgress(); const result = await this.runOne(outputStream, i, token); + this.reportVoiceProgressForRound(outputStream, result.round); if (lastRequestMessagesStartingIndexForRun === undefined) { lastRequestMessagesStartingIndexForRun = result.lastRequestMessages.length - 1; } @@ -1176,7 +1479,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = // If the model produced productive (non-task_complete) tool calls after being nudged, // reset the stop hook flag and iteration count so it can be nudged again. - if (this.autopilotStopHookActive && result.round.toolCalls.length && !result.round.toolCalls.some(tc => tc.name === ToolCallingLoop.TASK_COMPLETE_TOOL_NAME)) { + if (this.autopilotStopHookActive && this.hasProductiveToolCalls(result.round)) { this.autopilotStopHookActive = false; this.autopilotIterationCount = 0; } @@ -1191,6 +1494,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = if (result.response.type !== ChatFetchResponseType.Success && this.shouldAutoRetry(result.response)) { this.autopilotRetryCount++; this._logService.info(`[ToolCallingLoop] Auto-retrying on error (attempt ${this.autopilotRetryCount}/${ToolCallingLoop.MAX_AUTOPILOT_RETRIES}): ${result.response.type}`); + this.reportVoiceProgress(outputStream, 'recovering'); if (this.options.request.permissionLevel === 'autopilot') { this.showAutopilotProgress(outputStream, l10n.t('Autopilot: recovering from a request error\u2026'), l10n.t('Autopilot recovered from a request error')); } else { @@ -1288,7 +1592,12 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = } } } - return { ...lastResult, toolCallRounds: this.toolCallRounds, toolCallResults: this.toolCallResults }; + const persistableState = this.getPersistableToolCallingState(); + return { + ...lastResult, + round: this.withoutVoiceProgressToolCalls(lastResult.round), + ...persistableState, + }; } private async emitReadFileTrajectories() { @@ -1468,9 +1777,12 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = } // Ensure task_complete is available in autopilot mode so the model can signal completion - availableTools = this.ensureAutopilotTools(availableTools); + availableTools = this.ensureVoiceProgressTool(this.ensureAutopilotTools(availableTools)); const isToolInputFailure = effectiveBuildPromptResult.metadata.get(ToolFailureEncountered); + if (isToolInputFailure) { + this.reportVoiceProgress(outputStream, 'recovering'); + } const conversationSummary = effectiveBuildPromptResult.metadata.get(SummarizedConversationHistoryMetadata); if (conversationSummary) { this.turn.setMetadata(conversationSummary); @@ -1525,7 +1837,10 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = chatResult = await that.options.responseProcessor.processResponse(this.context, inputStream, responseStream, token); } else { const subagentInvocationId = getSubAgentInvocationId(context); - const responseProcessor = that._instantiationService.createInstance(PseudoStopStartResponseProcessor, [], undefined, { subagentInvocationId }); + const responseProcessor = that._instantiationService.createInstance(PseudoStopStartResponseProcessor, [], undefined, { + subagentInvocationId, + hiddenToolNames: that.isVoiceProgressEnabled() ? new Set([ToolCallingLoop.VOICE_PROGRESS_TOOL_NAME]) : undefined, + }); await responseProcessor.processResponse(this.context, inputStream, responseStream, token); } return chatResult; @@ -1626,6 +1941,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = const fetchResult = await this.fetch(fetchOptions, token).finally(() => { this.stopHookUserInitiated = false; }); + this.processVoiceProgressToolCalls(outputStream, toolCalls); markChatExt(this.options.conversation.sessionId, ChatExtPerfMark.DidFetch); // Store the server-echoed headerRequestId from the fetch response for subagent telemetry linking. @@ -1711,12 +2027,14 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions = thinkingItem?.updateWithFetchResult(fetchResult); // Log the assistant message to the transcript - const transcriptToolRequests: ToolRequest[] = toolCalls.map(tc => ({ + const transcriptToolRequests: ToolRequest[] = toolCalls + .filter(toolCall => toolCall.name !== ToolCallingLoop.VOICE_PROGRESS_TOOL_NAME) + .map(tc => ({ toolCallId: tc.id, name: tc.name, arguments: tc.arguments, type: 'function' as const, - })); + })); this._sessionTranscriptService.logAssistantMessage( this.options.conversation.sessionId, fetchResult.value, diff --git a/extensions/copilot/src/extension/intents/test/node/toolCallingLoopAutopilot.spec.ts b/extensions/copilot/src/extension/intents/test/node/toolCallingLoopAutopilot.spec.ts index 7777b5abef5..ff6a3b763f4 100644 --- a/extensions/copilot/src/extension/intents/test/node/toolCallingLoopAutopilot.spec.ts +++ b/extensions/copilot/src/extension/intents/test/node/toolCallingLoopAutopilot.spec.ts @@ -4,19 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { ChatRequest, LanguageModelToolInformation } from 'vscode'; +import type { ChatRequest, ChatResponseStream, LanguageModelToolInformation } from 'vscode'; import { IChatHookService } from '../../../../platform/chat/common/chatHookService'; import { ChatFetchResponseType, ChatResponse } from '../../../../platform/chat/common/commonTypes'; +import { SpyChatResponseStream } from '../../../../util/common/test/mockChatResponseStream'; import { CancellationToken, CancellationTokenSource } from '../../../../util/vs/base/common/cancellation'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { generateUuid } from '../../../../util/vs/base/common/uuid'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; import { Conversation, Turn } from '../../../prompt/common/conversation'; -import { IBuildPromptContext, IToolCallRound } from '../../../prompt/common/intents'; +import { IBuildPromptContext, IToolCall, IToolCallRound } from '../../../prompt/common/intents'; import { IBuildPromptResult, nullRenderPromptResult } from '../../../prompt/node/intents'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; +import { ToolName } from '../../../tools/common/toolNames'; import { IToolsService } from '../../../tools/common/toolsService'; import { TestToolsService } from '../../../tools/node/test/testToolsService'; +import { LanguageModelTextPart, LanguageModelToolResult2 } from '../../../../vscodeTypes'; import { IToolCallingLoopOptions, IToolCallSingleResult, ToolCallingLoop } from '../../node/toolCallingLoop'; import { MockChatHookService } from './mockChatHookService'; @@ -69,6 +72,34 @@ class AutopilotTestToolCallingLoop extends ToolCallingLoop<IToolCallingLoopOptio public testEnsureAutopilotTools(tools: LanguageModelToolInformation[]): LanguageModelToolInformation[] { return this.ensureAutopilotTools(tools); } + + public testReportVoiceProgress(stream: ChatResponseStream, phase: 'investigating' | 'planning' | 'editing' | 'validating' | 'recovering'): void { + this.reportVoiceProgress(stream, phase); + } + + public testReportVoiceProgressForRound(stream: ChatResponseStream, round: IToolCallRound): void { + this.reportVoiceProgressForRound(stream, round); + } + + public testEnsureVoiceProgressTool(tools: LanguageModelToolInformation[]): LanguageModelToolInformation[] { + return this.ensureVoiceProgressTool(tools); + } + + public testProcessVoiceProgressToolCalls(stream: ChatResponseStream, toolCalls: readonly IToolCall[]): void { + this.processVoiceProgressToolCalls(stream, toolCalls); + } + + public testGetToolCallResult(toolCallId: string): LanguageModelToolResult2 | undefined { + return this.createPromptContext([], undefined).toolCallResults?.[toolCallId]; + } + + public testGetPersistableToolCallingState(): { toolCallRounds: IToolCallRound[]; toolCallResults: Record<string, LanguageModelToolResult2> } { + return this.getPersistableToolCallingState(); + } + + public testHasProductiveToolCalls(round: IToolCallRound): boolean { + return this.hasProductiveToolCalls(round); + } } function createMockChatRequest(overrides: Partial<ChatRequest> = {}): ChatRequest { @@ -102,7 +133,7 @@ function createTestConversation(turnCount: number = 1): Conversation { return new Conversation(generateUuid(), turns); } -function createMockRound(toolCallNames: string[] = [], response: string = ''): IToolCallRound { +function createMockRound(toolCallNames: string[] = [], response: string = '', toolArguments = '{}'): IToolCallRound { return { id: generateUuid(), response, @@ -110,7 +141,7 @@ function createMockRound(toolCallNames: string[] = [], response: string = ''): I toolCalls: toolCallNames.map(name => ({ id: generateUuid(), name, - arguments: '{}', + arguments: toolArguments, })), }; } @@ -150,7 +181,7 @@ describe('ToolCallingLoop autopilot', () => { vi.restoreAllMocks(); }); - function createLoop(permissionLevel?: string, requestOverrides: Partial<ChatRequest> = {}): AutopilotTestToolCallingLoop { + function createLoop(permissionLevel?: string, requestOverrides: Partial<ChatRequest> = {}, enableVoiceProgress = true): AutopilotTestToolCallingLoop { const conversation = createTestConversation(1); const request = createMockChatRequest({ permissionLevel, @@ -162,12 +193,225 @@ describe('ToolCallingLoop autopilot', () => { conversation, toolCallLimit: 10, request, + enableVoiceProgress, } ); disposables.add(loop); return loop; } + describe('voice progress', () => { + it('classifies read and planning tools as semantic progress', () => { + const loop = createLoop(undefined, { id: 'voice-request', isVoiceModeInput: true }); + const stream = new SpyChatResponseStream(); + + loop.testReportVoiceProgressForRound(stream, createMockRound([ToolName.ReadFile])); + loop.testReportVoiceProgressForRound(stream, createMockRound([ToolName.CoreManageTodoList])); + + expect(stream.items).toEqual([ + expect.objectContaining({ id: 'investigating' }), + expect.objectContaining({ id: 'planning' }), + ]); + }); + + it('selects phrase variants deterministically per request', () => { + const firstLoop = createLoop(undefined, { id: 'stable-request', isVoiceModeInput: true }); + const secondLoop = createLoop(undefined, { id: 'stable-request', isVoiceModeInput: true }); + const firstStream = new SpyChatResponseStream(); + const secondStream = new SpyChatResponseStream(); + + firstLoop.testReportVoiceProgress(firstStream, 'editing'); + secondLoop.testReportVoiceProgress(secondStream, 'editing'); + + expect(firstStream.items).toEqual(secondStream.items); + }); + + it('does not emit for typed or subagent requests', () => { + const typedLoop = createLoop(); + const subagentLoop = createLoop(undefined, { isVoiceModeInput: true, subAgentInvocationId: 'subagent' }); + const disabledLoop = createLoop(undefined, { isVoiceModeInput: true }, false); + const stream = new SpyChatResponseStream(); + + typedLoop.testReportVoiceProgress(stream, 'editing'); + subagentLoop.testReportVoiceProgress(stream, 'editing'); + disabledLoop.testReportVoiceProgress(stream, 'editing'); + + expect(stream.items).toEqual([]); + }); + + it('emits each significant phase once in order', () => { + const loop = createLoop(undefined, { isVoiceModeInput: true }); + const stream = new SpyChatResponseStream(); + + loop.testReportVoiceProgressForRound(stream, createMockRound([ToolName.ReadFile])); + loop.testReportVoiceProgressForRound(stream, createMockRound([ToolName.CoreManageTodoList])); + loop.testReportVoiceProgressForRound(stream, createMockRound([ToolName.EditFile])); + loop.testReportVoiceProgressForRound(stream, createMockRound([ToolName.CoreRunInTerminal], '', '{"command":"npm test"}')); + loop.testReportVoiceProgressForRound(stream, createMockRound([ToolName.EditFile])); + + expect(stream.items).toEqual([ + expect.objectContaining({ id: 'investigating' }), + expect.objectContaining({ id: 'planning' }), + expect.objectContaining({ id: 'editing' }), + expect.objectContaining({ id: 'validating' }), + expect.objectContaining({ id: 'recovering' }), + ]); + }); + + it('offers the progress tool only to a top-level voice Agent loop', () => { + const voiceLoop = createLoop(undefined, { isVoiceModeInput: true }); + const typedLoop = createLoop(); + const subagentLoop = createLoop(undefined, { isVoiceModeInput: true, subAgentInvocationId: 'subagent' }); + const nonAgentLoop = createLoop(undefined, { isVoiceModeInput: true }, false); + + expect({ + voiceTools: voiceLoop.testEnsureVoiceProgressTool([]).map(tool => ({ + name: tool.name, + inputSchema: tool.inputSchema, + })), + typedTools: typedLoop.testEnsureVoiceProgressTool([]), + subagentTools: subagentLoop.testEnsureVoiceProgressTool([]), + nonAgentTools: nonAgentLoop.testEnsureVoiceProgressTool([]), + }).toEqual({ + voiceTools: [{ + name: 'report_voice_progress', + inputSchema: expect.objectContaining({ + required: ['stage', 'summary'], + additionalProperties: false, + }), + }], + typedTools: [], + subagentTools: [], + nonAgentTools: [], + }); + }); + + it('turns a valid model progress call into a hidden progress part and local result', () => { + const loop = createLoop(undefined, { isVoiceModeInput: true }); + const stream = new SpyChatResponseStream(); + + loop.testProcessVoiceProgressToolCalls(stream, [{ + id: 'progress-call', + name: 'report_voice_progress', + arguments: JSON.stringify({ + stage: 'investigating', + summary: 'I am tracing the request flow now.', + }), + }]); + + const result = loop.testGetToolCallResult('progress-call'); + expect({ + streamItems: stream.items, + resultText: result?.content[0] instanceof LanguageModelTextPart ? result.content[0].value : undefined, + }).toEqual({ + streamItems: [expect.objectContaining({ + id: 'investigating', + value: 'I am tracing the request flow now.', + })], + resultText: 'Voice progress reported.', + }); + }); + + it('rejects unsafe or out-of-bounds model progress summaries', () => { + const loop = createLoop(undefined, { isVoiceModeInput: true }); + const stream = new SpyChatResponseStream(); + const calls = [ + { id: 'bad-stage', stage: 'done', summary: 'Finished.' }, + { id: 'too-long', stage: 'editing', summary: 'x'.repeat(241) }, + { id: 'markdown', stage: 'editing', summary: 'Editing **src/secret.ts** now.' }, + ]; + + loop.testProcessVoiceProgressToolCalls(stream, calls.map(call => ({ + id: call.id, + name: 'report_voice_progress', + arguments: JSON.stringify({ stage: call.stage, summary: call.summary }), + }))); + + expect({ + streamItems: stream.items, + results: calls.map(call => { + const result = loop.testGetToolCallResult(call.id); + return result?.content[0] instanceof LanguageModelTextPart ? result.content[0].value : undefined; + }), + }).toEqual({ + streamItems: [], + results: [ + 'Voice progress was not reported because stage must be investigating, planning, editing, validating, or recovering.', + 'Voice progress was not reported because summary must be at most 240 characters.', + 'Voice progress was not reported because summary must use plain speech without markdown, paths, commands, identifiers, URLs, or secrets.', + ], + }); + }); + + it('model progress suppresses the same-stage deterministic fallback', () => { + const loop = createLoop(undefined, { isVoiceModeInput: true }); + const stream = new SpyChatResponseStream(); + + loop.testProcessVoiceProgressToolCalls(stream, [{ + id: 'editing-progress', + name: 'report_voice_progress', + arguments: '{"stage":"editing","summary":"I found the change point and I am updating it."}', + }]); + loop.testReportVoiceProgressForRound(stream, createMockRound([ToolName.EditFile])); + + expect(stream.items).toEqual([expect.objectContaining({ + id: 'editing', + value: 'I found the change point and I am updating it.', + })]); + }); + + it('removes the internal progress call and summary from persisted tool history', () => { + const loop = createLoop(undefined, { isVoiceModeInput: true }); + const stream = new SpyChatResponseStream(); + const progressCall: IToolCall = { + id: 'progress-call', + name: 'report_voice_progress', + arguments: '{"stage":"editing","summary":"I found the change point."}', + }; + loop.testProcessVoiceProgressToolCalls(stream, [progressCall]); + loop.addToolCallRound({ + id: 'mixed-round', + response: '', + toolInputRetry: 0, + toolCalls: [ + progressCall, + { id: 'edit-call', name: ToolName.EditFile, arguments: '{}' }, + ], + }); + + const state = loop.testGetPersistableToolCallingState(); + expect({ + state, + leaksProgress: JSON.stringify(state).includes('report_voice_progress') || JSON.stringify(state).includes('I found the change point.'), + }).toEqual({ + state: { + toolCallRounds: [{ + id: 'mixed-round', + response: '', + toolInputRetry: 0, + toolCalls: [{ id: 'edit-call', name: ToolName.EditFile, arguments: '{}' }], + }], + toolCallResults: {}, + }, + leaksProgress: false, + }); + }); + + it('does not treat voice progress as productive autopilot work', () => { + const loop = createLoop('autopilot', { isVoiceModeInput: true }); + + expect({ + progressOnly: loop.testHasProductiveToolCalls(createMockRound(['report_voice_progress'])), + taskCompleteOnly: loop.testHasProductiveToolCalls(createMockRound(['task_complete'])), + progressAndEdit: loop.testHasProductiveToolCalls(createMockRound(['report_voice_progress', ToolName.EditFile])), + }).toEqual({ + progressOnly: false, + taskCompleteOnly: false, + progressAndEdit: true, + }); + }); + }); + describe('shouldAutopilotContinue', () => { it('should return a nudge message when task_complete was not called', async () => { const loop = createLoop('autopilot'); diff --git a/extensions/copilot/src/extension/linkify/common/responseStreamWithLinkification.ts b/extensions/copilot/src/extension/linkify/common/responseStreamWithLinkification.ts index 5c8b96734c4..e951f144b55 100644 --- a/extensions/copilot/src/extension/linkify/common/responseStreamWithLinkification.ts +++ b/extensions/copilot/src/extension/linkify/common/responseStreamWithLinkification.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ChatQuestion, ChatResponseClearToPreviousToolInvocationReason, ChatResponseFileTree, ChatResponsePart, ChatResponseStream, ChatResultUsage, ChatToolInvocationStreamData, ChatVulnerability, ChatWorkspaceFileEdit, Command, Location, NotebookEdit, TextEdit, ThinkingDelta, Uri } from 'vscode'; +import type { ChatQuestion, ChatResponseClearToPreviousToolInvocationReason, ChatResponseFileTree, ChatResponsePart, ChatResponseStream, ChatResponseVoiceProgressStage, ChatResultUsage, ChatToolInvocationStreamData, ChatVulnerability, ChatWorkspaceFileEdit, Command, Location, NotebookEdit, TextEdit, ThinkingDelta, Uri } from 'vscode'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; import { FinalizableChatResponseStream } from '../../../util/common/chatResponseStreamImpl'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; @@ -89,6 +89,10 @@ export class ResponseStreamWithLinkification implements FinalizableChatResponseS return this; } + voiceProgress(id: ChatResponseVoiceProgressStage, value: string): ChatResponseStream { + this.enqueue(() => this._progress.voiceProgress(id, value), false); + return this; + } reference(value: Uri | Location): ChatResponseStream { this.enqueue(() => this._progress.reference(value), false); diff --git a/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts b/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts index b3a2933a397..0e1b673b291 100644 --- a/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts +++ b/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts @@ -40,6 +40,7 @@ import { assertType, Mutable } from '../../../util/vs/base/common/types'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ChatResponseMarkdownPart, ChatResponseProgressPart, ChatResponseTextEditPart, LanguageModelToolResult2 } from '../../../vscodeTypes'; import { CodeBlocksMetadata, CodeBlockTrackingChatResponseStream } from '../../codeBlocks/node/codeBlockProcessor'; +import { Intent } from '../../common/constants'; import { CopilotInteractiveEditorResponse, InteractionOutcomeComputer } from '../../inlineChat/node/promptCraftingTypes'; import { formatHookErrorMessage, HookAbortError, isHookAbortError, processHookResults } from '../../intents/node/hookResultProcessor'; import { EmptyPromptError, IToolCallingBuiltPromptEvent, IToolCallingLoopOptions, IToolCallingResponseEvent, IToolCallLoopResult, ToolCallingLoop, ToolCallingLoopFetchOptions, ToolCallLimitBehavior } from '../../intents/node/toolCallingLoop'; @@ -332,6 +333,7 @@ export class DefaultIntentRequestHandler { onHitToolCallLimit: this.handlerOptions.confirmOnMaxToolIterations !== false ? ToolCallLimitBehavior.Confirm : ToolCallLimitBehavior.Stop, request: this.request, + enableVoiceProgress: this.intent.id === Intent.Agent, documentContext: this.documentContext, streamParticipants: this.makeResponseStreamParticipants(intentInvocation), temperature: this.handlerOptions.temperature ?? this.options.temperature, diff --git a/extensions/copilot/src/extension/prompt/node/pseudoStartStopConversationCallback.ts b/extensions/copilot/src/extension/prompt/node/pseudoStartStopConversationCallback.ts index 48a57cbba6b..74955ad9da7 100644 --- a/extensions/copilot/src/extension/prompt/node/pseudoStartStopConversationCallback.ts +++ b/extensions/copilot/src/extension/prompt/node/pseudoStartStopConversationCallback.ts @@ -39,7 +39,7 @@ export class PseudoStopStartResponseProcessor implements IResponseProcessor { constructor( private readonly stopStartMappings: readonly StartStopMapping[], private readonly processNonReportedDelta: ((deltas: IResponseDelta[]) => string[]) | undefined, - private readonly options?: { subagentInvocationId?: string } + private readonly options?: { subagentInvocationId?: string; hiddenToolNames?: ReadonlySet<string> } ) { } async processResponse(_context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: ChatResponseStream, token: CancellationToken): Promise<void> { @@ -98,6 +98,9 @@ export class PseudoStopStartResponseProcessor implements IResponseProcessor { if (delta.beginToolCalls?.length) { for (const beginCall of delta.beginToolCalls) { + if (this.options?.hiddenToolNames?.has(beginCall.name)) { + continue; + } progress.beginToolInvocation(beginCall.id ?? '', getContributedToolName(beginCall.name), { subagentInvocationId: this.options?.subagentInvocationId }); } } @@ -105,7 +108,7 @@ export class PseudoStopStartResponseProcessor implements IResponseProcessor { if (delta.copilotToolCallStreamUpdates?.length) { const now = Date.now(); for (const update of delta.copilotToolCallStreamUpdates) { - if (!update.name) { + if (!update.name || this.options?.hiddenToolNames?.has(update.name)) { continue; } const toolId = update.id ?? ''; diff --git a/extensions/copilot/src/extension/prompt/node/test/defaultIntentRequestHandler.spec.ts b/extensions/copilot/src/extension/prompt/node/test/defaultIntentRequestHandler.spec.ts index 1d1131a3e39..54bc2da7331 100644 --- a/extensions/copilot/src/extension/prompt/node/test/defaultIntentRequestHandler.spec.ts +++ b/extensions/copilot/src/extension/prompt/node/test/defaultIntentRequestHandler.spec.ts @@ -6,7 +6,7 @@ import { Raw, RenderPromptResult } from '@vscode/prompt-tsx'; import { afterEach, beforeEach, expect, suite, test, vi } from 'vitest'; -import type { ChatLanguageModelToolReference, ChatPromptReference, ChatRequest, ExtendedChatResponsePart, LanguageModelChat } from 'vscode'; +import type { ChatLanguageModelToolReference, ChatPromptReference, ChatRequest, ExtendedChatResponsePart, LanguageModelChat, LanguageModelToolInformation } from 'vscode'; import { IChatMLFetcher } from '../../../../platform/chat/common/chatMLFetcher'; import { toTextPart } from '../../../../platform/chat/common/globalStringUtils'; import { StaticChatMLFetcher } from '../../../../platform/chat/test/common/staticChatMLFetcher'; @@ -25,10 +25,12 @@ import { isObject, isUndefinedOrNull } from '../../../../util/vs/base/common/typ import { generateUuid } from '../../../../util/vs/base/common/uuid'; import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; -import { ChatLocation, ChatResponseConfirmationPart, ChatResponseMarkdownPart, LanguageModelTextPart, LanguageModelToolResult, Uri } from '../../../../vscodeTypes'; +import { ChatLocation, ChatResponseConfirmationPart, ChatResponseMarkdownPart, ChatResponseVoiceProgressPart, LanguageModelTextPart, LanguageModelToolResult, Uri } from '../../../../vscodeTypes'; +import { Intent } from '../../../common/constants'; import { ToolCallingLoop } from '../../../intents/node/toolCallingLoop'; import { ToolResultMetadata } from '../../../prompts/node/panel/toolCalling'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; +import { ToolName } from '../../../tools/common/toolNames'; import { Conversation, Turn } from '../../common/conversation'; import { IBuildPromptContext } from '../../common/intents'; import { ToolCallRound } from '../../common/toolCallRound'; @@ -46,6 +48,7 @@ suite('defaultIntentRequestHandler', () => { let endpoint: IChatEndpoint; let turnIdCounter = 0; let builtPrompts: IBuildPromptContext[] = []; + let availableTools: LanguageModelToolInformation[] = []; const sessionId = 'some-session-id'; const getTurnId = () => `turn-id-${turnIdCounter}`; @@ -62,6 +65,7 @@ suite('defaultIntentRequestHandler', () => { accessor = services.createTestingAccessor(); endpoint = accessor.get(IInstantiationService).createInstance(MockEndpoint, undefined); builtPrompts = []; + availableTools = []; response = []; promptResult = nullRenderPromptResult(); turnIdCounter = 0; @@ -89,7 +93,7 @@ suite('defaultIntentRequestHandler', () => { } class TestIntent implements IIntent { - id = 'test'; + constructor(readonly id: string = 'test') { } description = 'test intent'; locations = [ChatLocation.Panel]; invoke(): Promise<IIntentInvocation> { @@ -118,6 +122,10 @@ suite('defaultIntentRequestHandler', () => { return promptResult; } + + async getAvailableTools(): Promise<LanguageModelToolInformation[]> { + return availableTools; + } } class TestChatRequest implements ChatRequest { @@ -127,6 +135,7 @@ suite('defaultIntentRequestHandler', () => { attempt = 1; enableCommandDetection = false; isParticipantDetected = false; + isVoiceModeInput?: boolean; location = ChatLocation.Panel; location2 = undefined; prompt = 'hello world!'; @@ -146,8 +155,9 @@ suite('defaultIntentRequestHandler', () => { const makeHandler = ({ request = new TestChatRequest(), - turns = [] - }: { request?: ChatRequest; turns?: Turn[] } = {}) => { + turns = [], + intent = new TestIntent(), + }: { request?: ChatRequest; turns?: Turn[]; intent?: IIntent } = {}) => { turns.push(new Turn( getTurnId(), { type: 'user', message: request.prompt }, @@ -157,7 +167,7 @@ suite('defaultIntentRequestHandler', () => { const instaService = accessor.get(IInstantiationService); return instaService.createInstance( DefaultIntentRequestHandler, - new TestIntent(), + intent, new Conversation(sessionId, turns), request, responseStream, @@ -305,6 +315,47 @@ suite('defaultIntentRequestHandler', () => { ]); }); + test('voice editAgent emits investigating fallback through the actual handler', async () => { + const request = new TestChatRequest(); + request.isVoiceModeInput = true; + availableTools = [{ + name: ToolName.ReadFile, + description: 'Read a file.', + inputSchema: { type: 'object' }, + tags: [], + source: undefined, + }]; + const requestSpy = vi.spyOn(endpoint, 'makeChatRequest2'); + const handler = makeHandler({ request, intent: new TestIntent(Intent.Agent) }); + chatResponse[0] = [{ + text: '', + copilotToolCalls: [{ + arguments: '{}', + name: ToolName.ReadFile, + id: 'read_call', + }], + }]; + chatResponse[1] = 'done'; + const toolResult = new LanguageModelToolResult([new LanguageModelTextPart('read result')]); + promptResult = { + ...nullRenderPromptResult(), + messages: [{ role: Raw.ChatRole.User, content: [toTextPart('hello world!')] }], + metadata: promptResultMetadata([new ToolResultMetadata('read_call__vscode-0', toolResult)]), + }; + + await handler.getResult(); + + expect({ + availableTools: requestSpy.mock.calls.at(0)?.[0]?.requestOptions?.tools?.map(tool => tool.function.name), + voiceProgress: response + .filter(part => part instanceof ChatResponseVoiceProgressPart) + .map(part => ({ id: part.id, value: part.value })), + }).toEqual({ + availableTools: [ToolName.ReadFile, 'report_voice_progress'], + voiceProgress: [expect.objectContaining({ id: 'investigating' })], + }); + }); + function fillWithToolCalls(insertN = 20) { promptResult = []; for (let i = 0; i < insertN; i++) { diff --git a/extensions/copilot/src/extension/prompts/node/agent/agentPrompt.tsx b/extensions/copilot/src/extension/prompts/node/agent/agentPrompt.tsx index 286a9fde4fe..ef7f92fedb3 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/agentPrompt.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/agentPrompt.tsx @@ -128,6 +128,7 @@ export class AgentPrompt extends PromptElement<AgentPromptProps> { </SystemMessage>} </>; const isAutopilot = this.props.promptContext.request?.permissionLevel === 'autopilot'; + const isVoiceModeInput = this.props.promptContext.request?.isVoiceModeInput && !this.props.promptContext.request.subAgentInvocationId; const sessionResource = this.props.promptContext.request?.sessionResource; const sessionId = sessionResource ? sessionResourceToId(sessionResource) : undefined; const debugTargetSessionIds = extractDebugTargetSessionIds([...this.props.promptContext.chatVariables].map(v => v.reference)); @@ -140,6 +141,11 @@ export class AgentPrompt extends PromptElement<AgentPromptProps> { When you have fully completed the task, call the task_complete tool to signal that you are done.<br /> IMPORTANT: Before calling task_complete, you MUST provide a brief text summary of what was accomplished in your message. The task is not complete until both the summary and the task_complete call are present. </SystemMessage>} + {isVoiceModeInput && <SystemMessage priority={80}> + Voice Mode is active, and you are GitHub Copilot speaking directly to the user. Keep the final response concise and easy to understand aloud. Do not expose internal reasoning.<br /> + You MUST call the report_voice_progress tool in the same response as your first real work tool calls, using investigating before reading or searching. Call it again at later meaningful stage changes, not for every operation, and in parallel with actual work when possible. Use planning when deciding the approach, editing while making changes, validating while running tests, builds, lint, or checks, and recovering after a concrete failure or change of approach.<br /> + Each summary must be a concise factual update in plain speech, at most 240 characters, with no markdown, paths, commands, identifiers, secrets, reasoning, or raw source and tool output. Do not repeat the acknowledgement or final response. Questions, confirmations, questionnaires, and the final response use their existing structured flows instead. + </SystemMessage>} {templateVariablesContext.length > 0 && <SystemMessage>{templateVariablesContext}</SystemMessage>} <UserMessage> {await this.getOrCreateGlobalAgentContext(this.props.endpoint)} diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx b/extensions/copilot/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx index 9f6a7e5ff8d..4256aa6b70e 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx @@ -167,6 +167,35 @@ testFamilies.forEach(family => { }, undefined)).toMatchFileSnapshot(getSnapshotFile('simple_case')); }); + if (family === 'default') { + test('voice progress guidance appears only for top-level voice requests', async () => { + const promptContext = { + chatVariables: new ChatVariablesCollection(), + history: [], + query: 'hello', + }; + const topLevelVoicePrompt = await agentPromptToString(accessor, { + ...promptContext, + request: { isVoiceModeInput: true } as IBuildPromptContext['request'], + }); + const subagentVoicePrompt = await agentPromptToString(accessor, { + ...promptContext, + request: { isVoiceModeInput: true, subAgentInvocationId: 'subagent' } as IBuildPromptContext['request'], + }); + const typedPrompt = await agentPromptToString(accessor, promptContext); + + expect({ + topLevelVoice: topLevelVoicePrompt.includes('You MUST call the report_voice_progress tool in the same response as your first real work tool calls'), + subagentVoice: subagentVoicePrompt.includes('You MUST call the report_voice_progress tool in the same response as your first real work tool calls'), + typed: typedPrompt.includes('You MUST call the report_voice_progress tool in the same response as your first real work tool calls'), + }).toEqual({ + topLevelVoice: true, + subagentVoice: false, + typed: false, + }); + }); + } + test('all tools', async () => { const toolsService = accessor.get(IToolsService); await expect(await agentPromptToString(accessor, { diff --git a/extensions/copilot/src/extension/test/node/pseudoStartStopConversationCallback.spec.ts b/extensions/copilot/src/extension/test/node/pseudoStartStopConversationCallback.spec.ts index 34b1b0cf072..14b8936f8e7 100644 --- a/extensions/copilot/src/extension/test/node/pseudoStartStopConversationCallback.spec.ts +++ b/extensions/copilot/src/extension/test/node/pseudoStartStopConversationCallback.spec.ts @@ -206,6 +206,46 @@ suite('Tool stream throttling', () => { assert.strictEqual(updateCalls[0].toolCallId, 'tool1'); }); + test('hidden local tools do not create or update tool cards', async () => { + const responseSource = new AsyncIterableSource<IResponsePart>(); + const beginCalls: { toolCallId: string; toolName: string }[] = []; + const hiddenStream = new ChatResponseStreamImpl( + () => { }, + () => { }, + undefined, + (toolCallId, toolName) => beginCalls.push({ toolCallId, toolName }), + (toolCallId, streamData) => updateCalls.push({ toolCallId, streamData }), + ); + const processor = new PseudoStopStartResponseProcessor([], undefined, { + hiddenToolNames: new Set(['report_voice_progress']), + }); + + responseSource.emitOne({ + delta: { + text: '', + beginToolCalls: [ + { id: 'hidden', name: 'report_voice_progress' }, + { id: 'visible', name: 'visible_tool' }, + ], + copilotToolCallStreamUpdates: [ + { id: 'hidden', name: 'report_voice_progress', arguments: '{"stage":"editing"}' }, + { id: 'visible', name: 'visible_tool', arguments: '{"value":1}' }, + ], + }, + }); + responseSource.resolve(); + + await processor.doProcessResponse(responseSource.asyncIterable, hiddenStream, CancellationToken.None); + + assert.deepStrictEqual({ + beginCalls, + updateCalls: updateCalls.map(call => call.toolCallId), + }, { + beginCalls: [{ toolCallId: 'visible', toolName: 'visible_tool' }], + updateCalls: ['visible'], + }); + }); + test('rapid updates within throttle window are throttled', async () => { const responseSource = new AsyncIterableSource<IResponsePart>(); const processor = new PseudoStopStartResponseProcessor([], undefined); diff --git a/extensions/copilot/src/util/common/chatResponseStreamImpl.ts b/extensions/copilot/src/util/common/chatResponseStreamImpl.ts index 07db8ae9aa7..80cbbe79951 100644 --- a/extensions/copilot/src/util/common/chatResponseStreamImpl.ts +++ b/extensions/copilot/src/util/common/chatResponseStreamImpl.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { ChatResponseReferencePartStatusKind } from '@vscode/prompt-tsx'; -import type { ChatQuestion, ChatResponseFileTree, ChatResponseStream, ChatResultUsage, ChatToolInvocationStreamData, ChatVulnerability, ChatWorkspaceFileEdit, Command, ExtendedChatResponsePart, Location, NotebookEdit, Progress, ThinkingDelta, Uri } from 'vscode'; -import { ChatHookType, ChatResponseAnchorPart, ChatResponseClearToPreviousToolInvocationReason, ChatResponseCodeblockUriPart, ChatResponseCodeCitationPart, ChatResponseCommandButtonPart, ChatResponseConfirmationPart, ChatResponseExternalEditPart, ChatResponseFileTreePart, ChatResponseHookPart, ChatResponseInfoPart, ChatResponseMarkdownPart, ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseNotebookEditPart, ChatResponseProgressPart, ChatResponseProgressPart2, ChatResponseReferencePart, ChatResponseReferencePart2, ChatResponseTextEditPart, ChatResponseThinkingProgressPart, ChatResponseWarningPart, ChatResponseWorkspaceEditPart, MarkdownString, TextEdit } from '../../vscodeTypes'; +import type { ChatQuestion, ChatResponseFileTree, ChatResponseStream, ChatResponseVoiceProgressStage, ChatResultUsage, ChatToolInvocationStreamData, ChatVulnerability, ChatWorkspaceFileEdit, Command, ExtendedChatResponsePart, Location, NotebookEdit, Progress, ThinkingDelta, Uri } from 'vscode'; +import { ChatHookType, ChatResponseAnchorPart, ChatResponseClearToPreviousToolInvocationReason, ChatResponseCodeblockUriPart, ChatResponseCodeCitationPart, ChatResponseCommandButtonPart, ChatResponseConfirmationPart, ChatResponseExternalEditPart, ChatResponseFileTreePart, ChatResponseHookPart, ChatResponseInfoPart, ChatResponseMarkdownPart, ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseNotebookEditPart, ChatResponseProgressPart, ChatResponseProgressPart2, ChatResponseReferencePart, ChatResponseReferencePart2, ChatResponseTextEditPart, ChatResponseThinkingProgressPart, ChatResponseVoiceProgressPart, ChatResponseWarningPart, ChatResponseWorkspaceEditPart, MarkdownString, TextEdit } from '../../vscodeTypes'; import type { ThemeIcon } from '../vs/base/common/themables'; @@ -135,6 +135,10 @@ export class ChatResponseStreamImpl implements FinalizableChatResponseStream { this._push(new ChatResponseHookPart(hookType, stopReason, systemMessage)); } + voiceProgress(id: ChatResponseVoiceProgressStage, value: string): void { + this._push(new ChatResponseVoiceProgressPart(id, value)); + } + button(command: Command): void { this._push(new ChatResponseCommandButtonPart(command)); } diff --git a/extensions/copilot/src/util/common/test/shims/chatTypes.ts b/extensions/copilot/src/util/common/test/shims/chatTypes.ts index b585aae679c..066fad29fa7 100644 --- a/extensions/copilot/src/util/common/test/shims/chatTypes.ts +++ b/extensions/copilot/src/util/common/test/shims/chatTypes.ts @@ -80,6 +80,13 @@ export class ChatResponseHookPart { } } +export class ChatResponseVoiceProgressPart { + constructor( + readonly id: vscode.ChatResponseVoiceProgressStage, + readonly value: string, + ) { } +} + export class ChatResponseExternalEditPart { applied: Thenable<string>; didGetApplied!: (value: string) => void; diff --git a/extensions/copilot/src/util/common/test/shims/vscodeTypesShim.ts b/extensions/copilot/src/util/common/test/shims/vscodeTypesShim.ts index a2aa56f6953..14966f1bf5c 100644 --- a/extensions/copilot/src/util/common/test/shims/vscodeTypesShim.ts +++ b/extensions/copilot/src/util/common/test/shims/vscodeTypesShim.ts @@ -18,7 +18,7 @@ import { SnippetString } from '../../../vs/workbench/api/common/extHostTypes/sni import { SnippetTextEdit } from '../../../vs/workbench/api/common/extHostTypes/snippetTextEdit'; import { SymbolInformation, SymbolKind } from '../../../vs/workbench/api/common/extHostTypes/symbolInformation'; import { EndOfLine, TextEdit } from '../../../vs/workbench/api/common/extHostTypes/textEdit'; -import { AISearchKeyword, ChatErrorLevel, ChatInputNotificationSeverity, ChatQuestion, ChatQuestionType, ChatReferenceBinaryData, ChatReferenceDiagnostic, ChatRequestEditedFileEventKind, ChatRequestEditorData, ChatRequestNotebookData, ChatRequestTurn, ChatRequestTurn2, ChatResponseAnchorPart, ChatResponseAutoModeResolutionPart, ChatResponseClearToPreviousToolInvocationReason, ChatResponseCodeblockUriPart, ChatResponseCodeCitationPart, ChatResponseCommandButtonPart, ChatResponseConfirmationPart, ChatResponseExtensionsPart, ChatResponseExternalEditPart, ChatResponseFileTreePart, ChatResponseHookPart, ChatResponseInfoPart, ChatResponseMarkdownPart, ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseMovePart, ChatResponseNotebookEditPart, ChatResponseProgressPart, ChatResponseProgressPart2, ChatResponsePullRequestPart, ChatResponseQuestionCarouselPart, ChatResponseReferencePart, ChatResponseReferencePart2, ChatResponseTextEditPart, ChatResponseThinkingProgressPart, ChatResponseTurn, ChatResponseTurn2, ChatResponseWarningPart, ChatResponseWorkspaceEditPart, ChatSessionStatus, ChatSubagentToolInvocationData, ChatToolInvocationPart, ExcludeSettingOptions, LanguageModelChatMessage, LanguageModelChatMessageRole, LanguageModelChatToolMode, LanguageModelDataPart, LanguageModelDataPart2, LanguageModelError, LanguageModelPartAudience, LanguageModelPromptTsxPart, LanguageModelTextPart, LanguageModelTextPart2, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolExtensionSource, LanguageModelToolMCPSource, LanguageModelToolResult, LanguageModelToolResult2, LanguageModelToolResultPart, LanguageModelToolResultPart2, McpHttpServerDefinition, McpStdioServerDefinition, McpToolInvocationContentData, TextSearchMatch2 } from './chatTypes'; +import { AISearchKeyword, ChatErrorLevel, ChatInputNotificationSeverity, ChatQuestion, ChatQuestionType, ChatReferenceBinaryData, ChatReferenceDiagnostic, ChatRequestEditedFileEventKind, ChatRequestEditorData, ChatRequestNotebookData, ChatRequestTurn, ChatRequestTurn2, ChatResponseAnchorPart, ChatResponseAutoModeResolutionPart, ChatResponseClearToPreviousToolInvocationReason, ChatResponseCodeblockUriPart, ChatResponseCodeCitationPart, ChatResponseCommandButtonPart, ChatResponseConfirmationPart, ChatResponseExtensionsPart, ChatResponseExternalEditPart, ChatResponseFileTreePart, ChatResponseHookPart, ChatResponseInfoPart, ChatResponseMarkdownPart, ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseMovePart, ChatResponseNotebookEditPart, ChatResponseProgressPart, ChatResponseProgressPart2, ChatResponsePullRequestPart, ChatResponseQuestionCarouselPart, ChatResponseReferencePart, ChatResponseReferencePart2, ChatResponseTextEditPart, ChatResponseThinkingProgressPart, ChatResponseTurn, ChatResponseTurn2, ChatResponseVoiceProgressPart, ChatResponseWarningPart, ChatResponseWorkspaceEditPart, ChatSessionStatus, ChatSubagentToolInvocationData, ChatToolInvocationPart, ExcludeSettingOptions, LanguageModelChatMessage, LanguageModelChatMessageRole, LanguageModelChatToolMode, LanguageModelDataPart, LanguageModelDataPart2, LanguageModelError, LanguageModelPartAudience, LanguageModelPromptTsxPart, LanguageModelTextPart, LanguageModelTextPart2, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolExtensionSource, LanguageModelToolMCPSource, LanguageModelToolResult, LanguageModelToolResult2, LanguageModelToolResultPart, LanguageModelToolResultPart2, McpHttpServerDefinition, McpStdioServerDefinition, McpToolInvocationContentData, TextSearchMatch2 } from './chatTypes'; import { TextDocumentChangeReason, TextEditorSelectionChangeKind, WorkspaceEdit } from './editing'; import { ChatLocation, ChatVariableLevel, DiagnosticSeverity, ExtensionMode, FileType, TextEditorCursorStyle, TextEditorLineNumbersStyle, TextEditorRevealType } from './enums'; import { t } from './l10n'; @@ -60,6 +60,7 @@ const shim: typeof vscodeTypes = { ChatResponseWarningPart, ChatResponseInfoPart, ChatResponseHookPart, + ChatResponseVoiceProgressPart, ChatResponseReferencePart, ChatResponseReferencePart2, ChatResponseCodeCitationPart, diff --git a/extensions/copilot/src/vscodeTypes.ts b/extensions/copilot/src/vscodeTypes.ts index a931ec06d7c..630b4a1e33f 100644 --- a/extensions/copilot/src/vscodeTypes.ts +++ b/extensions/copilot/src/vscodeTypes.ts @@ -27,6 +27,7 @@ export import ChatResponseClearToPreviousToolInvocationReason = vscode.ChatRespo export import ChatResponseMarkdownPart = vscode.ChatResponseMarkdownPart; export import ChatResponseThinkingProgressPart = vscode.ChatResponseThinkingProgressPart; export import ChatResponseHookPart = vscode.ChatResponseHookPart; +export import ChatResponseVoiceProgressPart = vscode.ChatResponseVoiceProgressPart; export import ChatHookType = vscode.ChatHookType; export import ChatResponseFileTreePart = vscode.ChatResponseFileTreePart; export import ChatResponseAnchorPart = vscode.ChatResponseAnchorPart; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index bf42962ec0b..93b1b4f31a2 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -2218,6 +2218,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseProgressPart2: extHostTypes.ChatResponseProgressPart2, ChatResponseThinkingProgressPart: extHostTypes.ChatResponseThinkingProgressPart, ChatResponseHookPart: extHostTypes.ChatResponseHookPart, + ChatResponseVoiceProgressPart: extHostTypes.ChatResponseVoiceProgressPart, ChatResponseAutoModeResolutionPart: extHostTypes.ChatResponseAutoModeResolutionPart, ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, ChatResponseReferencePart2: extHostTypes.ChatResponseReferencePart, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index eba48601a1d..6b03bf70318 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -216,6 +216,13 @@ export class ChatAgentResponseStream { _report(dto); return this; }, + voiceProgress(id: vscode.ChatResponseVoiceProgressStage, value: string) { + throwIfDone(this.voiceProgress); + checkProposedApiEnabled(that._extension, 'chatParticipantPrivate'); + const part = new extHostTypes.ChatResponseVoiceProgressPart(id, value); + _report(typeConvert.ChatResponseVoiceProgressPart.from(part)); + return this; + }, warning(value) { throwIfDone(this.progress); checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 48f49b4d52e..0ed06f435e5 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -43,7 +43,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/participants/chatAgents.js'; import { IChatRequestModeInstructions } from '../../contrib/chat/common/model/chatModel.js'; -import { IChatAgentMarkdownContentWithVulnerability, IChatAutoModeResolutionPart, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatExternalToolInvocationUpdate, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffDataSerialized, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTerminalToolInvocationData, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatWarningMessage, IChatInfoMessage, IChatWorkspaceEdit } from '../../contrib/chat/common/chatService/chatService.js'; +import { IChatAgentMarkdownContentWithVulnerability, IChatAutoModeResolutionPart, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatExternalToolInvocationUpdate, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffDataSerialized, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTerminalToolInvocationData, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatVoiceProgressPart, IChatWarningMessage, IChatInfoMessage, IChatWorkspaceEdit } from '../../contrib/chat/common/chatService/chatService.js'; import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js'; import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { coerceImageBuffer } from '../../contrib/chat/common/chatImageExtraction.js'; @@ -2855,6 +2855,16 @@ export namespace ChatResponseHookPart { } } +export namespace ChatResponseVoiceProgressPart { + export function from(part: vscode.ChatResponseVoiceProgressPart): Dto<IChatVoiceProgressPart> { + return { + kind: 'voiceProgress', + id: part.id, + value: part.value, + }; + } +} + export namespace ChatResponseAutoModeResolutionPart { const validLabels = new Set<IChatAutoModeResolutionPart['predictedLabel']>(['needs_reasoning', 'no_reasoning', 'fallback']); @@ -3386,6 +3396,8 @@ export namespace ChatResponsePart { return ChatResponseThinkingProgressPart.from(part); } else if (part instanceof types.ChatResponseHookPart) { return ChatResponseHookPart.from(part); + } else if (part instanceof types.ChatResponseVoiceProgressPart) { + return ChatResponseVoiceProgressPart.from(part); } else if (part instanceof types.ChatResponseFileTreePart) { return ChatResponseFilesPart.from(part); } else if (part instanceof types.ChatResponseMultiDiffPart) { @@ -3479,6 +3491,7 @@ export namespace ChatAgentRequest { attempt: request.attempt ?? 0, enableCommandDetection: request.enableCommandDetection ?? true, isParticipantDetected: request.isParticipantDetected ?? false, + isVoiceModeInput: request.isVoiceModeInput, sessionId, sessionResource: request.sessionResource, references: variableReferences @@ -3514,6 +3527,8 @@ export namespace ChatAgentRequest { // eslint-disable-next-line local/code-no-any-casts delete (requestWithAllProps as any).isParticipantDetected; // eslint-disable-next-line local/code-no-any-casts + delete (requestWithAllProps as any).isVoiceModeInput; + // eslint-disable-next-line local/code-no-any-casts delete (requestWithAllProps as any).location; // eslint-disable-next-line local/code-no-any-casts delete (requestWithAllProps as any).location2; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 38cd199b5fc..1bb1c5a5f10 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3271,6 +3271,17 @@ export class ChatResponseHookPart { } } +export type ChatResponseVoiceProgressStage = 'investigating' | 'planning' | 'editing' | 'validating' | 'recovering'; + +export class ChatResponseVoiceProgressPart { + readonly id: ChatResponseVoiceProgressStage; + readonly value: string; + constructor(id: ChatResponseVoiceProgressStage, value: string) { + this.id = id; + this.value = value; + } +} + export class ChatResponseAutoModeResolutionPart { resolvedModel: string; resolvedModelName: string; diff --git a/src/vs/workbench/api/test/common/extHostTypeConverters.test.ts b/src/vs/workbench/api/test/common/extHostTypeConverters.test.ts index 4c9c4f70cbc..51ed4ede6a3 100644 --- a/src/vs/workbench/api/test/common/extHostTypeConverters.test.ts +++ b/src/vs/workbench/api/test/common/extHostTypeConverters.test.ts @@ -8,8 +8,8 @@ import { URI, UriComponents } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { NullLogService } from '../../../../platform/log/common/log.js'; import { IconPathDto } from '../../common/extHost.protocol.js'; -import { ChatPromptReference, ChatRequestModeInstructions, ChatToolInvocationPart, IconPath } from '../../common/extHostTypeConverters.js'; -import { ChatReferenceBinaryData, ChatSubagentToolInvocationData, ChatToolInvocationPart as ExtHostChatToolInvocationPart, ThemeColor, ThemeIcon } from '../../common/extHostTypes.js'; +import { ChatPromptReference, ChatRequestModeInstructions, ChatResponseVoiceProgressPart, ChatToolInvocationPart, IconPath } from '../../common/extHostTypeConverters.js'; +import { ChatReferenceBinaryData, ChatResponseVoiceProgressPart as ExtHostChatResponseVoiceProgressPart, ChatSubagentToolInvocationData, ChatToolInvocationPart as ExtHostChatToolInvocationPart, ThemeColor, ThemeIcon } from '../../common/extHostTypes.js'; import { IElementVariableEntry } from '../../../contrib/chat/common/attachments/chatVariableEntries.js'; import { IChatRequestModeInstructions } from '../../../contrib/chat/common/model/chatModel.js'; import { Dto } from '../../../services/extensions/common/proxyIdentifier.js'; @@ -17,6 +17,13 @@ import { Dto } from '../../../services/extensions/common/proxyIdentifier.js'; suite('extHostTypeConverters', function () { ensureNoDisposablesAreLeakedInTestSuite(); + test('converts voice progress to hidden chat progress', () => { + assert.deepStrictEqual( + ChatResponseVoiceProgressPart.from(new ExtHostChatResponseVoiceProgressPart('investigating', 'Investigating the relevant code.')), + { kind: 'voiceProgress', id: 'investigating', value: 'Investigating the relevant code.' } + ); + }); + suite('IconPath', function () { suite('from', function () { test('undefined', function () { diff --git a/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts b/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts index 79759b047b7..ab03849b92e 100644 --- a/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts +++ b/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts @@ -8,6 +8,7 @@ import '../../chat/browser/voiceClient/micCaptureService.js'; import '../../chat/browser/voiceClient/ttsPlaybackService.js'; import '../../chat/browser/voiceClient/voiceClientService.js'; import { IVoiceSessionController } from '../../chat/browser/voiceClient/voiceSessionController.js'; +import { VOICE_AGENT_PROGRESS_SETTING } from '../../chat/common/voiceClient/voiceClientService.js'; import '../../chat/browser/voiceClient/voiceToolDispatchService.js'; import '../../chat/common/voicePlaybackService.js'; @@ -615,6 +616,13 @@ configurationRegistry.registerConfiguration({ default: true, scope: ConfigurationScope.APPLICATION, }, + [VOICE_AGENT_PROGRESS_SETTING]: { + type: 'boolean', + markdownDescription: nls.localize('agents.voice.agentProgress', "Allow Agent mode to speak brief semantic progress updates while it investigates, plans, edits, validates, or recovers from a problem."), + default: false, + tags: ['experimental'], + scope: ConfigurationScope.APPLICATION, + }, 'agents.voice.voice': { type: 'string', enum: ['victoria_neutral', 'kevin_neutral', 'maya_neutral', 'daniel_neutral'], diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index c8dbb6f7e65..8bdd38909e1 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -94,6 +94,9 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui content.push(localize('chat.voiceInputMode.segmented', 'When the segmented voice input control is enabled, the input toolbar offers Dictation, Voice Mode, and, in manual Voice Mode, a Start or Stop Listening button. Stopping listening sends the completed turn. Each button can be focused and activated with Enter or Space.')); content.push(localize('chat.voiceInputMode.holdToTalk', 'In manual Voice Mode, the Start or Stop Listening button toggles listening when tapped, or you can press and hold it to talk and release to send. You can also hold the Voice Mode: Hold to Talk keybinding{0} to talk and release to send; this interrupts the assistant to barge in.', '<keybinding:workbench.action.chat.voiceInputMode.holdToTalk>')); content.push(localize('chat.voiceMode.introduction', 'The first time Voice Mode starts, an introduction appears above the input box. Tab to reach it, then use the arrow keys to move between the available voices; Enter or Space plays a voice and keeps it for future conversations. Its description also contains two links: Settings, which opens the Voice Mode settings, and How It Responds, which opens a file for customizing what the agent says back. Voice Mode stays connected but does not listen while the introduction is open. Press Escape, or activate the Close button, to dismiss it and return to the input box.')); + if (type === 'agentView') { + content.push(localize('chat.voiceInputMode.agentProgress', 'When the experimental agents.voice.agentProgress setting is enabled, Voice Mode Agent requests may speak brief progress updates while investigating, planning, editing, validating, or recovering from a problem.')); + } content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}. Thinking content is included in order by default.', '<keybinding:editor.action.accessibleView>')); content.push(localize('chat.inspectResponseThinkingToggle', 'To include or exclude thinking content in the accessible view, run the Toggle Thinking Content in Accessible View command from the Command Palette.')); content.push(localize('chat.completedResponseDisclosure', 'When completed response collapsing is enabled, the final response remains visible while earlier work is collapsed. Use Tab to focus the work disclosure and press Enter or Space to show or hide that work.')); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index e1389d07498..043ff60687c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -339,6 +339,7 @@ export type IChatWidgetViewContext = IChatViewViewContext | IChatResourceViewCon export interface IChatAcceptInputOptions { noCommandDetection?: boolean; isVoiceInput?: boolean; + isVoiceModeInput?: boolean; enableImplicitContext?: boolean; // defaults to true // Whether to store the input to history. This defaults to 'true' if the input // box's current content is being accepted, or 'false' if a specific input diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts index 69086befb30..424ffc59f43 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts @@ -28,7 +28,10 @@ import { IVoiceNarrationAck, IVoiceNarrationSignal, IVoiceDispatchResult, + IVoiceCheckpointNarrationMetadata, + VoiceConfirmationType, VoiceNarrationKind, + isVoiceCheckpointId, } from '../../common/voiceClient/voiceClientService.js'; import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; @@ -92,6 +95,7 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic // state-change event needs to fire before the timer expires. private _pendingContext: IVoiceSessionContext | undefined; private _lastSentById = new Map<string, Record<string, unknown>>(); // session id → last-sent field values + private readonly _invalidatedSessionIds = new Set<string>(); // --- Events --- private readonly _onTranscription = this._register(new Emitter<IVoiceTranscription>()); @@ -351,8 +355,14 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic turn_id?: unknown; revision?: unknown; narration_id?: string; + request_id?: string; + checkpoint_id?: string; + sequence?: number; + narration_kind?: string; + playback_id?: string; interrupted_turn_id?: string; disposition?: string; + retryable?: boolean; }; try { msg = JSON.parse(evt.data as string); @@ -385,14 +395,20 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic interruptedTurnId: msg.interrupted_turn_id ?? '', }); break; - case 'narration_ack': + case 'narration_ack': { + const disposition = msg.disposition === 'busy' + || msg.disposition === 'invalid' + || msg.disposition === 'suppressed' + ? msg.disposition + : 'accepted'; this._onNarrationAck.fire({ narrationId: msg.narration_id ?? '', codingSessionId: msg.coding_session_id ?? '', - disposition: (msg.disposition as 'accepted' | 'busy' | 'invalid') ?? 'accepted', + disposition, reason: msg.reason, }); break; + } case 'narration_unblocked': this._onNarrationUnblocked.fire({ narrationId: msg.narration_id ?? '', @@ -403,6 +419,8 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic this._onNarrationInterrupted.fire({ narrationId: msg.narration_id ?? '', codingSessionId: msg.coding_session_id ?? '', + ...(typeof msg.retryable === 'boolean' ? { retryable: msg.retryable } : {}), + ...(msg.reason ? { reason: msg.reason } : {}), }); break; case 'transcription': { @@ -421,11 +439,19 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic }); break; } - case 'audio_response': + case 'audio_response': { // Old pre-streaming server (pre PR #44076) doesn't send // `is_first_chunk` at all. Treat missing field as TRUE so // suppression-clearing in _enqueueAudio still works; new // streaming server always emits true/false explicitly. + const requestId = asOptionalString(msg.request_id); + const checkpointId = isVoiceCheckpointId(msg.checkpoint_id) ? msg.checkpoint_id : undefined; + const sequence = typeof msg.sequence === 'number' && Number.isSafeInteger(msg.sequence) && msg.sequence > 0 ? msg.sequence : undefined; + const narrationKind = msg.narration_kind === 'response' || msg.narration_kind === 'confirmation' || msg.narration_kind === 'checkpoint' ? msg.narration_kind as VoiceNarrationKind : undefined; + const playbackId = asOptionalString(msg.playback_id); + if (narrationKind === 'checkpoint') { + this._logService.info(`[voice] checkpoint audio request=${requestId ?? 'none'} stage=${checkpointId ?? 'none'} sequence=${sequence ?? 'none'} first=${msg.is_first_chunk === undefined ? true : Boolean(msg.is_first_chunk)} final=${Boolean(msg.is_final)}`); + } this._onAudioResponse.fire({ audio: msg.audio ?? '', isFirstChunk: msg.is_first_chunk === undefined ? true : Boolean(msg.is_first_chunk), @@ -434,8 +460,14 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic transcript: msg.transcript, turnId: asOptionalString(msg.turn_id), responseId: msg.narration_id ?? asOptionalString(msg.turn_id), + ...(requestId ? { requestId } : {}), + ...(checkpointId ? { checkpointId } : {}), + ...(sequence !== undefined ? { sequence } : {}), + ...(narrationKind ? { narrationKind } : {}), + ...(playbackId ? { playbackId } : {}), }); break; + } case 'tool_call': this._onToolCall.fire({ callId: msg.call_id ?? '', @@ -533,6 +565,7 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic this._lastSessionId = undefined; this._isResuming = false; this._lastSentById.clear(); + this._invalidatedSessionIds.clear(); this._setConnected(false); } @@ -628,7 +661,7 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic } invalidateSessionCache(sessionId: string): void { - this._lastSentById.delete(sessionId); + this._invalidatedSessionIds.add(sessionId); } private _sendDelta(context: IVoiceSessionContext): void { @@ -646,20 +679,35 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic } else { const patch: Record<string, unknown> = { id: session.id }; let hasChanges = false; - // Fields that changed or were added - for (const key of Object.keys(current)) { - if (key === 'id') { continue; } - if (stableStringify(current[key]) !== stableStringify(prev[key])) { - patch[key] = current[key]; - hasChanges = true; + if (this._invalidatedSessionIds.has(session.id)) { + for (const key of Object.keys(current)) { + if (key !== 'id') { + patch[key] = current[key] ?? null; + hasChanges = true; + } } - } - // Fields that were removed (present in prev, absent in current) → null per RFC 7396 - for (const key of Object.keys(prev)) { - if (key === 'id') { continue; } - if (!Object.prototype.hasOwnProperty.call(current, key) || current[key] === undefined) { - patch[key] = null; - hasChanges = true; + for (const key of Object.keys(prev)) { + if (key !== 'id' && (!Object.prototype.hasOwnProperty.call(current, key) || current[key] === undefined)) { + patch[key] = null; + hasChanges = true; + } + } + } else { + // Fields that changed or were added + for (const key of Object.keys(current)) { + if (key === 'id') { continue; } + if (stableStringify(current[key]) !== stableStringify(prev[key])) { + patch[key] = current[key]; + hasChanges = true; + } + } + // Fields that were removed (present in prev, absent in current) → null per RFC 7396 + for (const key of Object.keys(prev)) { + if (key === 'id') { continue; } + if (!Object.prototype.hasOwnProperty.call(current, key) || current[key] === undefined) { + patch[key] = null; + hasChanges = true; + } } } // ``agent_state_detail`` (the confirmation prompt text) and @@ -698,8 +746,12 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic if (v !== undefined) { obj[k] = v; } } this._lastSentById.set(session.id, obj); + this._invalidatedSessionIds.delete(session.id); + } + for (const id of removes) { + this._lastSentById.delete(id); + this._invalidatedSessionIds.delete(id); } - for (const id of removes) { this._lastSentById.delete(id); } this._ws!.send(JSON.stringify({ type: 'session_context', @@ -712,6 +764,7 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic private _seedTracking(context: IVoiceSessionContext): void { this._lastSentById.clear(); + this._invalidatedSessionIds.clear(); for (const session of context.sessions) { const obj: Record<string, unknown> = {}; for (const [k, v] of Object.entries(session as unknown as Record<string, unknown>)) { @@ -727,7 +780,18 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic } } - requestNarration(codingSessionId: string, kind: VoiceNarrationKind, text: string, narrationId?: string, pending?: { pendingId: string }): string | undefined { + sendNarrationPlaybackComplete(codingSessionId: string, narrationId: string, playbackId: string): void { + if (this._ws?.readyState === WebSocket.OPEN && this._sessionStartedOnSocket) { + this._ws.send(JSON.stringify({ + type: 'narration_playback_complete', + coding_session_id: codingSessionId, + narration_id: narrationId, + playback_id: playbackId, + })); + } + } + + requestNarration(codingSessionId: string, kind: VoiceNarrationKind, text: string, narrationId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata, confirmationType?: VoiceConfirmationType, pending?: { pendingId: string }): string | undefined { // Gate on session_context having been sent: the WS preserves send order, // so the backend processes start_session/resume_session before any // request_narration. Pre-session this returns undefined, so _narrate queues @@ -741,9 +805,18 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic kind, text, narration_id: id, + ...(checkpoint ? { + request_id: checkpoint.requestId, + checkpoint_id: checkpoint.checkpointId, + sequence: checkpoint.sequence, + } : {}), + ...(kind === 'confirmation' && confirmationType ? { confirmation_type: confirmationType } : {}), ...(pending ? { pending_id: pending.pendingId } : {}), })); this._logService.trace(`[voice] request_narration kind=${kind} id=${codingSessionId.slice(-32)} narration_id=${id.slice(0, 8)}${narrationId ? ' (retry)' : ''}`); + if (checkpoint) { + this._logService.info(`[voice] checkpoint sent request=${checkpoint.requestId} stage=${checkpoint.checkpointId} sequence=${checkpoint.sequence}`); + } return id; } return undefined; diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts index 39168350f76..13453ced663 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts @@ -3,23 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { IObservable, observableValue, autorun, transaction, observableSignalFromEvent } from '../../../../../base/common/observable.js'; import { addDisposableListener, disposableWindowInterval } from '../../../../../base/browser/dom.js'; +import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { alert as ariaAlert } from '../../../../../base/browser/ui/aria/aria.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { localize } from '../../../../../nls.js'; import { disposableTimeout } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { URI } from '../../../../../base/common/uri.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; +import { isObject } from '../../../../../base/common/types.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IAuthenticationService } from '../../../../services/authentication/common/authentication.js'; import { IVoiceTranscriptEntryMetadata, IVoiceTranscriptStore, IVoiceTranscriptTurn, VoiceTranscriptKind } from '../../../agentsVoice/common/voiceTranscriptStore.js'; -import { IVoiceAudioResponse, IVoiceBargeIn, IVoiceClientService, IVoicePriorTimelineEntry, IVoiceSessionContext, IVoiceFeedbackPayload, IVoiceFeedbackTranscriptTurn, IVoiceTranscription, IVoiceTurnAutoEnded, IVoiceNarrationAck, IVoiceNarrationSignal, VoiceNarrationKind, IVoiceSessionPending, IVoicePendingQuestion, derivePendingId } from '../../common/voiceClient/voiceClientService.js'; +import { IVoiceAudioResponse, IVoiceBargeIn, IVoiceCheckpointNarrationMetadata, IVoiceClientService, IVoicePriorTimelineEntry, IVoiceSessionContext, IVoiceFeedbackPayload, IVoiceFeedbackTranscriptTurn, IVoiceTranscription, IVoiceTurnAutoEnded, IVoiceNarrationAck, IVoiceNarrationSignal, isVoiceCheckpointId, VoiceCheckpointId, VoiceConfirmationType, VoiceNarrationKind, IVoiceSessionPending, IVoicePendingQuestion, derivePendingId, VOICE_AGENT_PROGRESS_SETTING } from '../../common/voiceClient/voiceClientService.js'; +import { getVoiceConfirmationType, isPendingVoiceQuestionnaireInvocation, isVoiceQuestionnaireInvocation } from '../../common/voiceClient/voiceConfirmation.js'; import { IMicCaptureService, IPttDiagnostic } from './micCaptureService.js'; import { ITtsPlaybackService } from './ttsPlaybackService.js'; import { IVoiceToolDispatchService, VoiceToolDispatchService } from './voiceToolDispatchService.js'; @@ -27,12 +31,11 @@ import { IVoicePlaybackService } from '../../common/voicePlaybackService.js'; import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; import { AgentSessionStatus } from '../agentSessions/agentSessionsModel.js'; import { toAgentHostBackendSessionUri } from '../agentSessions/agentHost/agentHostSessionUri.js'; -import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { IChatService, IChatToolInvocation, ToolConfirmKind, IChatModelReference, IChatQuestionCarousel } from '../../common/chatService/chatService.js'; +import { ChatSendResult, IChatConfirmation, IChatElicitationRequest, IChatPlanReview, IChatQuestionCarousel, IChatService, IChatToolInvocation, ToolConfirmKind, IChatModelReference } from '../../common/chatService/chatService.js'; import { getDisplayedQuestionText, getOptionsWithDefaultsFirst } from '../../common/chatService/chatQuestionCarouselHelpers.js'; import { formatQuestionPrompt } from '../../common/voiceClient/voicePendingNarration.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; -import { IChatModel, IChatProgressResponseContent } from '../../common/model/chatModel.js'; +import { IChatModel, IChatProgressResponseContent, IChatResponseModel } from '../../common/model/chatModel.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; @@ -99,10 +102,54 @@ interface IPendingSolicitedNarration { readonly text: string; /** The form this narration speaks, when it has one. Identifies the occurrence for dedup; see `_narratableIdentity`. */ readonly pending?: { pendingId: string }; + readonly checkpoint?: IVoiceCheckpointNarrationMetadata; + readonly confirmationType?: VoiceConfirmationType; readonly audioStartTimer: ReturnType<typeof setTimeout>; hasReceivedAudio: boolean; } +interface IVoiceNarratable { + readonly kind: Exclude<VoiceNarrationKind, 'checkpoint'>; + readonly text: string; + readonly pending?: { pendingId: string }; + readonly confirmationType?: VoiceConfirmationType; +} + +interface IPlaybackNarration { + readonly kind: VoiceNarrationKind; + readonly checkpoint?: IVoiceCheckpointNarrationMetadata; + readonly playbackId?: string; +} + +interface IQueuedAudioResponse { + readonly sessionId: string | undefined; + readonly responseId?: string; + readonly narration?: IPlaybackNarration; + finalized: boolean; + readonly chunks: { audio: string; isFirstChunk: boolean; isFinal: boolean; transcript: string | undefined }[]; +} + +interface IVoiceAgentStateInfo { + readonly state: string; + readonly detail?: string; + readonly confirmation_type?: VoiceConfirmationType; + readonly last_response_summary?: string; +} + +interface IVisibleVoiceQuestionnaire { + readonly context?: string | IMarkdownString; + readonly questions: readonly { + readonly prompt?: string | IMarkdownString; + readonly details?: string | IMarkdownString; + readonly options: readonly string[]; + readonly allowFreeformInput: boolean; + }[]; +} + +function hasOwn<K extends string>(value: object, key: K): value is Record<K, unknown> { + return Object.prototype.hasOwnProperty.call(value, key); +} + export interface IPendingToolConfirmation { readonly type: 'approval' | 'input'; readonly sessionLabel: string; @@ -369,7 +416,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC private _awaitingReplyWatchdog: ReturnType<typeof setTimeout> | undefined; // --- Audio FIFO queue --- - private readonly _audioQueue: { sessionId: string | undefined; responseId?: string; finalized: boolean; chunks: { audio: string; isFirstChunk: boolean; isFinal: boolean; transcript: string | undefined }[] }[] = []; + private readonly _audioQueue: IQueuedAudioResponse[] = []; private _currentPlaybackSessionId: string | undefined | null = null; // null = nothing playing // The narration id of the response currently occupying the playback slot, if // it was a solicited narration. Set when a chunk actually claims the slot and @@ -377,6 +424,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // has truly finished playing (never merely queued or received - see // {@link _markNarrationHeard}). private _currentPlaybackResponseId: string | undefined; + private _currentPlaybackNarration: IPlaybackNarration | undefined; // True once the currently-playing response has received its final audio // chunk. A same-session frame arriving after this marks a NEW response and // must be serialized (queued) rather than fast-pathed, or its audio would be @@ -521,7 +569,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC private _replaySourceNode: AudioBufferSourceNode | undefined; // --- Session state tracking for explicit change notifications --- - private readonly _prevSessionStates = new Map<string, { state: string; detail: string; pendingId: string; lastResponseSummary: string }>(); + private readonly _prevSessionStates = new Map<string, { state: string; detail: string; pendingId: string; confirmationType?: VoiceConfirmationType; lastResponseSummary: string }>(); // Sessions the user explicitly cancelled from VS Code UI. We swallow the // NEXT state change for each (typically the chat model going `idle`) so the @@ -555,7 +603,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC * also records the burst's baseline (``fromState``/``fromDetail``) so a wobble * that returns to its starting state is recognized as net-zero. */ - private readonly _pendingStateChanges = new Map<string, { sessionId: string; currentState: string; label: string; detail?: string; lastResponseSummary?: string; fromState: string; fromDetail: string; fromResponseSummary: string; pendingId: string; fromPendingId: string }>(); + private readonly _pendingStateChanges = new Map<string, { sessionId: string; currentState: string; label: string; detail?: string; confirmationType?: VoiceConfirmationType; lastResponseSummary?: string; fromState: string; fromDetail: string; fromConfirmationType?: VoiceConfirmationType; fromResponseSummary: string; pendingId: string; fromPendingId: string }>(); private _stateChangeEmitTimer: ReturnType<typeof setTimeout> | undefined; private static readonly _STATE_CHANGE_SETTLE_MS = 120; @@ -614,29 +662,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC * {@link _narrate}). Replayed once on the next `session_init` so a reply or * confirmation that landed during a disconnect is still spoken on reconnect. */ - private readonly _pendingNarrationRetries = new Map<string, VoiceNarrationKind>(); - - /** - * Replay a narration that could not be sent while the socket was down. - * - * Re-derives the item from the session as it is *now*, because a form or - * confirmation can be answered, dismissed or replaced during a disconnect. - * Mirrors {@link _retryDeferredNarration} on the busy path. - */ - private _replayPendingNarrationRetry(sessionId: string, queuedKind: VoiceNarrationKind): boolean { - let resource: URI | undefined; - try { - resource = URI.parse(sessionId); - } catch { - resource = undefined; - } - const narratable = resource ? this._currentNarratable(resource) : undefined; - if (!narratable || narratable.kind !== queuedKind) { - this.logService.trace(`[voice] queued narration for ${sessionId.slice(-32)} no longer warranted after reconnect; dropping`); - return false; - } - return this._narrate(sessionId, narratable.kind, narratable.text, undefined, narratable.pending); - } + private readonly _pendingNarrationRetries = new Map<string, IVoiceNarratable>(); /** * Narrations we requested (got a `narration_id` back) but whose audio has not @@ -650,6 +676,16 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC */ private readonly _pendingSolicitedNarrations = new Map<string, IPendingSolicitedNarration>(); private static readonly _SOLICITED_NARRATION_AUDIO_START_TIMEOUT_MS = 30_000; + private static readonly _VOICE_PROGRESS_INITIAL_DELAY_MS = 5_000; + private static readonly _VOICE_PROGRESS_INTERVAL_MS = 10_000; + private static readonly _MAX_VOICE_PROGRESS_PER_REQUEST = 5; + private static readonly _MAX_CONFIRMATION_NARRATION_CHARS = 2_400; + private static readonly _MAX_QUESTIONNAIRE_QUESTIONS = 6; + private static readonly _MAX_QUESTIONNAIRE_OPTIONS = 5; + private static readonly _MAX_CONFIRMATION_FIELD_CHARS = 280; + private readonly _voiceProgressListeners = this._register(new DisposableMap<string, DisposableStore>()); + private readonly _voiceProgressSessionByResponse = new Map<string, string>(); + private readonly _lastSpokenAtBySession = new Map<string, number>(); /** * Narrations the backend bounced (`narration_ack` `busy`) or cancelled @@ -659,7 +695,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC * since a dropped socket loses any in-flight nudge. See * `_retryDeferredNarration`. Cleared on a new turn (`thinking`) or teardown. */ - private readonly _deferredNarrations = new Map<string, { narrationId: string; kind: VoiceNarrationKind; text: string; reuseNarrationId: boolean; pending?: { pendingId: string } }>(); + private readonly _deferredNarrations = new Map<string, IVoiceNarratable & { narrationId: string; reuseNarrationId: boolean }>(); /** * The confirmation detail text last actually HEARD (final audio arrived) per @@ -776,16 +812,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._autoApprovedSessions.add(s.resource.toString()); const model = this.chatService.getSession(s.resource); if (model) { - for (const req of model.getRequests()) { - const pending = req.response?.isPendingConfirmation.get(); - if (pending && req.response) { - for (const part of req.response.response.value) { - if (part.kind === 'toolInvocation') { - IChatToolInvocation.confirmWith(part as IChatToolInvocation, { type: ToolConfirmKind.UserAction }); - } - } - } - } + this._autoApprovePendingTools(model); } } }, @@ -1084,7 +1111,14 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._currentPlaybackFinalized = false; const finishedResponseId = this._currentPlaybackResponseId; this._currentPlaybackResponseId = undefined; + const finishedNarration = this._currentPlaybackNarration; + this._currentPlaybackNarration = undefined; if (finishedResponseId && !wasInterrupted) { + const spokenSessionId = finishedSessionId ?? this._shownSessionId(); + if (spokenSessionId) { + this._lastSpokenAtBySession.set(this._sessionKey(spokenSessionId), Date.now()); + this._notifyCheckpointPlaybackComplete(spokenSessionId, finishedResponseId, finishedNarration); + } // The response actually played to the end: mark it heard (set the // exactly-once dedup and clear its pending indicator). This is the // only point that means the audio truly played through, not merely @@ -1206,7 +1240,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC : s.status === AgentSessionStatus.Completed ? 'idle' : 'unknown'); if (currentState !== 'unknown') { - this._prevSessionStates.set(s.resource.toString(), { state: currentState, detail: info?.detail ?? '', pendingId: currentState === 'waiting_for_confirmation' ? this._pendingIdFor(s.resource.toString()) : '', lastResponseSummary: info?.last_response_summary ?? '' }); + this._prevSessionStates.set(s.resource.toString(), { state: currentState, detail: info?.detail ?? '', pendingId: currentState === 'waiting_for_confirmation' ? this._pendingIdFor(s.resource.toString()) : '', confirmationType: info?.confirmation_type, lastResponseSummary: info?.last_response_summary ?? '' }); } } // Also seed regular chat sessions so the autorun doesn't trigger false transitions @@ -1216,7 +1250,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC if (chatModel.getRequests().length === 0) { continue; } const info = this._getAgentStateInfo(chatModel); if (info.state !== 'unknown') { - this._prevSessionStates.set(key, { state: info.state, detail: info.detail ?? '', pendingId: info.state === 'waiting_for_confirmation' ? this._pendingIdFor(key) : '', lastResponseSummary: info.last_response_summary ?? '' }); + this._prevSessionStates.set(key, { state: info.state, detail: info.detail ?? '', pendingId: info.state === 'waiting_for_confirmation' ? this._pendingIdFor(key) : '', confirmationType: info.confirmation_type, lastResponseSummary: info.last_response_summary ?? '' }); } } @@ -1231,7 +1265,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const autorunDisposable = autorun(reader => { const agentSessions = this.agentSessionsService.model.sessions.filter(s => !s.isArchived()); let needsRecheck = false; - const stateChanges: { sessionId: string; currentState: string; label: string; detail?: string; lastResponseSummary?: string; fromState: string; fromDetail: string; fromResponseSummary: string; pendingId: string; fromPendingId: string }[] = []; + const stateChanges: { sessionId: string; currentState: string; label: string; detail?: string; confirmationType?: VoiceConfirmationType; lastResponseSummary?: string; fromState: string; fromDetail: string; fromConfirmationType?: VoiceConfirmationType; fromResponseSummary: string; pendingId: string; fromPendingId: string }[] = []; const waitingForConfirmationSessions: { sessionId: string; label: string; detail?: string; transition: boolean }[] = []; const processedResources = new Set<string>(); @@ -1243,7 +1277,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC lastReq.response.isIncomplete.read(reader); const pending = lastReq.response.isPendingConfirmation.read(reader); - if (pending && this._autoApprovedSessions.has(sessionId)) { + const confirmationType = getVoiceConfirmationType(lastReq.response.response.value); + if (pending && confirmationType === 'tool' && this._autoApprovedSessions.has(sessionId)) { for (const part of lastReq.response.response.value) { if (part.kind === 'toolInvocation') { if (IChatToolInvocation.confirmWith(part as IChatToolInvocation, { type: ToolConfirmKind.UserAction })) { @@ -1273,6 +1308,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._pendingIdleNarration.delete(sessionId); } const detail = info.detail; + const confirmationType = info.confirmation_type; const lastResponseSummary = info.last_response_summary; // Capture the summary while the model is resident so a later // completion reported after disposal can still narrate. @@ -1286,7 +1322,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // id names the occurrence, and is what makes replacing one form // with another a transition worth narrating. const pendingId = currentState === 'waiting_for_confirmation' ? this._pendingIdFor(sessionId) : ''; - const isDetailTransition = !isStateTransition && prev !== undefined && currentState === 'waiting_for_confirmation' && ((detail ?? '') !== prev.detail || pendingId !== prev.pendingId); + const isDetailTransition = !isStateTransition && prev !== undefined && currentState === 'waiting_for_confirmation' + && ((detail ?? '') !== prev.detail || pendingId !== prev.pendingId || confirmationType !== prev.confirmationType); // A completed reply's summary often lands AFTER the idle // transition (or updates while still idle); the model stays // idle so no state transition fires. Detect the summary @@ -1320,7 +1357,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC clearTimeout(cancelExpiry); this._userCancelledSessions.delete(sessionId); } else { - stateChanges.push({ sessionId, currentState, label, detail, lastResponseSummary, fromState: prev?.state ?? currentState, fromDetail: prev?.detail ?? '', fromResponseSummary: prev?.lastResponseSummary ?? '', pendingId, fromPendingId: prev?.pendingId ?? '' }); + stateChanges.push({ sessionId, currentState, label, detail, confirmationType, lastResponseSummary, fromState: prev?.state ?? currentState, fromDetail: prev?.detail ?? '', fromConfirmationType: prev?.confirmationType, fromResponseSummary: prev?.lastResponseSummary ?? '', pendingId, fromPendingId: prev?.pendingId ?? '' }); } } if (currentState !== 'unknown') { @@ -1328,7 +1365,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // so a model unload→reload can't manufacture an ''→old-summary // "transition" that looks like a fresh reply. const rememberedSummary = normalizedSummary || this._lastResponseSummaryById.get(sessionId) || prev?.lastResponseSummary || ''; - this._prevSessionStates.set(sessionId, { state: currentState, detail: detail ?? '', pendingId, lastResponseSummary: rememberedSummary }); + this._prevSessionStates.set(sessionId, { state: currentState, detail: detail ?? '', pendingId, confirmationType, lastResponseSummary: rememberedSummary }); // Leaving waiting_for_confirmation releases the per-occurrence // narration marker, so the next confirmation - even with // identical text - is narrated afresh on focus. @@ -1410,7 +1447,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } this._sessionsAwaitingResponseSummary.delete(sessionId); if (!this._userCancelledSessions.has(sessionId)) { - stateChanges.push({ sessionId, currentState, label: s.label || 'Untitled session', lastResponseSummary: cachedSummary, fromState: prev?.state ?? currentState, fromDetail: prev?.detail ?? '', fromResponseSummary: prev?.lastResponseSummary ?? '', pendingId: '', fromPendingId: prev?.pendingId ?? '' }); + stateChanges.push({ sessionId, currentState, label: s.label || 'Untitled session', lastResponseSummary: cachedSummary, fromState: prev?.state ?? currentState, fromDetail: prev?.detail ?? '', fromConfirmationType: prev?.confirmationType, fromResponseSummary: prev?.lastResponseSummary ?? '', pendingId: '', fromPendingId: prev?.pendingId ?? '' }); } this._prevSessionStates.set(sessionId, { state: currentState, detail: '', pendingId: '', lastResponseSummary: cachedSummary ?? '' }); continue; @@ -1422,7 +1459,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC clearTimeout(cancelExpiry); this._userCancelledSessions.delete(sessionId); } else { - stateChanges.push({ sessionId, currentState, label: s.label || 'Untitled session', fromState: prev?.state ?? currentState, fromDetail: prev?.detail ?? '', fromResponseSummary: prev?.lastResponseSummary ?? '', pendingId: '', fromPendingId: prev?.pendingId ?? '' }); + stateChanges.push({ sessionId, currentState, label: s.label || 'Untitled session', fromState: prev?.state ?? currentState, fromDetail: prev?.detail ?? '', fromConfirmationType: prev?.confirmationType, fromResponseSummary: prev?.lastResponseSummary ?? '', pendingId: '', fromPendingId: prev?.pendingId ?? '' }); } } if (currentState !== 'unknown') { @@ -1478,7 +1515,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC for (const change of stateChanges) { const existing = this._pendingStateChanges.get(change.sessionId); this._pendingStateChanges.set(change.sessionId, existing - ? { ...change, fromState: existing.fromState, fromDetail: existing.fromDetail, fromResponseSummary: existing.fromResponseSummary, fromPendingId: existing.fromPendingId } + ? { ...change, fromState: existing.fromState, fromDetail: existing.fromDetail, fromConfirmationType: existing.fromConfirmationType, fromResponseSummary: existing.fromResponseSummary, fromPendingId: existing.fromPendingId } : change); } this._scheduleStateChangeEmit(); @@ -1576,8 +1613,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC if (this._pendingNarrationRetries.size > 0) { const retries = [...this._pendingNarrationRetries.entries()]; this._pendingNarrationRetries.clear(); - for (const [sessionId, kind] of retries) { - narrated = this._replayPendingNarrationRetry(sessionId, kind) || narrated; + for (const [sessionId, item] of retries) { + narrated = this._retryPendingNarration(sessionId, item) || narrated; } } // The `narration_unblocked` nudge was lost with the dropped socket, so @@ -1638,6 +1675,22 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC if (this._isInterruptedAudio(e)) { return; } + const solicitedNarration = e.responseId ? this._pendingSolicitedNarrations.get(e.responseId) : undefined; + const echoedCheckpoint: IVoiceCheckpointNarrationMetadata | undefined = e.requestId && e.checkpointId && e.sequence !== undefined + ? { requestId: e.requestId, checkpointId: e.checkpointId, sequence: e.sequence } + : undefined; + const narrationKind = e.narrationKind ?? solicitedNarration?.kind; + const playbackNarration: IPlaybackNarration | undefined = narrationKind + ? { + kind: narrationKind, + checkpoint: echoedCheckpoint ?? solicitedNarration?.checkpoint, + playbackId: e.playbackId, + } + : undefined; + const isCheckpointNarration = playbackNarration?.kind === 'checkpoint'; + if (isCheckpointNarration && e.isFinal) { + this.logService.trace(`[voice][checkpoint] received narration_id=${e.responseId} request_id=${playbackNarration.checkpoint?.requestId ?? '<unknown>'} phase=${playbackNarration.checkpoint?.checkpointId ?? '<unknown>'} sequence=${playbackNarration.checkpoint?.sequence ?? 0} playback_id=${playbackNarration.playbackId ?? '<none>'} spoken=${JSON.stringify(e.transcript ?? '')}`); + } // Latency telemetry: first audio chunk marks end of turn if (e.isFirstChunk && this._telemetryPttUpMs) { const ttft = this._telemetryFirstTranscriptionMs && this._telemetryPttDownMs @@ -1670,6 +1723,14 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC if (e.audio) { this._markSolicitedNarrationAudioStarted(e.responseId); } + if (isCheckpointNarration && solicitedNarration && e.isFinal && !e.audio && !solicitedNarration.hasReceivedAudio) { + if (e.responseId) { + this._clearPendingSolicitedNarration(e.responseId, solicitedNarration); + this._solicitedNarrationIds.delete(e.responseId); + this._responseRoutes.delete(e.responseId); + } + return; + } // If this response is for a session the user isn't currently looking // at, don't play it now: buffer it until that session is focused and // notify with a short audio cue instead. When the backend echoes a @@ -1689,9 +1750,18 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // Backend re-narrated a reply we already read for this session // (matched by content). Drop it so the user never hears it twice. this.logService.trace(`[voice] dropping re-narration for session=${codingSessionId} responseId=${e.responseId?.slice(0, 8) ?? '<none>'} isFirstChunk=${e.isFirstChunk} isFinal=${e.isFinal}`); + } else if (defer && isCheckpointNarration) { + if (e.responseId && solicitedNarration) { + this._clearPendingSolicitedNarration(e.responseId, solicitedNarration); + this._solicitedNarrationIds.delete(e.responseId); + } + return; } else if (defer) { this._deferResponse(codingSessionId!, e.audio, e.isFirstChunk, e.isFinal, e.transcript, e.responseId, e.turnId); } else { + if (e.audio && !isCheckpointNarration) { + this._preemptCheckpointPlayback(); + } // A fresh reply is about to play live for this session. Anything // still buffered for it (earlier background updates the user never // returned to hear) must be played FIRST, in order, so nothing is @@ -1702,7 +1772,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC && !this._deferredBufferHasResponse(codingSessionId, e.responseId)) { this._flushDeferredResponse(codingSessionId); } - this._enqueueAudio(codingSessionId, e.audio, e.isFirstChunk, e.isFinal, e.transcript, e.responseId); + this._enqueueAudio(codingSessionId, e.audio, e.isFirstChunk, e.isFinal, e.transcript, e.responseId, playbackNarration); if (e.isFinal) { this._liveReplyKeys.delete(codingSessionId ?? ''); // Record this heard reply so an immediate backend re-narration @@ -1716,7 +1786,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // (dropping its next reply / misrouting this one). See // _reconcileConfirmationIndicators for the same caveat. const heardSessionId = codingSessionId ?? this._awaitingReplyForSession ?? this._shownSessionId(); - if (heardSessionId && e.transcript) { + if (!isCheckpointNarration && heardSessionId && e.transcript) { const heard = this._normalizeTranscript(e.transcript); if (heard) { const heardKey = this._sessionKey(heardSessionId); @@ -1727,7 +1797,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } } // On the final chunk we have the complete assistant transcript to persist. - if (e.isFinal && e.transcript) { + if (!isCheckpointNarration && e.isFinal && e.transcript) { this._persistTurn('assistant', e.transcript); } // NOTE: a reply is marked "heard" (dedup set, pending indicator cleared) @@ -1958,6 +2028,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._audioQueue.length = 0; this._currentPlaybackSessionId = null; this._currentPlaybackResponseId = undefined; + this._currentPlaybackNarration = undefined; this._isProcessingQueue = false; this._suppressIncomingAudio = false; this._interruptedAudioIds.clear(); @@ -1998,6 +2069,9 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._lastResponseSummaryById.clear(); this._lastNarratedText.clear(); this._pendingNarrationRetries.clear(); + this._voiceProgressListeners.clearAndDisposeAll(); + this._voiceProgressSessionByResponse.clear(); + this._lastSpokenAtBySession.clear(); for (const [narrationId, pending] of this._pendingSolicitedNarrations) { this._clearPendingSolicitedNarration(narrationId, pending); } @@ -2078,6 +2152,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._audioQueue.length = 0; this._currentPlaybackSessionId = null; this._currentPlaybackResponseId = undefined; + this._currentPlaybackNarration = undefined; this._isProcessingQueue = false; this.ttsPlaybackService.closeContext(); this.micCaptureService.stopCapture(); @@ -2095,6 +2170,9 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._solicitedNarrationIds.clear(); this._cancelledPendingNarrationIds.clear(); this._pendingNarrationRetries.clear(); + this._voiceProgressListeners.clearAndDisposeAll(); + this._voiceProgressSessionByResponse.clear(); + this._lastSpokenAtBySession.clear(); this._deferredNarrations.clear(); this._narratedPending.clear(); // Terminal disconnect (no reconnect): drop the routing target and @@ -2302,6 +2380,11 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // afterwards goes through the normal `pttUp()` path. if (this._bargeInListenActive) { this.logService.trace('[voice] pttDown: promoting passive barge-in listen to user interrupt'); + const shownSessionId = this._shownSessionId(); + if (shownSessionId) { + this._cancelVoiceProgress(shownSessionId); + } + this._preemptCheckpointPlayback(undefined, undefined, false); this._bargeInListenActive = false; // A promoted press is a deliberate interrupt, so it latches the backend // like a fresh press: clear the passive flag (kept consistent with the @@ -2324,6 +2407,9 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._startUserTurn(); this._audioQueue.length = 0; this._currentPlaybackSessionId = null; + this._currentPlaybackResponseId = undefined; + this._currentPlaybackNarration = undefined; + this._currentPlaybackFinalized = false; this._isProcessingQueue = false; this._suppressIncomingAudio = true; this.ttsPlaybackService.stopPlayback(); @@ -2344,6 +2430,13 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } if (this._pttHeld) { this.logService.trace('[voice] pttDown ignored: already held'); return; } + if (source === 'explicit') { + const shownSessionId = this._shownSessionId(); + if (shownSessionId) { + this._cancelVoiceProgress(shownSessionId); + } + this._preemptCheckpointPlayback(undefined, undefined, false); + } this._pttHeld = true; this._pttCurrentTurnPassive = passive; this._autoListenSuppressed = false; @@ -2377,6 +2470,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._audioQueue.length = 0; this._currentPlaybackSessionId = null; this._currentPlaybackResponseId = undefined; + this._currentPlaybackNarration = undefined; + this._currentPlaybackFinalized = false; this._isProcessingQueue = false; this._suppressIncomingAudio = true; @@ -2595,6 +2690,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } markUserCancelled(sessionId: string): void { + this._cancelVoiceProgress(sessionId); + this._preemptCheckpointPlayback(sessionId); const existing = this._userCancelledSessions.get(sessionId); if (existing) { clearTimeout(existing); } const expiry = setTimeout(() => { @@ -2850,6 +2947,167 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } } + private _acceptVoiceInput(text: string, sessionResource: URI): void { + this.commandService.executeCommand<IChatResponseModel | undefined>('_chat.voice.acceptInput', text).then(response => { + this.logService.info(`[voice] acceptInput completed session=${sessionResource.toString()} response=${response?.id ?? 'none'} connected=${this._isConnected.get()}`); + if (response && this._isConnected.get()) { + this._watchVoiceProgress(sessionResource, response); + } + }).catch(err => this.logService.warn('[voice] acceptInput failed:', err)); + } + + private async _sendVoiceRequest(sessionResource: URI, text: string): Promise<ChatSendResult | undefined> { + const result = await this.chatService.sendRequest(sessionResource, text, { isVoiceModeInput: this._isVoiceProgressEnabled() }).catch(err => { + this.logService.warn('[voice] Error sending transcription:', err); + return undefined; + }); + if (!result) { + return undefined; + } + + const sentResult = ChatSendResult.isQueued(result) ? result.deferred : Promise.resolve(result); + sentResult.then(async sent => { + if (ChatSendResult.isSent(sent)) { + const response = await sent.data.responseCreatedPromise; + if (this._isConnected.get()) { + this._watchVoiceProgress(sessionResource, response); + } + } + }).catch(err => this.logService.warn('[voice] Failed to watch voice response:', err)); + return result; + } + + private _watchVoiceProgress(sessionResource: URI, response: IChatResponseModel): void { + if (!this._isVoiceProgressEnabled()) { + return; + } + const disposables = new DisposableStore(); + const timer = disposables.add(new MutableDisposable()); + const seen = new Set<string>(); + const sessionId = sessionResource.toString(); + const sessionKey = this._sessionKey(sessionId); + const requestStartedAt = Date.now(); + let narratedCount = 0; + let lastCheckpointAt: number | undefined; + let nextSequence = 1; + let pending: { id: VoiceCheckpointId; value: string } | undefined; + this.logService.info(`[voice] watching progress session=${sessionId} response=${response.id} request=${response.requestId}`); + + const dispose = () => this._voiceProgressListeners.deleteAndDispose(response.id); + const nextEligibleAt = () => { + if (lastCheckpointAt !== undefined) { + return lastCheckpointAt + VoiceSessionController._VOICE_PROGRESS_INTERVAL_MS; + } + const lastSpokenAt = this._lastSpokenAtBySession.get(sessionKey); + return Math.max( + requestStartedAt + VoiceSessionController._VOICE_PROGRESS_INITIAL_DELAY_MS, + (lastSpokenAt ?? 0) + VoiceSessionController._VOICE_PROGRESS_INITIAL_DELAY_MS, + ); + }; + const flush = () => { + timer.clear(); + if (!this._isVoiceProgressEnabled()) { + dispose(); + return; + } + if (response.isComplete || response.isCanceled) { + dispose(); + return; + } + if (!pending || narratedCount >= VoiceSessionController._MAX_VOICE_PROGRESS_PER_REQUEST) { + return; + } + if (!this._isConnected.get()) { + return; + } + const canReplacePlayingCheckpoint = this._currentPlaybackNarration?.kind === 'checkpoint'; + if (this.ttsPlaybackService.isPlaying && !canReplacePlayingCheckpoint) { + return; + } + const delay = nextEligibleAt() - Date.now(); + if (delay > 0) { + timer.value = disposableTimeout(flush, delay); + return; + } + + const checkpoint = pending; + pending = undefined; + const metadata: IVoiceCheckpointNarrationMetadata = { + requestId: response.requestId, + checkpointId: checkpoint.id, + sequence: nextSequence++, + }; + const narrated = this._isConnected.get() + && this._isSameSession(sessionId, this._shownSessionId()) + && this._narrate(sessionId, 'checkpoint', checkpoint.value, undefined, metadata); + this.logService.info(`[voice] checkpoint dispatch session=${sessionId} response=${response.id} stage=${checkpoint.id} sequence=${metadata.sequence} narrated=${Boolean(narrated)}`); + if (narrated) { + narratedCount++; + lastCheckpointAt = Date.now(); + } + }; + const schedule = () => { + timer.clear(); + const delay = nextEligibleAt() - Date.now(); + if (delay <= 0) { + flush(); + } else { + timer.value = disposableTimeout(flush, delay); + } + }; + const update = () => { + if (!this._isVoiceProgressEnabled()) { + dispose(); + return; + } + if (response.isComplete || response.isCanceled) { + this._preemptCheckpointPlayback(sessionId); + dispose(); + return; + } + for (const part of response.response.value) { + if (part.kind !== 'voiceProgress' || !isVoiceCheckpointId(part.id) || seen.has(part.id)) { + continue; + } + seen.add(part.id); + pending = { id: part.id, value: part.value }; + this.logService.info(`[voice] checkpoint observed session=${sessionId} response=${response.id} stage=${part.id}`); + } + if (pending) { + schedule(); + } + }; + + disposables.add(response.onDidChange(update)); + disposables.add(autorun(reader => { + if (this._isConnected.read(reader) && pending) { + schedule(); + } + })); + disposables.add(this.ttsPlaybackService.onPlaybackStopped(() => { + if (pending) { + schedule(); + } + })); + disposables.add({ dispose: () => this._voiceProgressSessionByResponse.delete(response.id) }); + this._voiceProgressListeners.set(response.id, disposables); + this._voiceProgressSessionByResponse.set(response.id, sessionKey); + update(); + } + + private _isVoiceProgressEnabled(): boolean { + return this.configurationService.getValue<boolean>(VOICE_AGENT_PROGRESS_SETTING) === true; + } + + private _cancelVoiceProgress(sessionId?: string): void { + const sessionKey = sessionId ? this._sessionKey(sessionId) : undefined; + for (const responseId of [...this._voiceProgressListeners.keys()]) { + if (sessionKey === undefined || this._voiceProgressSessionByResponse.get(responseId) === sessionKey) { + this._voiceProgressListeners.deleteAndDispose(responseId); + } + } + } + /** * Send transcription text to the target session or active chat. */ @@ -2865,9 +3123,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC if (isTargetVisible) { // Target is visible — send via the chat pane directly - await this.commandService.executeCommand('_chat.voice.acceptInput', text).catch(err => { - this.logService.warn('[voice] acceptInput failed for visible target:', err); - }); + this._acceptVoiceInput(text, target); } else { // Target is NOT visible — ensure session is loaded, then send const cts = new CancellationTokenSource(); @@ -2882,14 +3138,11 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const switched = await this.commandService.executeCommand<boolean>('_chat.voice.switchToSession', target.toString()).catch(() => false); if (switched) { await new Promise(resolve => setTimeout(resolve, 200)); - await this.commandService.executeCommand('_chat.voice.acceptInput', text).catch(() => { }); + this._acceptVoiceInput(text, target); } return; } - const result = await this.chatService.sendRequest(target, text).catch(err => { - this.logService.warn('[voice] Error sending transcription to target session:', err); - return undefined; - }); + const result = await this._sendVoiceRequest(target, text); if (result && result.kind !== 'rejected') { // Surface response in floating window this._watchResponseForFloatingWindow(target); @@ -2922,9 +3175,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const currentSession = await this.commandService.executeCommand<string | undefined>('_chat.voice.getCurrentSession').catch(() => undefined); if (currentSession) { // There's an active chat widget — send to it - this.commandService.executeCommand('_chat.voice.acceptInput', text).catch(err => { - this.logService.warn('[voice] acceptInput failed for current session:', err); - }); + this._acceptVoiceInput(text, URI.parse(currentSession)); } else { // No focused chat session — find the most recent existing session // instead of creating a new one, so voice continues the conversation. @@ -2937,14 +3188,10 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const switched = await this.commandService.executeCommand<boolean>('_chat.voice.switchToSession', sessionResource.toString()).catch(() => false); if (switched) { await new Promise(resolve => setTimeout(resolve, 200)); - await this.commandService.executeCommand('_chat.voice.acceptInput', text).catch(err => { - this.logService.warn('[voice] acceptInput failed after switch to existing:', err); - }); + this._acceptVoiceInput(text, sessionResource); } else { // Direct send as fallback - this.chatService.sendRequest(sessionResource, text).catch(err => { - this.logService.warn('[voice] Error sending transcription to existing session:', err); - }); + await this._sendVoiceRequest(sessionResource, text); } } else { // Truly no sessions exist — create one @@ -2953,9 +3200,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC ref.dispose(); // Switch to the new session so the user sees the response this.commandService.executeCommand('_chat.voice.switchToSession', resource.toString()).catch(() => { /* pane may not exist */ }); - this.chatService.sendRequest(resource, text).catch(err => { - this.logService.warn('[voice] Error sending transcription to new session:', err); - }); + await this._sendVoiceRequest(resource, text); } } @@ -3536,7 +3781,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } else if (bufferRetainedUnderPress) { this.logService.trace(`[voice] activate skip: buffered reply retained under held press for ${key.slice(-32)}`); } else { - this._narrate(key, narratable.kind, narratable.text, undefined, narratable.pending); + this._narrate(key, narratable.kind, narratable.text, undefined, undefined, narratable.confirmationType, narratable.pending); } if (narratable.kind === 'response') { // A request being SENT is not the reply being heard: keep the @@ -3561,7 +3806,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } /** Ask the backend to narrate a session's pending item, de-duped by the exact text last spoken for it ({@link _lastNarratedText}) and by any in-flight request for the same text ({@link _pendingSolicitedNarrations}); the single narration trigger for both live and on-focus paths. Returns `true` when a request was actually SENT - NOT that the reply was heard (the audio may still be dropped/deferred/never arrive). The reply is marked narrated and its pending indicator cleared only once its audio finalizes (see {@link _markNarrationHeard}). */ - private _narrate(sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, pending?: { pendingId: string }): boolean { + private _narrate(sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata, confirmationType?: VoiceConfirmationType, pending?: { pendingId: string }): boolean { if (!text) { return false; } @@ -3579,22 +3824,36 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // and on the pending id so a *different* form that happens to render the // same prompt is not mistaken for the one already in flight. const sessionKey = this._sessionKey(sessionId); - const identity = this._narratableIdentity({ text, pending }); + const identity = this._narratableIdentity({ text, pending, confirmationType }); for (const s of this._pendingSolicitedNarrations.values()) { if (s.kind === kind && this._narratableIdentity(s) === identity && this._sessionKey(s.sessionId) === sessionKey) { return false; } } + // A response only supersedes checkpoint playback once non-empty response audio arrives. + if (kind !== 'response') { + this._preemptCheckpointPlayback(); + } + if (kind === 'confirmation') { + this._sendContext(); + this.voiceClientService.flushSessionContext(); + } this.logService.trace(`[voice] narrate kind=${kind} id=${sessionId.slice(-32)}`); - const narrationId = this.voiceClientService.requestNarration(sessionId, kind, text, reuseId, pending); + const narrationId = this.voiceClientService.requestNarration(sessionId, kind, text, reuseId, checkpoint, confirmationType, pending); if (!narrationId) { + if (kind === 'checkpoint') { + return false; + } // Socket was closed, so nothing was sent: don't touch playback/listening // state (that would tear down a freshly-entered listen on connect). // Remember the item so the next session_init replays it after resume; // leaving the dedup unset lets a later focus/state event retry too. - this._pendingNarrationRetries.set(sessionId, kind); + this._pendingNarrationRetries.set(sessionId, { kind, text, confirmationType, pending }); return false; } + if (kind === 'checkpoint') { + this.logService.trace(`[voice][checkpoint] requested narration_id=${narrationId} request_id=${checkpoint?.requestId ?? '<unknown>'} phase=${checkpoint?.checkpointId ?? '<unknown>'} sequence=${checkpoint?.sequence ?? 0} seed=${JSON.stringify(text)}`); + } // The narration audio is now inbound. Get out of listening/auto-listen so // the echoed audio isn't suppressed (or captured as the user's own turn) // while PTT/mic capture is active. Done here so every narration path @@ -3636,6 +3895,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC kind, text, pending, + checkpoint, + confirmationType, audioStartTimer, hasReceivedAudio: false, }); @@ -3693,6 +3954,12 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._pendingSolicitedNarrations.delete(narrationId); } + private _notifyCheckpointPlaybackComplete(sessionId: string, narrationId: string, narration: IPlaybackNarration | undefined): void { + if (narration?.kind === 'checkpoint' && narration.playbackId) { + this.voiceClientService.sendNarrationPlaybackComplete(sessionId, narrationId, narration.playbackId); + } + } + private _restoreVoiceStateAfterNarrationTimeout(): void { if (this.ttsPlaybackService.isPlaying || this._audioQueue.length > 0 || this._currentPlaybackSessionId !== null || this._pttHeld) { return; @@ -3722,7 +3989,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC if (solicited.kind === 'response') { this._lastNarratedText.set(sessionKey, solicited.text); this._clearPendingResponse(sessionKey); - } else { + } else if (solicited.kind === 'confirmation') { // Confirmation heard: mark THIS occurrence spoken so a mere refocus // while it is still pending doesn't re-narrate it (see // _activateShownSession). Cleared when the session leaves @@ -3742,7 +4009,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC * `busy` means the backend could not play right now (user speaking / reply in * flight); it will nudge us with `narration_unblocked` when the guard clears, * so we stop tracking the id as in-flight and remember it for a revalidated - * retry. `invalid` is terminal, so we drop it entirely. + * retry. `invalid` and legacy `suppressed` are terminal, so we drop them entirely. */ private _handleNarrationAck(e: IVoiceNarrationAck): void { if (e.disposition === 'accepted') { @@ -3754,11 +4021,11 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._clearPendingSolicitedNarration(e.narrationId, solicited); } this._solicitedNarrationIds.delete(e.narrationId); - if (e.disposition === 'invalid') { - this.logService.trace(`[voice] narration_ack invalid id=${e.narrationId.slice(0, 8)} reason=${e.reason ?? '<none>'}; dropping`); + if (e.disposition === 'invalid' || e.disposition === 'suppressed') { + this.logService.trace(`[voice] narration_ack ${e.disposition} id=${e.narrationId.slice(0, 8)} reason=${e.reason ?? '<none>'}; dropping`); this._clearDeferred(key); if (solicited) { - this.telemetryService.publicLog2<VoiceNarrationDroppedEvent, VoiceNarrationDroppedClassification>('voiceNarrationDropped', { kind: solicited.kind, reason: 'invalid' }); + this.telemetryService.publicLog2<VoiceNarrationDroppedEvent, VoiceNarrationDroppedClassification>('voiceNarrationDropped', { kind: solicited.kind, reason: e.disposition }); } return; } @@ -3766,8 +4033,12 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const kind = solicited?.kind; const text = solicited?.text; if (kind && text) { + if (kind === 'checkpoint') { + this.logService.trace(`[voice] narration_ack busy id=${e.narrationId.slice(0, 8)}; dropping checkpoint`); + return; + } this.logService.trace(`[voice] narration_ack busy id=${e.narrationId.slice(0, 8)} reason=${e.reason ?? '<none>'}; deferring`); - this._deferredNarrations.set(key, { narrationId: e.narrationId, kind, text, reuseNarrationId: true, pending: solicited?.pending }); + this._deferredNarrations.set(key, { narrationId: e.narrationId, kind, text, reuseNarrationId: true, confirmationType: solicited.confirmationType, pending: solicited.pending }); this.telemetryService.publicLog2<VoiceNarrationDeferredEvent, VoiceNarrationDeferredClassification>('voiceNarrationDeferred', { kind, reason: 'busy' }); } } @@ -3781,6 +4052,10 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC private _handleNarrationInterrupted(e: IVoiceNarrationSignal): void { const solicited = this._pendingSolicitedNarrations.get(e.narrationId); if (solicited) { + if (solicited.kind === 'checkpoint') { + this._preemptCheckpointPlayback(e.codingSessionId, e.narrationId); + return; + } this._deferInterruptedNarration(e.narrationId, solicited); this.logService.trace(`[voice] narration_interrupted id=${e.narrationId.slice(0, 8)}; deferring for revalidation`); this.telemetryService.publicLog2<VoiceNarrationDeferredEvent, VoiceNarrationDeferredClassification>('voiceNarrationDeferred', { kind: solicited.kind, reason: 'interrupted' }); @@ -3792,12 +4067,16 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC private _deferInterruptedNarration(narrationId: string, solicited: IPendingSolicitedNarration): void { this._clearPendingSolicitedNarration(narrationId, solicited); this._solicitedNarrationIds.delete(narrationId); + if (solicited.kind === 'checkpoint') { + return; + } this._deferredNarrations.set(this._sessionKey(solicited.sessionId), { narrationId, kind: solicited.kind, text: solicited.text, reuseNarrationId: false, pending: solicited.pending, + confirmationType: solicited.confirmationType, }); } @@ -3826,7 +4105,10 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC resource = undefined; } const narratable = resource ? this._currentNarratable(resource) : undefined; - if (!narratable || narratable.kind !== deferred.kind) { + if (!narratable + || narratable.kind !== deferred.kind + || narratable.text !== deferred.text + || (deferred.kind === 'confirmation' && narratable.confirmationType !== deferred.confirmationType)) { this.logService.trace(`[voice] deferred narration for ${sessionKey.slice(-32)} no longer warranted; dropping`); this._clearDeferred(sessionKey); this.telemetryService.publicLog2<VoiceNarrationDroppedEvent, VoiceNarrationDroppedClassification>('voiceNarrationDropped', { kind: deferred.kind, reason: 'stale' }); @@ -3849,7 +4131,29 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const reuseId = deferred.reuseNarrationId && this._narratableIdentity(narratable) === this._narratableIdentity(deferred) ? deferred.narrationId : undefined; this.logService.trace(`[voice] retrying deferred narration for ${sessionKey.slice(-32)} reuse=${!!reuseId}`); this._clearDeferred(sessionKey); - return this._narrate(sessionKey, narratable.kind, narratable.text, reuseId, narratable.pending); + return this._narrate(sessionKey, narratable.kind, narratable.text, reuseId, undefined, narratable.confirmationType, narratable.pending); + } + + private _retryPendingNarration(sessionId: string, pending: IVoiceNarratable): boolean { + let resource: URI; + try { + resource = URI.parse(sessionId); + } catch { + this.logService.trace(`[voice] queued confirmation for invalid session id; dropping`); + return false; + } + const current = this._currentNarratable(resource); + if (!current + || current.kind !== pending.kind + || this._narratableIdentity(current) !== this._narratableIdentity(pending)) { + this.logService.trace(`[voice] queued narration for ${sessionId.slice(-32)} no longer matches current state; dropping`); + return false; + } + if (current.kind !== 'response' && this._shouldDeferForSession(this._sessionKey(sessionId))) { + this.logService.trace(`[voice] queued narration for ${sessionId.slice(-32)} is no longer shown; dropping`); + return false; + } + return this._narrate(sessionId, current.kind, current.text, undefined, undefined, current.confirmationType, current.pending); } /** Drop a deferred narration. */ @@ -3858,7 +4162,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } /** The pending item a session would narrate now (waiting confirmation prompt or completed reply summary), from the resident model or cached summary/status; returns undefined (kicking off a load) if a confirmation's detail isn't ready. */ - private _currentNarratable(resource: URI): { kind: VoiceNarrationKind; text: string; pending?: { pendingId: string } } | undefined { + private _currentNarratable(resource: URI): IVoiceNarratable | undefined { const model = this.chatService.getSession(resource); if (model) { // A question form is narrated from the structured payload, not from @@ -3871,7 +4175,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } const info = this._getAgentStateInfo(model); if (info.state === 'waiting_for_confirmation' && info.detail) { - return { kind: 'confirmation', text: info.detail }; + return { kind: 'confirmation', text: info.detail, confirmationType: info.confirmation_type }; } if (info.state === 'idle' && info.last_response_summary) { return { kind: 'response', text: info.last_response_summary }; @@ -3923,8 +4227,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC * prompt, so keying "already heard" on text alone swallows the second one. * Text is only a fallback for narratables with no structured pending. */ - private _narratableIdentity(narratable: { text: string; pending?: { pendingId: string } }): string { - return narratable.pending ? `#${narratable.pending.pendingId}` : narratable.text; + private _narratableIdentity(narratable: { text: string; pending?: { pendingId: string }; confirmationType?: VoiceConfirmationType }): string { + return narratable.pending ? `#${narratable.pending.pendingId}` : `${narratable.confirmationType ?? ''}:${narratable.text}`; } /** @@ -3937,7 +4241,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC * debounce window before the backend's mirror catches up, which is by * definition first sighting. */ - private _questionNarratable(model: IChatModel | undefined | null): { kind: VoiceNarrationKind; text: string; pending: { pendingId: string } } | undefined { + private _questionNarratable(model: IChatModel | undefined | null): { kind: 'question'; text: string; pending: { pendingId: string } } | undefined { const pending = model ? this._buildPendingPayload(model) : undefined; const question = pending?.type === 'questions' ? pending.questions?.[0] : undefined; if (!pending || !question) { @@ -4539,20 +4843,95 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // --- Audio FIFO queue --- + private _preemptCheckpointPlayback(sessionId?: string, targetNarrationId?: string, stopActivePlayback = true): void { + const sessionKey = sessionId ? this._sessionKey(sessionId) : undefined; + const shouldPreempt = (candidateSessionId: string | undefined, candidateNarrationId: string | undefined, narration: IPlaybackNarration | undefined) => { + return narration?.kind === 'checkpoint' + && (targetNarrationId === undefined || candidateNarrationId === targetNarrationId) + && (sessionKey === undefined || (candidateSessionId !== undefined && this._sessionKey(candidateSessionId) === sessionKey)); + }; + + const interruptedIds = new Set<string>(); + for (let i = this._audioQueue.length - 1; i >= 0; i--) { + const queued = this._audioQueue[i]; + if (!shouldPreempt(queued.sessionId, queued.responseId, queued.narration)) { + continue; + } + if (queued.responseId) { + interruptedIds.add(queued.responseId); + } + this._audioQueue.splice(i, 1); + } + for (const [candidateNarrationId, pending] of this._pendingSolicitedNarrations) { + if (pending.kind !== 'checkpoint' + || (targetNarrationId !== undefined && candidateNarrationId !== targetNarrationId) + || (sessionKey !== undefined && this._sessionKey(pending.sessionId) !== sessionKey)) { + continue; + } + interruptedIds.add(candidateNarrationId); + this._clearPendingSolicitedNarration(candidateNarrationId, pending); + this._solicitedNarrationIds.delete(candidateNarrationId); + } + for (const narrationId of interruptedIds) { + this._rememberInterruptedAudioId(narrationId); + } + + const activeCheckpointMatches = shouldPreempt(this._currentPlaybackSessionId ?? undefined, this._currentPlaybackResponseId, this._currentPlaybackNarration); + if (activeCheckpointMatches && this._currentPlaybackResponseId) { + this._rememberInterruptedAudioId(this._currentPlaybackResponseId); + } + if (activeCheckpointMatches && stopActivePlayback) { + this._stopCurrentPlaybackAsInterrupted(); + } + } + private _interruptAssistantPlayback(): void { + const interruptedSessionId = this._currentPlaybackSessionId ?? this._shownSessionId(); + if (interruptedSessionId) { + this._cancelVoiceProgress(interruptedSessionId); + } + this._preemptCheckpointPlayback(undefined, undefined, false); this._rememberInterruptedPlaybackIds(); this._telemetryTtsInterrupted = this._telemetryTtsInterrupted || this.ttsPlaybackService.isPlaying; this._audioQueue.length = 0; this._currentPlaybackSessionId = null; + this._currentPlaybackFinalized = false; this._isProcessingQueue = false; this._suppressIncomingAudio = true; this.ttsPlaybackService.stopPlayback(); // Clear any narration id left over if stopPlayback didn't fire onPlaybackStopped // (e.g. nothing was playing), so a later stray stop can't consume a stale id. this._currentPlaybackResponseId = undefined; + this._currentPlaybackNarration = undefined; this.voicePlaybackService.notifyPlaybackEnd(undefined); } + private _stopCurrentPlaybackAsInterrupted(): void { + if (this.ttsPlaybackService.isPlaying) { + this._telemetryTtsInterrupted = true; + this.ttsPlaybackService.stopPlayback(); + return; + } + + // The controller claims the playback slot before WebAudio finishes decoding. + // Stopping during that window emits no playback-stopped event, so close the + // lifecycle here instead of leaking interruption state into the next reply. + this.ttsPlaybackService.stopPlayback(); + this._telemetryTtsInterrupted = false; + this._currentPlaybackSessionId = null; + this._currentPlaybackResponseId = undefined; + this._currentPlaybackNarration = undefined; + this._currentPlaybackFinalized = false; + this.voicePlaybackService.notifyPlaybackEnd(undefined); + if (this._audioQueue.length > 0) { + if (!this._isProcessingQueue) { + this._processQueue(); + } + } else { + this._restoreVoiceStateAfterNarrationTimeout(); + } + } + /** * Stop reading an actionable pending request aloud once it has been resolved * (e.g. the user pressed Allow, or answered the form with the mouse, before @@ -4629,12 +5008,12 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // "heard"; that handler then resets the slot, drains the queue and // restores idle / hands-free listening. if (this._currentPlaybackResponseId !== undefined && cancelledIds.has(this._currentPlaybackResponseId)) { - this._telemetryTtsInterrupted = true; - this.ttsPlaybackService.stopPlayback(); + this._stopCurrentPlaybackAsInterrupted(); } } - private _enqueueAudio(sessionId: string | undefined, audio: string, isFirstChunk: boolean, isFinal: boolean, transcript: string | undefined, responseId?: string): void { + private _enqueueAudio(sessionId: string | undefined, audio: string, isFirstChunk: boolean, isFinal: boolean, transcript: string | undefined, responseId?: string, narration?: IPlaybackNarration): void { + const isCheckpointNarration = narration?.kind === 'checkpoint'; // An incoming response frame means the assistant is actively replying, so // cancel any pending auto-listen. Otherwise a debounced listen scheduled // when the previous session's playback stopped can fire mid-response and @@ -4659,7 +5038,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } } - if (isFirstChunk) { + if (isFirstChunk && !isCheckpointNarration) { this._clearAwaitingReply(); } @@ -4678,7 +5057,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // too - forcing a fresh turn once the current one finishes. const continuationOfCurrent = sameSession && !isFirstChunk && !this._currentPlaybackFinalized; if ((nothingPlaying && this._audioQueue.length === 0) || continuationOfCurrent) { - this._playChunk(sessionId, audio, isFirstChunk, isFinal, transcript, responseId); + this._playChunk(sessionId, audio, isFirstChunk, isFinal, transcript, responseId, narration); return; } @@ -4694,7 +5073,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC !e.finalized && (e.sessionId === sessionId || (e.sessionId === undefined && sessionId === undefined)) ); if (!entry) { - entry = { sessionId, responseId, finalized: false, chunks: [] }; + entry = { sessionId, responseId, narration, finalized: false, chunks: [] }; this._audioQueue.push(entry); } entry.chunks.push({ audio, isFirstChunk, isFinal, transcript }); @@ -4708,7 +5087,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } } - private _playChunk(sessionId: string | undefined, audio: string, isFirstChunk: boolean, isFinal: boolean, transcript: string | undefined, responseId?: string): void { + private _playChunk(sessionId: string | undefined, audio: string, isFirstChunk: boolean, isFinal: boolean, transcript: string | undefined, responseId?: string, narration?: IPlaybackNarration): void { + const isCheckpointNarration = narration?.kind === 'checkpoint'; // Streaming pipeline sends a monotonically-growing transcript on every // chunk. On the FIRST chunk of a response we push a fresh assistant // turn into the rolling buffer; on subsequent chunks we REPLACE that @@ -4736,11 +5116,14 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // Track the response now occupying the slot so onPlaybackStopped can // mark it heard once its audio truly finishes (not merely queued). this._currentPlaybackResponseId = responseId; + this._currentPlaybackNarration = narration; // A same-session frame arriving after the final chunk is a NEW // response and must be serialized (see `_enqueueAudio`). this._currentPlaybackFinalized = isFinal; this._clearAutoListenTimer(); - this._replyPlayedSinceSend = true; + if (!isCheckpointNarration) { + this._replyPlayedSinceSend = true; + } this._voiceState.set('speaking', undefined); this._statusText.set('Speaking...', undefined); this.ttsPlaybackService.playAudioChunk(audio, isFinal, this._window!); @@ -4756,14 +5139,20 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this.micCaptureService.suppressUntil(Date.now() + 800); } } else if (!speakResponsesEnabled) { - this._replyPlayedSinceSend = true; + if (!isCheckpointNarration) { + this._replyPlayedSinceSend = true; + } if (isFinal) { this._currentPlaybackSessionId = null; this._currentPlaybackResponseId = undefined; + this._currentPlaybackNarration = undefined; // Speech is disabled so no audio plays and onPlaybackStopped won't // fire: the reply is nonetheless consumed, so mark the solicited // narration heard here to clear its pending indicator. if (responseId) { + if (sessionId) { + this._notifyCheckpointPlaybackComplete(sessionId, responseId, narration); + } this._markNarrationHeard(responseId); } // Avoid re-entering _processQueue if we're already inside its @@ -4797,7 +5186,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC while (this._currentPlaybackSessionId === null && this._audioQueue.length > 0) { const next = this._audioQueue.shift()!; for (const chunk of next.chunks) { - this._playChunk(next.sessionId, chunk.audio, chunk.isFirstChunk, chunk.isFinal, chunk.transcript, next.responseId); + this._playChunk(next.sessionId, chunk.audio, chunk.isFirstChunk, chunk.isFinal, chunk.transcript, next.responseId, next.narration); } } this._isProcessingQueue = false; @@ -4867,8 +5256,11 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } /** React to a session reaching a narratable state. If it's the shown session, speak it now; a completed reply on a background session instead shows the sessions-list pending indicator and is read when focused. A new turn (`thinking`) clears both the dedup and any stale pending indicator. */ - private _handleNarratableStateChange(sessionId: string, currentState: string, detail: string | undefined, lastResponseSummary: string | undefined, shownNow: string | undefined): void { + private _handleNarratableStateChange(sessionId: string, currentState: string, detail: string | undefined, lastResponseSummary: string | undefined, shownNow: string | undefined, confirmationType?: VoiceConfirmationType): void { const sessionKey = this._sessionKey(sessionId); + if (currentState === 'idle' || currentState === 'waiting_for_confirmation') { + this._cancelVoiceProgress(sessionId); + } if (currentState === 'thinking') { this._clearLastNarratedText(sessionKey); // A new turn supersedes any completed reply that was waiting to be @@ -4920,9 +5312,9 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // prose, which is what it got before. const question = this._questionNarratable(this._modelForSession(sessionId)); if (question) { - this._narrate(sessionId, question.kind, question.text, undefined, question.pending); + this._narrate(sessionId, question.kind, question.text, undefined, undefined, undefined, question.pending); } else { - this._narrate(sessionId, 'confirmation', detail); + this._narrate(sessionId, 'confirmation', detail, undefined, undefined, confirmationType); } } } @@ -4966,7 +5358,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // Same pendingId test as the per-session path: two forms asking the same // things have identical detail, so only the id distinguishes them. const detailOnly = !stateChanged && change.currentState === 'waiting_for_confirmation' - && (change.fromDetail !== detail || change.fromPendingId !== change.pendingId); + && (change.fromDetail !== detail || change.fromPendingId !== change.pendingId || change.fromConfirmationType !== change.confirmationType); // A summary that appeared/changed while the session stayed idle is a // real narratable change even though the coarse state didn't move. const responseSummaryOnly = !stateChanged && change.currentState === 'idle' && !!summary && change.fromResponseSummary !== summary; @@ -4995,7 +5387,6 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } } this._sendContext(); - this.logService.trace(`[voice] emitting ${netChanges.length} settled stateChange(s): ${netChanges.map(({ change, detailOnly }) => `${change.label}:${change.currentState}${detailOnly ? ' (detail-only)' : ''}`).join(', ')}`); this.voiceClientService.flushSessionContext(); // Speak the settled item for the shown session; a background session's item // waits until the user focuses it. Both this coalesced path and the direct @@ -5003,8 +5394,9 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // surfaced only by the latter are covered too. const shownNow = this._shownSessionId(); for (const { change } of netChanges) { - this._handleNarratableStateChange(change.sessionId, change.currentState, change.detail, change.lastResponseSummary, shownNow); + this._handleNarratableStateChange(change.sessionId, change.currentState, change.detail, change.lastResponseSummary, shownNow, change.confirmationType); } + this.logService.trace(`[voice] emitting ${netChanges.length} settled stateChange(s): ${netChanges.map(({ change, detailOnly }) => `${change.label}:${change.currentState}${detailOnly ? ' (detail-only)' : ''}`).join(', ')}`); for (const { change } of netChanges) { // Persist as a coding_event in the local timeline so // "session X went from thinking → waiting_for_confirmation" @@ -5079,7 +5471,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } const sessions = this.agentSessionsService.model.sessions.filter(s => !s.isArchived()); - const stateChanges: { sessionId: string; currentState: string; label: string; detail?: string; lastResponseSummary?: string }[] = []; + const stateChanges: { sessionId: string; currentState: string; label: string; detail?: string; confirmationType?: VoiceConfirmationType; lastResponseSummary?: string }[] = []; const processedResources = new Set<string>(); const waitingSessionIds = new Set<string>(); @@ -5089,6 +5481,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const model = this.chatService.getSession(s.resource); let currentState: string; let detail: string | undefined; + let confirmationType: VoiceConfirmationType | undefined; let lastResponseSummary: string | undefined; if (model) { const info = this._getAgentStateInfo(model); @@ -5097,6 +5490,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // resident with a proper summary, so drop the pending idle deferral. currentState = this._effectiveResidentState(sessionId, info); detail = info.detail; + confirmationType = info.confirmation_type; lastResponseSummary = currentState === info.state ? info.last_response_summary : undefined; // Capture the summary while resident so a later completion after // disposal can still narrate. @@ -5120,7 +5514,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const prev = this._prevSessionStates.get(sessionId); const isStateChange = prev !== undefined && prev.state !== currentState && currentState !== 'unknown'; const pendingId = currentState === 'waiting_for_confirmation' ? this._pendingIdFor(sessionId) : ''; - const isDetailChange = !isStateChange && prev !== undefined && currentState === 'waiting_for_confirmation' && ((detail ?? '') !== prev.detail || pendingId !== prev.pendingId); + const isDetailChange = !isStateChange && prev !== undefined && currentState === 'waiting_for_confirmation' + && ((detail ?? '') !== prev.detail || pendingId !== prev.pendingId || confirmationType !== prev.confirmationType); // Arm the awaiting-summary marker on a genuine new turn so this run's // completion is later recognized as new (see autorun for rationale). @@ -5163,14 +5558,14 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC if (isDetailChange) { this.voiceClientService.invalidateSessionCache(sessionId); } - stateChanges.push({ sessionId, currentState, label: s.label || 'Untitled session', detail, lastResponseSummary }); + stateChanges.push({ sessionId, currentState, label: s.label || 'Untitled session', detail, confirmationType, lastResponseSummary }); } } if (currentState !== 'unknown') { // Preserve a known summary rather than clobbering with '' so a // model unload→reload can't manufacture a fresh-reply transition. const rememberedSummary = normalizedSummary || this._lastResponseSummaryById.get(sessionId) || prev?.lastResponseSummary || ''; - this._prevSessionStates.set(sessionId, { state: currentState, detail: detail ?? '', pendingId, lastResponseSummary: rememberedSummary }); + this._prevSessionStates.set(sessionId, { state: currentState, detail: detail ?? '', pendingId, confirmationType, lastResponseSummary: rememberedSummary }); } if (currentState === 'waiting_for_confirmation') { waitingSessionIds.add(sessionId); @@ -5186,12 +5581,14 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const info = this._getAgentStateInfo(chatModel); const currentState = info.state; const detail = info.detail; + const confirmationType = info.confirmation_type; const lastResponseSummary = info.last_response_summary; const prev = this._prevSessionStates.get(key); const isStateChange = prev !== undefined && prev.state !== currentState && currentState !== 'unknown'; const pendingId = currentState === 'waiting_for_confirmation' ? this._pendingIdFor(key) : ''; - const isDetailChange = !isStateChange && prev !== undefined && currentState === 'waiting_for_confirmation' && ((detail ?? '') !== prev.detail || pendingId !== prev.pendingId); + const isDetailChange = !isStateChange && prev !== undefined && currentState === 'waiting_for_confirmation' + && ((detail ?? '') !== prev.detail || pendingId !== prev.pendingId || confirmationType !== prev.confirmationType); // Arm the awaiting-summary marker on a genuine new turn. if (isStateChange && currentState === 'thinking' && !this._eagerModelLoading.has(key)) { @@ -5210,11 +5607,11 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC if (isDetailChange) { this.voiceClientService.invalidateSessionCache(key); } - stateChanges.push({ sessionId: key, currentState, label: chatModel.title || 'Chat', detail, lastResponseSummary }); + stateChanges.push({ sessionId: key, currentState, label: chatModel.title || 'Chat', detail, confirmationType, lastResponseSummary }); } if (currentState !== 'unknown') { const rememberedSummary = normalizedSummary || this._lastResponseSummaryById.get(key) || prev?.lastResponseSummary || ''; - this._prevSessionStates.set(key, { state: currentState, detail: detail ?? '', pendingId, lastResponseSummary: rememberedSummary }); + this._prevSessionStates.set(key, { state: currentState, detail: detail ?? '', pendingId, confirmationType, lastResponseSummary: rememberedSummary }); } if (currentState === 'waiting_for_confirmation') { waitingSessionIds.add(key); @@ -5238,7 +5635,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // are spoken on focus. const shownNow = this._shownSessionId(); for (const change of stateChanges) { - this._handleNarratableStateChange(change.sessionId, change.currentState, change.detail, change.lastResponseSummary, shownNow); + this._handleNarratableStateChange(change.sessionId, change.currentState, change.detail, change.lastResponseSummary, shownNow, change.confirmationType); } if (stateChanges.length > 0) { @@ -5365,6 +5762,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC is_active: isActive, agent_state: scoped.state, ...(!scoped.hideConfirmationDetail && stateInfo.detail ? { agent_state_detail: stateInfo.detail } : {}), + ...(!scoped.hideConfirmationDetail && stateInfo.confirmation_type ? { confirmation_type: stateInfo.confirmation_type } : {}), ...(shipSummary ? { last_response_summary: shipSummary } : {}), ...(pending ? { pending } : {}), }; @@ -5392,6 +5790,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC is_active: isActive, agent_state: scoped.state, ...(!scoped.hideConfirmationDetail && stateInfo.detail ? { agent_state_detail: stateInfo.detail } : {}), + ...(!scoped.hideConfirmationDetail && stateInfo.confirmation_type ? { confirmation_type: stateInfo.confirmation_type } : {}), ...(stateInfo.last_response_summary ? { last_response_summary: stateInfo.last_response_summary } : {}), ...(pending ? { pending } : {}), }); @@ -5541,83 +5940,352 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC return stateInfo.state; } - /** - * Returns the oldest still-open pending part of the last request. - * - * Answer routing and narration prose MUST both come from here, or they can - * name different forms and a spoken answer lands on the wrong one. - */ - private _selectPendingPart(model: IChatModel | undefined | null): { requestId: string; part: IChatProgressResponseContent } | undefined { + private _visibleConfirmationText(value: string | IMarkdownString | undefined, maxLength = VoiceSessionController._MAX_CONFIRMATION_FIELD_CHARS): string { + if (!value) { + return ''; + } + const plainText = renderAsPlaintext(typeof value === 'string' ? { value } : value, { useLinkFormatter: true }).replace(/\s+/g, ' ').trim(); + if (plainText.length <= maxLength) { + return plainText; + } + const prefix = plainText.slice(0, maxLength - 3); + const wordBoundary = prefix.lastIndexOf(' '); + const truncated = wordBoundary > Math.floor(maxLength * 0.6) ? prefix.slice(0, wordBoundary) : prefix; + return localize('voice.confirmation.truncated', "{0}...", truncated); + } + + private _boundedConfirmationLines(lines: readonly string[], fallback: string): string { + const result: string[] = []; + for (const line of lines.filter(Boolean)) { + const candidate = [...result, line].join('\n'); + if (candidate.length > VoiceSessionController._MAX_CONFIRMATION_NARRATION_CHARS) { + break; + } + result.push(line); + } + return result.join('\n') || fallback; + } + + private _visibleQuestionnaireFromCarousel(carousel: IChatQuestionCarousel, includeDetails: boolean): IVisibleVoiceQuestionnaire { + return { + context: carousel.message, + questions: carousel.questions.map(question => ({ + prompt: question.message ?? (question.title !== question.id ? question.title : undefined), + details: includeDetails ? question.description ?? question.detailedMessage : undefined, + options: (question.options ?? []).map(option => option.label), + allowFreeformInput: question.allowFreeformInput !== false, + })), + }; + } + + private _visibleQuestionnaireFromToolInvocation(toolInvocation: IChatToolInvocation): IVisibleVoiceQuestionnaire | undefined { + if (!isPendingVoiceQuestionnaireInvocation(toolInvocation)) { + return undefined; + } + const state = toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation && state.type !== IChatToolInvocation.StateKind.WaitingForPostApproval) { + return undefined; + } + const parameters = state.parameters; + if (!isObject(parameters) || !hasOwn(parameters, 'questions') || !Array.isArray(parameters.questions) || parameters.questions.length === 0) { + return undefined; + } + + return { + questions: parameters.questions.map(rawQuestion => { + if (!isObject(rawQuestion)) { + return { options: [], allowFreeformInput: true }; + } + const prompt = hasOwn(rawQuestion, 'question') && typeof rawQuestion.question === 'string' + ? rawQuestion.question + : undefined; + const options: string[] = []; + if (hasOwn(rawQuestion, 'options') && Array.isArray(rawQuestion.options)) { + for (const rawOption of rawQuestion.options) { + if (!isObject(rawOption) || !hasOwn(rawOption, 'label') || typeof rawOption.label !== 'string') { + continue; + } + const description = hasOwn(rawOption, 'description') && typeof rawOption.description === 'string' + ? rawOption.description + : undefined; + options.push(description ? `${rawOption.label} - ${description}` : rawOption.label); + } + } + const allowFreeformInput = !(hasOwn(rawQuestion, 'allowFreeformInput') && rawQuestion.allowFreeformInput === false); + return { prompt, options, allowFreeformInput }; + }), + }; + } + + private _formatQuestionnaireNarration(questionnaire: IVisibleVoiceQuestionnaire): string | undefined { + const fallback = localize('voice.questionnaire.fallback', "I need your input in the open questionnaire."); + if (questionnaire.questions.length === 0) { + return undefined; + } + + const lines = [ + questionnaire.questions.length === 1 + ? localize('voice.questionnaire.single', "questionnaire: 1 question") + : localize('voice.questionnaire.multiple', "questionnaire: {0} questions", questionnaire.questions.length), + ]; + const context = this._visibleConfirmationText(questionnaire.context, 220); + if (context) { + lines.push(localize('voice.questionnaire.context', "context: {0}", context)); + } + + let includedQuestions = 0; + const questionLimit = Math.min(questionnaire.questions.length, VoiceSessionController._MAX_QUESTIONNAIRE_QUESTIONS); + for (let index = 0; index < questionLimit; index++) { + const question = questionnaire.questions[index]; + const prompt = this._visibleConfirmationText(question.prompt); + const questionLines = [ + localize('voice.questionnaire.question', "{0}. {1}", index + 1, prompt || fallback), + ]; + const description = this._visibleConfirmationText(question.details, 180); + if (description && description !== prompt) { + questionLines.push(localize('voice.questionnaire.description', "details: {0}", description)); + } + + const visibleOptions = question.options + .map(option => this._visibleConfirmationText(option, 160)) + .filter(Boolean); + if (visibleOptions.length > 0) { + const includedOptions = visibleOptions.slice(0, VoiceSessionController._MAX_QUESTIONNAIRE_OPTIONS); + const omittedOptions = visibleOptions.length - includedOptions.length; + let optionsText = includedOptions.join('; '); + if (omittedOptions > 0) { + optionsText = localize('voice.questionnaire.moreOptions', "{0}; {1} more options", optionsText, omittedOptions); + } + if (question.allowFreeformInput) { + optionsText = localize('voice.questionnaire.customOption', "{0}; a custom response is also available", optionsText); + } + questionLines.push(localize('voice.questionnaire.options', "options: {0}", optionsText)); + } else { + questionLines.push(localize('voice.questionnaire.freeform', "response: enter a free-form answer in GitHub Copilot")); + } + + const remainingAfterCandidate = questionnaire.questions.length - (includedQuestions + 1); + const reservedSuffix = remainingAfterCandidate > 0 + ? remainingAfterCandidate === 1 + ? localize('voice.questionnaire.oneOmitted', "1 more question is open in GitHub Copilot.") + : localize('voice.questionnaire.manyOmitted', "{0} more questions are open in GitHub Copilot.", remainingAfterCandidate) + : localize('voice.questionnaire.open', "The questionnaire is open in GitHub Copilot."); + const candidate = [...lines, ...questionLines, reservedSuffix].join('\n'); + if (candidate.length > VoiceSessionController._MAX_CONFIRMATION_NARRATION_CHARS) { + break; + } + lines.push(...questionLines); + includedQuestions++; + } + + const omittedQuestions = questionnaire.questions.length - includedQuestions; + if (omittedQuestions > 0) { + lines.push(omittedQuestions === 1 + ? localize('voice.questionnaire.oneOmitted', "1 more question is open in GitHub Copilot.") + : localize('voice.questionnaire.manyOmitted', "{0} more questions are open in GitHub Copilot.", omittedQuestions)); + } else { + lines.push(localize('voice.questionnaire.open', "The questionnaire is open in GitHub Copilot.")); + } + return lines.join('\n') || fallback; + } + + private _formatChoiceLabels(choices: readonly { label: string; description?: string }[]): string | undefined { + const visibleChoices = choices.map(choice => { + const label = this._visibleConfirmationText(choice.label, 160); + const description = this._visibleConfirmationText(choice.description, 160); + return description ? localize('voice.confirmation.choiceDescription', "{0} - {1}", label, description) : label; + }).filter(Boolean); + if (visibleChoices.length === 0) { + return undefined; + } + const includedChoices = visibleChoices.slice(0, VoiceSessionController._MAX_QUESTIONNAIRE_OPTIONS); + const omittedChoices = visibleChoices.length - includedChoices.length; + const text = includedChoices.join('; '); + return omittedChoices > 0 + ? localize('voice.confirmation.moreChoices', "{0}; {1} more choices", text, omittedChoices) + : text; + } + + private _formatPlanNarration(plan: IChatPlanReview): string { + const fallback = localize('voice.plan.fallback', "A plan is open in GitHub Copilot and needs your approval."); + const title = this._visibleConfirmationText(plan.title) || fallback; + const lines = [localize('voice.plan.title', "plan approval: {0}", title)]; + const choices = this._formatChoiceLabels(plan.actions); + if (choices) { + lines.push(localize('voice.plan.choices', "choices: {0}", choices)); + } + lines.push(localize('voice.plan.open', "The plan is open in GitHub Copilot.")); + return this._boundedConfirmationLines(lines, fallback); + } + + private _formatElicitationNarration(elicitation: IChatElicitationRequest): string { + const fallback = localize('voice.elicitation.fallback', "GitHub Copilot needs your input in the open request."); + const title = this._visibleConfirmationText(elicitation.title); + const message = this._visibleConfirmationText(elicitation.message); + const subtitle = this._visibleConfirmationText(elicitation.subtitle); + const lines = [localize('voice.elicitation.title', "input request: {0}", title || message || fallback)]; + if (subtitle && subtitle !== title) { + lines.push(subtitle); + } + if (message && message !== title) { + lines.push(message); + } + const choices = this._formatChoiceLabels([ + { label: elicitation.acceptButtonLabel }, + ...(elicitation.rejectButtonLabel ? [{ label: elicitation.rejectButtonLabel }] : []), + ...(elicitation.moreActions ?? []).map(action => ({ label: action.label })), + ]); + if (choices) { + lines.push(localize('voice.elicitation.choices', "choices: {0}", choices)); + } + return this._boundedConfirmationLines(lines, fallback); + } + + private _formatConfirmationNarration(confirmation: IChatConfirmation): string { + const fallback = localize('voice.confirmation.fallback', "GitHub Copilot needs your approval to continue."); + const title = this._visibleConfirmationText(confirmation.title); + const message = this._visibleConfirmationText(confirmation.message); + const lines = [localize('voice.confirmation.title', "confirmation: {0}", title || message || fallback)]; + if (message && message !== title) { + lines.push(message); + } + const choices = this._formatChoiceLabels((confirmation.buttons ?? []).map(label => ({ label }))); + if (choices) { + lines.push(localize('voice.confirmation.choices', "choices: {0}", choices)); + } + return this._boundedConfirmationLines(lines, fallback); + } + + private _formatToolNarration(toolInvocation: IChatToolInvocation): string { + const fallback = localize('voice.toolConfirmation.fallback', "GitHub Copilot needs your approval to continue."); + const state = toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation && state.type !== IChatToolInvocation.StateKind.WaitingForPostApproval) { + return fallback; + } + const messages = state.confirmationMessages; + const title = this._visibleConfirmationText(messages?.title) || this._visibleConfirmationText(toolInvocation.invocationMessage); + const message = this._visibleConfirmationText(messages?.message); + const lines = [localize('voice.toolConfirmation.title', "tool approval: {0}", title || message || fallback)]; + if (message && message !== title) { + lines.push(message); + } + return this._boundedConfirmationLines(lines, fallback); + } + + private _formatToolNarrationFallback(): string { + const fallback = localize('voice.toolConfirmation.fallback', "GitHub Copilot needs your approval to continue."); + return localize('voice.toolConfirmation.title', "tool approval: {0}", fallback); + } + + private _formatToolAuthenticationNarration(toolInvocation: IChatToolInvocation): string | undefined { + const state = toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForAuthentication) { + return undefined; + } + const serverName = this._visibleConfirmationText(state.server.name); + const fallback = localize('voice.authentication.fallback', "GitHub Copilot needs authentication to continue."); + return this._boundedConfirmationLines([ + localize('voice.authentication.title', "authentication request: MCP authentication required"), + serverName + ? localize('voice.authentication.message', "The MCP server {0} requires authentication to continue this tool call.", serverName) + : fallback, + localize('voice.authentication.choices', "choices: Authenticate; Cancel"), + ], fallback); + } + + private _selectPendingPart(model: IChatModel | undefined | null): { requestId: string; type: VoiceConfirmationType; part: IChatProgressResponseContent } | undefined { const lastRequest = model?.getRequests().at(-1); const parts = lastRequest?.response?.response.value; if (!lastRequest || !parts) { return undefined; } - for (const part of parts) { - if (this._isOpenPendingPart(part)) { - return { requestId: lastRequest.id, part }; + + for (let index = 0; index < parts.length; index++) { + const part = parts[index]; + const type = getVoiceConfirmationType([part]); + if (type && this._isOpenPendingPart(part)) { + if (type === 'questionnaire' && isVoiceQuestionnaireInvocation(part)) { + const carousel = parts.slice(index + 1).find(candidate => + candidate.kind === 'questionCarousel' + && candidate.resolveId === part.toolCallId + && this._isOpenPendingPart(candidate)); + if (carousel) { + return { requestId: lastRequest.id, type, part: carousel }; + } + } + return { requestId: lastRequest.id, type, part }; } } return undefined; } - /** Whether a response part is still waiting on the user. */ private _isOpenPendingPart(part: IChatProgressResponseContent): boolean { if (part.kind === 'questionCarousel') { - const carousel = part as IChatQuestionCarousel; - // A form with no questions can't be answered by voice or by mouse, so - // it must not hold the queue. - return !carousel.isUsed && !carousel.answeredExternally && carousel.questions.length > 0; - } - if (part.kind === 'planReview' || part.kind === 'confirmation') { - return !(part as { isUsed?: boolean }).isUsed; + return !part.isUsed && !part.answeredExternally; } if (part.kind === 'elicitation2') { - return (part as { state: IObservable<string> }).state.get() === 'pending'; + return part.state.get() === 'pending'; + } + if (part.kind === 'planReview' || part.kind === 'confirmation') { + return !part.isUsed; } if (part.kind === 'toolInvocation') { - return (part as IChatToolInvocation).state.get().type === IChatToolInvocation.StateKind.WaitingForConfirmation; + const state = part.state.get(); + return state.type === IChatToolInvocation.StateKind.WaitingForConfirmation + || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval + || state.type === IChatToolInvocation.StateKind.WaitingForAuthentication; } return false; } - /** Prose for the selected pending part, for `agent_state_detail`. */ - private _describePendingPart(part: IChatProgressResponseContent, fallbackDetail: string | undefined): string { - if (part.kind === 'questionCarousel') { - const carousel = part as IChatQuestionCarousel; - const titles = carousel.questions.map(question => question.title).filter(Boolean); - if (titles.length > 0) { - return `questions: ${titles.join(', ')}`; + private _getPendingConfirmationInfo(model: IChatModel): { type: VoiceConfirmationType; detail?: string } | undefined { + const lastResponse = model.getRequests().at(-1)?.response; + if (!lastResponse) { + return undefined; + } + + const parts = lastResponse.response.value; + const selected = this._selectPendingPart(model); + if (!selected) { + return undefined; + } + const { type, part } = selected; + + const askQuestionsCallIds = new Set(parts + .filter(isVoiceQuestionnaireInvocation) + .map(part => part.toolCallId)); + if (type === 'questionnaire' && part?.kind === 'questionCarousel') { + const includeDetails = !part.resolveId || !askQuestionsCallIds.has(part.resolveId); + return { type, detail: this._formatQuestionnaireNarration(this._visibleQuestionnaireFromCarousel(part, includeDetails)) }; + } + if (type === 'questionnaire' && part?.kind === 'toolInvocation') { + const questionnaire = this._visibleQuestionnaireFromToolInvocation(part); + if (questionnaire) { + return { type, detail: this._formatQuestionnaireNarration(questionnaire) }; } - return this._plainText(carousel.message) || 'asking clarifying questions'; } - if (part.kind === 'planReview') { - return 'review the plan to continue'; + if (type === 'elicitation' && part?.kind === 'elicitation2') { + return { type, detail: this._formatElicitationNarration(part) }; } - if (part.kind === 'elicitation2') { - return this._plainText((part as { title?: string | IMarkdownString }).title) || 'needs input'; + if (type === 'plan' && part?.kind === 'planReview') { + return { type, detail: this._formatPlanNarration(part) }; } - if (part.kind === 'confirmation') { - return (part as { title?: string }).title ?? 'needs approval'; + if (type === 'tool' && part?.kind === 'toolInvocation') { + return { type, detail: this._formatToolNarration(part) }; } - if (part.kind === 'toolInvocation') { - const state = (part as IChatToolInvocation).state.get(); - if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { - return ''; - } - const params = state.parameters as Record<string, unknown> | undefined; - const command = params?.['command'] ?? params?.['input']; - const explanation = params?.['explanation'] ?? params?.['goal']; - if (typeof command !== 'string' || !command) { - return fallbackDetail ?? ''; - } - const reason = typeof explanation === 'string' && explanation ? `\nreason: ${explanation}` : ''; - return `command: ${command}${reason}`; + if (type === 'generic' && part?.kind === 'confirmation') { + return { type, detail: this._formatConfirmationNarration(part) }; } - return ''; + if (type === 'generic' && part?.kind === 'toolInvocation') { + return { type, detail: this._formatToolAuthenticationNarration(part) }; + } + if (type === 'questionnaire') { + return { type }; + } + return { type, detail: this._formatToolNarrationFallback() }; } - private _getAgentStateInfo(model: IChatModel | undefined | null): { state: string; detail?: string; last_response_summary?: string } { + private _getAgentStateInfo(model: IChatModel | undefined | null): IVoiceAgentStateInfo { if (!model) { return { state: 'unknown' }; } @@ -5630,53 +6298,15 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } const pendingConfirmation = lastRequest?.response?.isPendingConfirmation.get(); - if (pendingConfirmation) { - // Same part `_buildPendingPayload` publishes, so the prose the model - // hears and the form an answer routes to can never name different - // forms. See `_selectPendingPart`. - const selected = this._selectPendingPart(model); - const confirmDetail = selected ? this._describePendingPart(selected.part, pendingConfirmation.detail) : ''; + const confirmation = this._getPendingConfirmationInfo(model); + if (pendingConfirmation || confirmation) { return { state: 'waiting_for_confirmation', - detail: confirmDetail || pendingConfirmation.detail || '', + ...(confirmation?.detail ? { detail: confirmation.detail } : !confirmation ? { detail: this._formatToolNarrationFallback() } : {}), + confirmation_type: confirmation?.type ?? 'generic', }; } - // Fallback: some tools (e.g. askQuestions) enter WaitingForConfirmation - // without setting confirmationMessages, so isPendingConfirmation is - // undefined. Scan response parts directly to catch these. - if (lastRequest?.response) { - let fallbackDetail: string | undefined; - for (const part of lastRequest.response.response.value) { - if (part.kind === 'toolInvocation') { - const state = part.state.get(); - if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { - const params = state.parameters as Record<string, unknown> | undefined; - const questions = params?.['questions']; - let detail = ''; - if (Array.isArray(questions) && questions.length > 0) { - const headers = questions - .map((q: Record<string, unknown>) => q['header'] || q['question']) - .filter(Boolean) - .join(', '); - detail = headers ? `questions: ${headers}` : 'asking clarifying questions'; - } - if (!detail) { - const invMsg = (part as { invocationMessage?: string | { value: string } }).invocationMessage; - detail = invMsg ? (typeof invMsg === 'string' ? invMsg : invMsg.value) : 'needs input'; - } - fallbackDetail = detail; - } - } - } - if (fallbackDetail !== undefined) { - return { - state: 'waiting_for_confirmation', - detail: fallbackDetail, - }; - } - } - const incomplete = lastRequest?.response?.isIncomplete.get() ?? false; if (incomplete) { return { state: 'thinking' }; @@ -5697,25 +6327,21 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC * `questions: <titles>`, losing the options, their values and the ids. This * returns what the backend needs to route an answer back to the exact part. * - * The part is chosen by `_selectPendingPart`, shared with - * `_getAgentStateInfo` so the routable payload and the spoken detail can - * never name different forms. Only carousels and tool confirmations have a - * typed shape; anything else the selector lands on publishes nothing, and the - * session simply has no voice-answerable pending until it is resolved. + * Uses the same typed pending selection as narration, so the backend never + * receives an id for a different action than the one the user heard. */ private _buildPendingPayload(model: IChatModel | undefined | null): IVoiceSessionPending | undefined { const selected = this._selectPendingPart(model); - if (!selected) { + if (!selected || (selected.type !== 'questionnaire' && selected.type !== 'plan' && selected.type !== 'tool')) { return undefined; } - const { requestId, part } = selected; - // Minted lazily: an id is issued only once the part is confirmed to be - // a live pending request, so a part the backend can never answer never - // gets an identity that a stale id could collide with. + const { requestId, type, part } = selected; const routing = () => ({ pending_id: derivePendingId(requestId, part), request_id: requestId }); - - if (part.kind === 'questionCarousel') { + if (type === 'questionnaire' && part.kind === 'questionCarousel') { const carousel = part as IChatQuestionCarousel; + if (carousel.answeredExternally || carousel.questions.length === 0) { + return undefined; + } return { type: 'questions', ...routing(), @@ -5724,13 +6350,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC questions: carousel.questions.map((question): IVoicePendingQuestion => ({ id: question.id, type: question.type, - // The same text the widget shows, so voice reads the question - // rather than its header. title: this._plainText(getDisplayedQuestionText(question)), allow_freeform: question.allowFreeformInput !== false, - // The ordinal the user hears has to be the one they see, so the - // list is in the same order the widget renders, and both sides - // number it by position. options: getOptionsWithDefaultsFirst(question).map(({ option }) => ({ label: option.label, value: option.value, @@ -5738,14 +6359,11 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC })), }; } - - if (part.kind === 'toolInvocation') { - const message = this._plainText((part as { invocationMessage?: string | IMarkdownString }).invocationMessage); - return { - type: 'approval', - ...routing(), - ...(message ? { message } : {}), - }; + if (type === 'plan' && part.kind === 'planReview') { + return { type: 'approval', ...routing(), message: this._formatPlanNarration(part) }; + } + if (type === 'tool' && part.kind === 'toolInvocation') { + return { type: 'approval', ...routing(), message: this._formatToolNarration(part) }; } return undefined; @@ -5832,14 +6450,19 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC if (!this._autoApprovedSessions.has(s.resource.toString())) { continue; } const model = this.chatService.getSession(s.resource); if (!model) { continue; } - for (const req of model.getRequests()) { - const pending = req.response?.isPendingConfirmation.get(); - if (pending && req.response) { - for (const part of req.response.response.value) { - if (part.kind === 'toolInvocation') { - IChatToolInvocation.confirmWith(part as IChatToolInvocation, { type: ToolConfirmKind.UserAction }); - } - } + this._autoApprovePendingTools(model); + } + } + + private _autoApprovePendingTools(model: IChatModel): void { + for (const request of model.getRequests()) { + const response = request.response; + if (!response?.isPendingConfirmation.get() || getVoiceConfirmationType(response.response.value) !== 'tool') { + continue; + } + for (const part of response.response.value) { + if (part.kind === 'toolInvocation') { + IChatToolInvocation.confirmWith(part, { type: ToolConfirmKind.UserAction }); } } } diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceTelemetry.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceTelemetry.ts index 96c7cc99076..b8438ff1556 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceTelemetry.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceTelemetry.ts @@ -124,7 +124,7 @@ export type VoiceNarrationDeferredEvent = { export type VoiceNarrationDeferredClassification = { owner: 'meganrogge'; comment: 'Fired client-side when a requested narration cannot play now and is queued for a later retry (no narration text is logged).'; - kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the deferred narration was a response or a confirmation prompt.' }; + kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the deferred narration was a response, confirmation prompt, or checkpoint.' }; reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Why it was deferred: busy (narration_ack busy) or interrupted (narration_interrupted).' }; }; @@ -135,7 +135,6 @@ export type VoiceNarrationDroppedEvent = { export type VoiceNarrationDroppedClassification = { owner: 'meganrogge'; comment: 'Fired client-side when a requested narration is dropped without being played (no narration text is logged).'; - kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the dropped narration was a response or a confirmation prompt.' }; + kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the dropped narration was a response, confirmation prompt, or checkpoint.' }; reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Why it was dropped: invalid (narration_ack invalid), stale (no longer the current narratable item), or session_changed (user switched away from the session).' }; }; - diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceToolDispatchService.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceToolDispatchService.ts index 9cb92b7b246..70262a1f0d6 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceToolDispatchService.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceToolDispatchService.ts @@ -10,13 +10,15 @@ import { createDecorator } from '../../../../../platform/instantiation/common/in import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; import { AgentSessionStatus, getAgentChangesSummary } from '../agentSessions/agentSessionsModel.js'; -import { IChatQuestionAnswers, IChatQuestionCarousel, IChatSendRequestOptions, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js'; +import { IChatPlanReviewResult, IChatQuestionAnswers, IChatQuestionCarousel, IChatSendRequestOptions, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js'; import { IBackendQuestionAnswer, resolveQuestionAnswers } from '../../common/voiceClient/voiceQuestionAnswers.js'; import { ChatQuestionCarouselData } from '../../common/model/chatProgressTypes/chatQuestionCarouselData.js'; +import { ChatPlanReviewData } from '../../common/model/chatProgressTypes/chatPlanReviewData.js'; import { IChatModel } from '../../common/model/chatModel.js'; import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; import { IVoiceDispatchResult, IVoiceToolCall, peekPendingId } from '../../common/voiceClient/voiceClientService.js'; +import { getVoiceConfirmationType } from '../../common/voiceClient/voiceConfirmation.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; /** @@ -304,7 +306,14 @@ export class VoiceToolDispatchService implements IVoiceToolDispatchService { } const approve = responseType === 'approve'; + if (part.kind === 'planReview' && part instanceof ChatPlanReviewData) { + return this._resolvePlanReview(part, approve) ? { ok: true } : { ok: false, reason: 'stale_pending' }; + } + if (part.kind === 'toolInvocation') { + if (getVoiceConfirmationType([part]) !== 'tool') { + return { ok: false, reason: 'unsupported' }; + } const confirmed = IChatToolInvocation.confirmWith( part as IChatToolInvocation, approve ? { type: ToolConfirmKind.UserAction } : { type: ToolConfirmKind.Denied }, @@ -315,6 +324,30 @@ export class VoiceToolDispatchService implements IVoiceToolDispatchService { return { ok: false, reason: 'unsupported' }; } + private _resolvePlanReview(plan: ChatPlanReviewData, approve: boolean): boolean { + if (plan.isUsed) { + return false; + } + let result: IChatPlanReviewResult; + if (approve) { + const action = plan.actions.find(candidate => candidate.default) ?? plan.actions[0]; + if (!action) { + return false; + } + result = { + action: action.label, + actionId: action.id, + rejected: false, + }; + } else { + result = { rejected: true }; + } + plan.data = result; + plan.isUsed = true; + void plan.completion.complete(result); + return true; + } + /** Resolve a coding session id to its chat model, never falling back to the focused session. */ private async _resolveModelForResponse(codingSessionId: string): Promise<{ model: IChatModel; dispose(): void } | undefined> { if (!codingSessionId) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index a36a3143a4f..ec1acb89685 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -2920,6 +2920,7 @@ export class ChatWidget extends Disposable implements IChatWidget { attachedContext: requestInputs.attachedContext.asArray(), resolvedVariables: resolvedImageVariables, noCommandDetection: options?.noCommandDetection, + isVoiceModeInput: options?.isVoiceModeInput, ...this.getModeRequestOptions(), modeInfo, agentIdSilent: this._lockedAgent?.id, diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 266bb95c10b..30ba76d0ecc 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -80,6 +80,7 @@ import { computeVoiceGlowStyle, isGlowingVoiceState, readVoiceGlowIntensity, Voi import { combineVoiceInput } from '../../voiceClient/voiceInputUtils.js'; import { IAgentTitleBarStatusService } from '../../agentSessions/experiments/agentTitleBarStatusService.js'; import { IVoicePlaybackService } from '../../../common/voicePlaybackService.js'; +import { VOICE_AGENT_PROGRESS_SETTING } from '../../../common/voiceClient/voiceClientService.js'; import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; interface IChatViewPaneState extends Partial<IChatModelInputState> { @@ -423,9 +424,13 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { widget.input.setValue(text, false); } else { // Preserve any text the user already typed in the input. - widget.acceptInput(combineVoiceInput(widget.getInput(), text), { preserveFocus: true }); + return widget.acceptInput(combineVoiceInput(widget.getInput(), text), { + preserveFocus: true, + isVoiceModeInput: this.configurationService.getValue<boolean>(VOICE_AGENT_PROGRESS_SETTING) === true, + }); } } + return undefined; })); this._voiceBarDisposables.add(CommandsRegistry.registerCommand('_chat.voice.switchToSession', async (_accessor, resourceStr: string): Promise<boolean> => { if (!resourceStr) { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index f80feea2b3a..19346eb60cc 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -602,6 +602,14 @@ export interface IChatHookPart { subAgentInvocationId?: string; } +export type ChatVoiceProgressStage = 'investigating' | 'planning' | 'editing' | 'validating' | 'recovering'; + +export interface IChatVoiceProgressPart { + readonly kind: 'voiceProgress'; + readonly id: ChatVoiceProgressStage; + readonly value: string; +} + export interface IChatTerminalToolInvocationData { kind: 'terminal'; commandLine: { @@ -1469,6 +1477,7 @@ export type IChatProgress = | IChatPullRequestContent | IChatUndoStop | IChatThinkingPart + | IChatVoiceProgressPart | IChatTaskSerialized | IChatElicitationRequest | IChatElicitationRequestSerialized @@ -1808,6 +1817,7 @@ export interface IRemotePendingRequest { export interface IChatSendRequestOptions { modeInfo?: IChatRequestModeInfo; + isVoiceModeInput?: boolean; userSelectedModelId?: string; /** * The configuration (e.g. context size, thinking effort) for the selected diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index b8a4be4c375..51637dde035 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -1611,6 +1611,7 @@ export class ChatService extends Disposable implements IChatService { editedFileEvents: thisRequest.editedFileEvents, hooks: collectedHooks, hasHooksEnabled: !!collectedHooks && Object.values(collectedHooks).some(arr => arr.length > 0), + isVoiceModeInput: options?.isVoiceModeInput, isSystemInitiated: options?.isSystemInitiated, workingDirectory: model.workingDirectory, }; diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 5ea9db6d3a8..93c53dd8378 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -31,7 +31,7 @@ import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCo import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImplicitVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../attachments/chatVariableEntries.js'; import { migrateLegacyTerminalToolSpecificData } from '../chat.js'; import { ChatPerfMark, markChat } from '../chatPerf.js'; -import { ChatAgentVoteDirection, ChatRequestQueueKind, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatAutoModeResolutionPart, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatDisabledClaudeHooksPart, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExternalEdit, IChatExternalToolInvocationUpdate, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatInfoMessage, IChatLocationData, IChatMarkdownContent, IChatMcpAuthenticationRequired, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatMcpServersStartingSlow, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatPlanReview, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatSessionTiming, IChatSystemNotificationPart, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsagePromptTokenDetail, IChatUsedContext, IChatWarningMessage, IChatWorkspaceEdit, ResponseModelState, ToolConfirmKind, isIUsedContext } from '../chatService/chatService.js'; +import { ChatAgentVoteDirection, ChatRequestQueueKind, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatAutoModeResolutionPart, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatDisabledClaudeHooksPart, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExternalEdit, IChatExternalToolInvocationUpdate, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatInfoMessage, IChatLocationData, IChatMarkdownContent, IChatMcpAuthenticationRequired, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatMcpServersStartingSlow, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatPlanReview, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatSessionTiming, IChatSystemNotificationPart, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsagePromptTokenDetail, IChatUsedContext, IChatVoiceProgressPart, IChatWarningMessage, IChatWorkspaceEdit, ResponseModelState, ToolConfirmKind, isIUsedContext } from '../chatService/chatService.js'; import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ChatToolInvocation } from './chatProgressTypes/chatToolInvocation.js'; import { ChatPlanReviewData } from './chatProgressTypes/chatPlanReviewData.js'; @@ -72,6 +72,7 @@ export interface ISerializableSendOptions { locationData?: IChatLocationData; attempt?: number; noCommandDetection?: boolean; + isVoiceModeInput?: boolean; agentId?: string; agentIdSilent?: string; slashCommand?: string; @@ -228,7 +229,8 @@ export type IChatProgressResponseContent = | IChatMcpServersStartingSerialized | IChatMcpAuthenticationRequired | IChatMcpServersStartingSlow - | IChatDisabledClaudeHooksPart; + | IChatDisabledClaudeHooksPart + | IChatVoiceProgressPart; export type IChatProgressResponseContentSerialized = Exclude<IChatProgressResponseContent, | IChatToolInvocation @@ -239,9 +241,10 @@ export type IChatProgressResponseContentSerialized = Exclude<IChatProgressRespon | IChatMcpAuthenticationRequired | IChatMcpServersStartingSlow | IChatDisabledClaudeHooksPart + | IChatVoiceProgressPart >; -const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop']); +const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop', 'voiceProgress']); function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent { return !nonHistoryKinds.has(content.kind); } @@ -250,7 +253,7 @@ export function toChatHistoryContent(content: ReadonlyArray<IChatProgressRespons return content.filter(isChatProgressHistoryResponseContent); } -export type IChatProgressRenderableResponseContent = Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart>; +export type IChatProgressRenderableResponseContent = Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart | IChatVoiceProgressPart>; export interface IResponse { readonly value: ReadonlyArray<IChatProgressResponseContent>; @@ -628,6 +631,7 @@ class AbstractResponse implements IResponse { case 'elicitationSerialized': case 'thinking': case 'hook': + case 'voiceProgress': case 'multiDiffData': case 'mcpServersStarting': case 'mcpAuthenticationRequired': @@ -3168,7 +3172,7 @@ export class ChatModel extends Disposable implements IChatModel { message, variableData: IChatRequestVariableData.toExport(r.variableData), response: r.response ? - r.response.entireResponse.value.map(item => { + r.response.entireResponse.value.filter(item => item.kind !== 'voiceProgress').map(item => { // Keeping the shape of the persisted data the same for back compat if (item.kind === 'treeData') { return item.treeData; @@ -3293,6 +3297,7 @@ export function serializeSendOptions(options: IChatSendRequestOptions): ISeriali locationData: options.locationData, attempt: options.attempt, noCommandDetection: options.noCommandDetection, + isVoiceModeInput: options.isVoiceModeInput, agentId: options.agentId, agentIdSilent: options.agentIdSilent, slashCommand: options.slashCommand, diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts index dd91d5d428d..951ca1bde83 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts @@ -10,7 +10,7 @@ import { isEqual as _urisEqual } from '../../../../../base/common/resources.js'; import { hasKey } from '../../../../../base/common/types.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; -import { IChatMarkdownContent, IChatMcpAuthenticationRequired, IChatMcpServersStartingSlow, ResponseModelState } from '../chatService/chatService.js'; +import { IChatMarkdownContent, IChatMcpAuthenticationRequired, IChatMcpServersStartingSlow, IChatVoiceProgressPart, ResponseModelState } from '../chatService/chatService.js'; import { ModifiedFileEntryState } from '../editing/chatEditingService.js'; import { IParsedChatRequest } from '../requestParser/chatParserTypes.js'; import { IChatAgentEditedFileEvent, IChatDataSerializerLog, IChatModel, IChatPendingRequest, IChatProgressResponseContent, IChatRequestModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatModelInputState, ISerializableChatRequestData, ISerializablePendingRequestData, SerializedChatResponsePart, serializeSendOptions } from './chatModel.js'; @@ -38,7 +38,9 @@ const toJson = <T>(obj: T): T extends { toJSON?(): infer R } ? R : T => { return (cast && typeof cast.toJSON === 'function' ? cast.toJSON() : obj) as any; }; -const responsePartSchema = Adapt.v<Exclude<IChatProgressResponseContent, IChatMcpAuthenticationRequired | IChatMcpServersStartingSlow>, SerializedChatResponsePart>( +type PersistedResponsePart = Exclude<IChatProgressResponseContent, IChatMcpAuthenticationRequired | IChatMcpServersStartingSlow | IChatVoiceProgressPart>; + +const responsePartSchema = Adapt.v<PersistedResponsePart, SerializedChatResponsePart>( (obj): SerializedChatResponsePart => obj.kind === 'markdownContent' ? obj.content : toJson(obj), (a, b) => { if (isMarkdownString(a) && isMarkdownString(b)) { @@ -139,7 +141,7 @@ const requestSchema = Adapt.object<IChatRequestModel, ISerializableChatRequestDa isHidden: Adapt.v(() => undefined), // deprecated, always undefined for new data isCanceled: Adapt.v(() => undefined), // deprecated, modelState is used instead - response: Adapt.t(m => m.response?.entireResponse.value.filter((p): p is Exclude<IChatProgressResponseContent, IChatMcpAuthenticationRequired | IChatMcpServersStartingSlow> => p.kind !== 'mcpAuthenticationRequired' && p.kind !== 'mcpServersStartingSlow'), Adapt.array(responsePartSchema)), + response: Adapt.t(m => m.response?.entireResponse.value.filter((p): p is PersistedResponsePart => p.kind !== 'mcpAuthenticationRequired' && p.kind !== 'mcpServersStartingSlow' && p.kind !== 'voiceProgress'), Adapt.array(responsePartSchema)), responseId: Adapt.v(m => m.response?.id), responseTimestamp: Adapt.v(m => m.response?.timestamp), result: Adapt.v(m => m.response?.result, objectsEqual), diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index 106ff7624c2..577a39487b8 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -175,6 +175,10 @@ export interface IChatAgentRequest { * Whether any hooks are enabled for this request. */ hasHooksEnabled?: boolean; + /** + * Whether this request was submitted through Agents Voice Mode. + */ + isVoiceModeInput?: boolean; /** * The permission level for tool auto-approval in this request. * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. diff --git a/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts b/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts index db6d2dc4267..a3bac20bf13 100644 --- a/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts +++ b/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts @@ -5,6 +5,7 @@ import { Event } from '../../../../../base/common/event.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import type { ChatVoiceProgressStage } from '../chatService/chatService.js'; /** * One selectable option on a pending question, positioned in *displayed* order. @@ -102,12 +103,26 @@ export interface IVoiceSessionContext { is_active: boolean; agent_state: string; agent_state_detail?: string; + confirmation_type?: VoiceConfirmationType; last_response_summary?: string; pending?: IVoiceSessionPending; }[]; display_locale: string; } +export type VoiceConfirmationType = 'questionnaire' | 'elicitation' | 'plan' | 'tool' | 'generic'; +export type VoiceCheckpointId = ChatVoiceProgressStage; + +export function isVoiceCheckpointId(value: unknown): value is VoiceCheckpointId { + return value === 'investigating' || value === 'planning' || value === 'editing' || value === 'validating' || value === 'recovering'; +} + +export interface IVoiceCheckpointNarrationMetadata { + readonly requestId: string; + readonly checkpointId: VoiceCheckpointId; + readonly sequence: number; +} + /** * What a client-requested narration is speaking. Mirrors `NarrationKind` in the * voice backend. @@ -116,7 +131,7 @@ export interface IVoiceSessionContext { * by the narration model: the numbered options are the ordinals the user says * back, so a summary that drops them breaks answering. */ -export type VoiceNarrationKind = 'response' | 'confirmation' | 'question'; +export type VoiceNarrationKind = 'response' | 'confirmation' | 'question' | 'checkpoint'; /** * Structured outcome of a dispatched voice tool call. The backend speaks an @@ -158,6 +173,11 @@ export interface IVoiceAudioResponse { * direct replies and for backends that don't yet echo it (legacy fallback). */ readonly responseId?: string; + readonly requestId?: string; + readonly checkpointId?: VoiceCheckpointId; + readonly sequence?: number; + readonly narrationKind?: VoiceNarrationKind; + readonly playbackId?: string; } export interface IVoiceBargeIn { @@ -166,7 +186,7 @@ export interface IVoiceBargeIn { } /** Disposition of a client `request_narration`, reported by `narration_ack`. */ -export type IVoiceNarrationDisposition = 'accepted' | 'busy' | 'invalid'; +export type IVoiceNarrationDisposition = 'accepted' | 'busy' | 'invalid' | 'suppressed'; /** The backend's acknowledgement of a `request_narration`. */ export interface IVoiceNarrationAck { @@ -186,6 +206,8 @@ export interface IVoiceNarrationAck { export interface IVoiceNarrationSignal { readonly narrationId: string; readonly codingSessionId: string; + readonly retryable?: boolean; + readonly reason?: string; } export interface IVoiceToolCall { @@ -342,6 +364,8 @@ export interface IVoiceClientService { */ invalidateSessionCache(sessionId: string): void; sendToolResult(callId: string, result: string | IVoiceDispatchResult): void; + /** Report that one correlated checkpoint playback attempt finished locally. */ + sendNarrationPlaybackComplete(codingSessionId: string, narrationId: string, playbackId: string): void; /** * Ask the backend to speak `text` for a session now; returns the narration id * echoed on the resulting `audio_response`, or `undefined` if nothing was @@ -356,7 +380,7 @@ export interface IVoiceClientService { * backend's mirror has caught up. The id is deliberately *not* folded into * `text`, which every dedup and retry-reuse guard keys on. */ - requestNarration(codingSessionId: string, kind: VoiceNarrationKind, text: string, narrationId?: string, pending?: { pendingId: string }): string | undefined; + requestNarration(codingSessionId: string, kind: VoiceNarrationKind, text: string, narrationId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata, confirmationType?: VoiceConfirmationType, pending?: { pendingId: string }): string | undefined; /** * Notify the backend of a session state transition. * @@ -412,3 +436,4 @@ export interface IVoiceClientService { } export const IVoiceClientService = createDecorator<IVoiceClientService>('voiceClientService'); +export const VOICE_AGENT_PROGRESS_SETTING = 'agents.voice.agentProgress'; diff --git a/src/vs/workbench/contrib/chat/common/voiceClient/voiceConfirmation.ts b/src/vs/workbench/contrib/chat/common/voiceClient/voiceConfirmation.ts new file mode 100644 index 00000000000..2c50744cc14 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/voiceClient/voiceConfirmation.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ElicitationState, IChatToolInvocation } from '../chatService/chatService.js'; +import type { IChatProgressResponseContent } from '../model/chatModel.js'; +import { AskQuestionsToolId } from '../tools/builtinTools/askQuestionsTool.js'; +import type { VoiceConfirmationType } from './voiceClientService.js'; + +export function isVoiceQuestionnaireInvocation(part: IChatProgressResponseContent): part is IChatToolInvocation { + return part.kind === 'toolInvocation' && part.toolId === AskQuestionsToolId; +} + +export function isPendingVoiceQuestionnaireInvocation(part: IChatProgressResponseContent): part is IChatToolInvocation { + if (!isVoiceQuestionnaireInvocation(part)) { + return false; + } + const state = part.state.get(); + return state.type === IChatToolInvocation.StateKind.WaitingForConfirmation + || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval; +} + +export function getVoiceConfirmationType(parts: readonly IChatProgressResponseContent[]): VoiceConfirmationType | undefined { + for (let index = parts.length - 1; index >= 0; index--) { + const part = parts[index]; + if (part.kind === 'questionCarousel' && !part.isUsed) { + return 'questionnaire'; + } + if (part.kind === 'elicitation2' && part.state.get() === ElicitationState.Pending) { + return 'elicitation'; + } + if (isPendingVoiceQuestionnaireInvocation(part)) { + return 'questionnaire'; + } + } + + for (let index = parts.length - 1; index >= 0; index--) { + const part = parts[index]; + if (part.kind === 'planReview' && !part.isUsed) { + return 'plan'; + } + if (part.kind === 'toolInvocation') { + const state = part.state.get(); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + return 'tool'; + } + if (state.type === IChatToolInvocation.StateKind.WaitingForAuthentication) { + return 'generic'; + } + } + if (part.kind === 'confirmation' && !part.isUsed) { + return 'generic'; + } + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/chat/common/widget/annotations.ts b/src/vs/workbench/contrib/chat/common/widget/annotations.ts index eb013e2b766..e27515a0330 100644 --- a/src/vs/workbench/contrib/chat/common/widget/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/widget/annotations.ts @@ -96,6 +96,8 @@ export function annotateSpecialMarkdownContent(response: Iterable<IChatProgressR result.splice(previousItemIndex, 1); result.push({ ...previousItem, content: merged }); } + } else if (item.kind === 'voiceProgress') { + continue; } else { result.push(item); } diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatAccessibilityHelp.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatAccessibilityHelp.test.ts index 5e85ca37c92..ff82ba4e572 100644 --- a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatAccessibilityHelp.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatAccessibilityHelp.test.ts @@ -73,4 +73,18 @@ suite('Chat Accessibility Help', () => { byDefault: false, }); }); + + test('only describes spoken agent progress in agent mode', () => { + const keybindingService = { + lookupKeybindings: () => [], + } as unknown as IKeybindingService; + + assert.deepStrictEqual({ + agentView: getAccessibilityHelpText('agentView', keybindingService, true).includes('brief progress updates'), + panelChat: getAccessibilityHelpText('panelChat', keybindingService, true).includes('brief progress updates'), + }, { + agentView: true, + panelChat: false, + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts index b30425357bd..af298ee2365 100644 --- a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts @@ -12,7 +12,7 @@ import { NullLogService } from '../../../../../../platform/log/common/log.js'; import product from '../../../../../../platform/product/common/product.js'; import { IProductService } from '../../../../../../platform/product/common/productService.js'; import { VoiceClientService } from '../../../browser/voiceClient/voiceClientService.js'; -import { IVoiceAudioResponse, IVoiceBargeIn, IVoiceTranscription } from '../../../common/voiceClient/voiceClientService.js'; +import { IVoiceAudioResponse, IVoiceBargeIn, IVoiceNarrationAck, IVoiceNarrationSignal, IVoiceTranscription } from '../../../common/voiceClient/voiceClientService.js'; class TestWebSocket { static instance: TestWebSocket | undefined; @@ -125,6 +125,30 @@ suite('VoiceClientService', () => { }]); }); + test('preserves checkpoint interruption metadata from the backend', async () => { + const { service } = createService(); + const events: IVoiceNarrationSignal[] = []; + store.add(service.onNarrationInterrupted(event => events.push(event))); + + await service.connect(createTestWindow()); + socket().onmessage?.(new mainWindow.MessageEvent('message', { + data: JSON.stringify({ + type: 'narration_interrupted', + narration_id: 'checkpoint-narration', + coding_session_id: 'chat-session:/one', + retryable: false, + reason: 'superseded_by_response', + }), + })); + + assert.deepStrictEqual(events, [{ + narrationId: 'checkpoint-narration', + codingSessionId: 'chat-session:/one', + retryable: false, + reason: 'superseded_by_response', + }]); + }); + test('preserves the backend turn ID when audio has a narration ID', async () => { const { service } = createService(); const events: IVoiceAudioResponse[] = []; @@ -143,6 +167,11 @@ suite('VoiceClientService', () => { is_final: false, turn_id: 'backend-turn', narration_id: 'client-narration', + request_id: 'request-1', + checkpoint_id: 'planning', + sequence: 1, + narration_kind: 'checkpoint', + playback_id: 'playback-1', }), })); @@ -154,6 +183,11 @@ suite('VoiceClientService', () => { transcript: undefined, turnId: 'backend-turn', responseId: 'client-narration', + requestId: 'request-1', + checkpointId: 'planning', + sequence: 1, + narrationKind: 'checkpoint', + playbackId: 'playback-1', }]); }); @@ -249,6 +283,191 @@ suite('VoiceClientService', () => { ]); }); + test('sends first-class checkpoint narration metadata', async () => { + const { service } = createService(); + await service.connect(createTestWindow()); + service.sendStartSession({ sessions: [], display_locale: '' }, 'machine'); + + const narrationId = service.requestNarration('chat-session:/one', 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 2, + }); + service.sendNarrationPlaybackComplete('chat-session:/one', narrationId!, 'playback-1'); + + assert.deepStrictEqual(socket().sent.slice(1), [ + { + type: 'request_narration', + coding_session_id: 'chat-session:/one', + kind: 'checkpoint', + text: 'Updating the code.', + narration_id: narrationId, + request_id: 'request-1', + checkpoint_id: 'editing', + sequence: 2, + }, + { + type: 'narration_playback_complete', + coding_session_id: 'chat-session:/one', + narration_id: narrationId, + playback_id: 'playback-1', + }, + ]); + }); + + test('sends typed confirmation narration metadata', async () => { + const { service } = createService(); + await service.connect(createTestWindow()); + service.sendStartSession({ sessions: [], display_locale: '' }, 'machine'); + + const narrationId = service.requestNarration( + 'chat-session:/one', + 'confirmation', + 'questionnaire: 1 question', + undefined, + undefined, + 'questionnaire', + ); + + assert.deepStrictEqual(socket().sent[1], { + type: 'request_narration', + coding_session_id: 'chat-session:/one', + kind: 'confirmation', + text: 'questionnaire: 1 question', + narration_id: narrationId, + confirmation_type: 'questionnaire', + }); + }); + + test('persists and clears typed confirmation session state', async () => { + const { service } = createService(); + await service.connect(createTestWindow()); + socket().onopen?.(); + service.sendStartSession({ sessions: [], display_locale: '' }, 'machine'); + + service.sendSessionContext({ + sessions: [{ + id: 'chat-session:/one', + is_active: true, + agent_state: 'waiting_for_confirmation', + agent_state_detail: 'questionnaire: 1 question', + confirmation_type: 'questionnaire', + }], + display_locale: 'en-US', + }); + service.flushSessionContext(); + service.sendSessionContext({ + sessions: [{ + id: 'chat-session:/one', + is_active: true, + agent_state: 'idle', + }], + display_locale: 'en-US', + }); + service.flushSessionContext(); + + assert.deepStrictEqual(socket().sent.slice(1), [ + { + type: 'session_context', + mode: 'delta', + upserts: [{ + id: 'chat-session:/one', + is_active: true, + agent_state: 'waiting_for_confirmation', + agent_state_detail: 'questionnaire: 1 question', + confirmation_type: 'questionnaire', + }], + removes: [], + }, + { + type: 'session_context', + mode: 'delta', + upserts: [{ + id: 'chat-session:/one', + agent_state: 'idle', + agent_state_detail: null, + confirmation_type: null, + }], + removes: [], + }, + ]); + }); + + test('invalidated context preserves pending deletion tombstones', async () => { + const { service } = createService(); + await service.connect(createTestWindow()); + socket().onopen?.(); + service.sendStartSession({ sessions: [], display_locale: '' }, 'machine'); + const sessionId = 'chat-session:/one'; + + service.sendSessionContext({ + sessions: [{ + id: sessionId, + is_active: true, + agent_state: 'waiting_for_confirmation', + agent_state_detail: 'Which region?', + confirmation_type: 'questionnaire', + pending: { + type: 'questions', + pending_id: 'request-1#p1', + request_id: 'request-1', + questions: [], + }, + }], + display_locale: 'en-US', + }); + service.flushSessionContext(); + service.invalidateSessionCache(sessionId); + service.sendSessionContext({ + sessions: [{ + id: sessionId, + is_active: true, + agent_state: 'waiting_for_confirmation', + agent_state_detail: 'Which region?', + confirmation_type: 'questionnaire', + }], + display_locale: 'en-US', + }); + service.flushSessionContext(); + + assert.deepStrictEqual(socket().sent.at(-1), { + type: 'session_context', + mode: 'delta', + upserts: [{ + id: sessionId, + is_active: true, + agent_state: 'waiting_for_confirmation', + agent_state_detail: 'Which region?', + confirmation_type: 'questionnaire', + pending: null, + }], + removes: [], + }); + }); + + test('normalizes legacy suppressed narration acknowledgements', async () => { + const { service } = createService(); + const events: IVoiceNarrationAck[] = []; + store.add(service.onNarrationAck(event => events.push(event))); + await service.connect(createTestWindow()); + + socket().onmessage?.(new mainWindow.MessageEvent('message', { + data: JSON.stringify({ + type: 'narration_ack', + narration_id: 'narration-1', + coding_session_id: 'chat-session:/one', + disposition: 'suppressed', + reason: 'stale', + }), + })); + assert.deepStrictEqual(events, [{ + narrationId: 'narration-1', + codingSessionId: 'chat-session:/one', + disposition: 'suppressed', + reason: 'stale', + }]); + }); + test('flags a passive ptt_start for hands-free barge-in listens', async () => { const { service } = createService(); @@ -269,7 +488,7 @@ suite('VoiceClientService', () => { await service.connect(createTestWindow()); service.sendStartSession({ sessions: [], display_locale: '' }, 'machine'); - const questionId = service.requestNarration('cs1', 'question', 'Which region?', undefined, { pendingId: 'p1' }); + const questionId = service.requestNarration('cs1', 'question', 'Which region?', undefined, undefined, undefined, { pendingId: 'p1' }); const replyId = service.requestNarration('cs1', 'response', 'Done.'); assert.deepStrictEqual(socket().sent.filter(message => message.type === 'request_narration'), [ @@ -282,7 +501,7 @@ suite('VoiceClientService', () => { const { service } = createService(); await service.connect(createTestWindow()); - const narrationId = service.requestNarration('cs1', 'question', 'Which region?', undefined, { pendingId: 'p1' }); + const narrationId = service.requestNarration('cs1', 'question', 'Which region?', undefined, undefined, undefined, { pendingId: 'p1' }); assert.strictEqual(narrationId, undefined); assert.deepStrictEqual(socket().sent.filter(message => message.type === 'request_narration'), []); @@ -301,10 +520,12 @@ suite('VoiceClientService', () => { type: message.type, session_context: message.session_context, voice: message.voice, + auto_narrate: message.auto_narrate, })), [{ type: 'start_session', session_context: { sessions: [], display_locale: 'fr-FR' }, voice: 'kevin_neutral', + auto_narrate: false, }]); }); @@ -455,6 +676,7 @@ suite('VoiceClientService', () => { session_context: message.session_context, voice: message.voice, voice_instructions: message.voice_instructions, + auto_narrate: message.auto_narrate, })), }, { disconnectedMessages: [], @@ -464,6 +686,7 @@ suite('VoiceClientService', () => { session_context: { sessions: [], display_locale: 'de-DE' }, voice: 'daniel_neutral', voice_instructions: 'Keep replies concise.', + auto_narrate: false, }], }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts index 6589bdf7ac8..ab7ead30d4e 100644 --- a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts @@ -8,6 +8,7 @@ import sinon from 'sinon'; import { mainWindow } from '../../../../../../base/browser/window.js'; import { DeferredPromise } from '../../../../../../base/common/async.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { observableValue } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; import { mock } from '../../../../../../base/test/common/mock.js'; @@ -32,16 +33,20 @@ import { IMicCaptureService } from '../../../browser/voiceClient/micCaptureServi import { ITtsPlaybackService } from '../../../browser/voiceClient/ttsPlaybackService.js'; import { IVoiceSessionController, VoiceSessionController } from '../../../browser/voiceClient/voiceSessionController.js'; import { IVoiceToolDispatchService } from '../../../browser/voiceClient/voiceToolDispatchService.js'; -import { IChatService, IChatToolInvocation } from '../../../common/chatService/chatService.js'; +import { ChatSendResult, ElicitationState, IChatConfirmation, IChatSendRequestOptions, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../../common/chatService/chatService.js'; import { IPromptsService } from '../../../common/promptSyntax/service/promptsService.js'; -import { derivePendingId, IVoiceAudioResponse, IVoiceBargeIn, IVoiceClientService, IVoiceNarrationSignal, IVoiceSpeechStarted, IVoiceToolCall, IVoiceTranscription, VoiceNarrationKind, IVoiceDispatchResult } from '../../../common/voiceClient/voiceClientService.js'; -import { IChatModel } from '../../../common/model/chatModel.js'; +import { derivePendingId, IVoiceAudioResponse, IVoiceBargeIn, IVoiceCheckpointNarrationMetadata, IVoiceClientService, IVoiceDispatchResult, IVoiceNarrationAck, IVoiceNarrationSignal, IVoiceSessionContext, IVoiceSpeechStarted, IVoiceToolCall, IVoiceTranscription, peekPendingId, VoiceConfirmationType, VoiceNarrationKind, VOICE_AGENT_PROGRESS_SETTING } from '../../../common/voiceClient/voiceClientService.js'; +import { IChatModel, IChatProgressResponseContent, IChatResponseModel } from '../../../common/model/chatModel.js'; +import { ChatElicitationRequestPart } from '../../../common/model/chatProgressTypes/chatElicitationRequestPart.js'; +import { ChatPlanReviewData } from '../../../common/model/chatProgressTypes/chatPlanReviewData.js'; +import { ChatQuestionCarouselData } from '../../../common/model/chatProgressTypes/chatQuestionCarouselData.js'; import { IVoicePlaybackService } from '../../../common/voicePlaybackService.js'; +import { AskQuestionsToolId } from '../../../common/tools/builtinTools/askQuestionsTool.js'; import { MockChatService } from '../../common/chatService/mockChatService.js'; class TestVoiceClientService extends mock<IVoiceClientService>() { private narrationCounter = 0; - readonly requests: { sessionId: string; kind: VoiceNarrationKind; text: string; narrationId: string; pendingId?: string }[] = []; + readonly requests: { sessionId: string; kind: VoiceNarrationKind; text: string; narrationId: string; pendingId?: string; checkpoint?: IVoiceCheckpointNarrationMetadata; confirmationType?: VoiceConfirmationType }[] = []; private readonly audioResponseEmitter = new Emitter<IVoiceAudioResponse>(); override readonly onAudioResponse = this.audioResponseEmitter.event; private readonly bargeInEmitter = new Emitter<IVoiceBargeIn>(); @@ -52,12 +57,14 @@ class TestVoiceClientService extends mock<IVoiceClientService>() { override readonly onToolCall = this.toolCallEmitter.event; private readonly speechStartedEmitter = new Emitter<IVoiceSpeechStarted>(); override readonly onSpeechStarted = this.speechStartedEmitter.event; - override readonly onNarrationAck = Event.None; + private readonly narrationAckEmitter = new Emitter<IVoiceNarrationAck>(); + override readonly onNarrationAck = this.narrationAckEmitter.event; private readonly narrationUnblockedEmitter = new Emitter<IVoiceNarrationSignal>(); override readonly onNarrationUnblocked = this.narrationUnblockedEmitter.event; private readonly narrationInterruptedEmitter = new Emitter<IVoiceNarrationSignal>(); override readonly onNarrationInterrupted = this.narrationInterruptedEmitter.event; - override readonly onSessionInit = Event.None; + private readonly sessionInitEmitter = new Emitter<{ sessionId: string }>(); + override readonly onSessionInit = this.sessionInitEmitter.event; override readonly onError = Event.None; private readonly connectionStateEmitter = new Emitter<boolean>(); override readonly onDidChangeConnectionState = this.connectionStateEmitter.event; @@ -68,19 +75,34 @@ class TestVoiceClientService extends mock<IVoiceClientService>() { override get isConnected(): boolean { return this.connected; } override disconnect(): void { this.connected = false; } override async connect(): Promise<void> { } - override sendSessionContext(): void { } - override flushSessionContext(): void { } - readonly toolResults: { callId: string; result: string }[] = []; + readonly wireEvents: ({ type: 'session_context'; context: IVoiceSessionContext } | { type: 'request_narration'; kind: VoiceNarrationKind; text: string; confirmationType?: VoiceConfirmationType })[] = []; + private pendingContext: IVoiceSessionContext | undefined; + override sendSessionContext(context: IVoiceSessionContext): void { + this.pendingContext = context; + } + override flushSessionContext(): void { + if (this.pendingContext) { + this.wireEvents.push({ type: 'session_context', context: this.pendingContext }); + this.pendingContext = undefined; + } + } + override invalidateSessionCache(): void { } + readonly playbackCompletions: { sessionId: string; narrationId: string; playbackId: string }[] = []; + override sendNarrationPlaybackComplete(codingSessionId: string, narrationId: string, playbackId: string): void { + this.playbackCompletions.push({ sessionId: codingSessionId, narrationId, playbackId }); + } + readonly toolResults: { callId: string; result: string | IVoiceDispatchResult }[] = []; private toolResultResolver: (() => void) | undefined; readonly toolResultReceived = new Promise<void>(resolve => this.toolResultResolver = resolve); - override sendToolResult(callId: string, result: string): void { + override sendToolResult(callId: string, result: string | IVoiceDispatchResult): void { this.toolResults.push({ callId, result }); this.toolResultResolver?.(); } - override requestNarration(codingSessionId: string, kind: VoiceNarrationKind, text: string, narrationId?: string, pending?: { pendingId: string }): string | undefined { + override requestNarration(codingSessionId: string, kind: VoiceNarrationKind, text: string, narrationId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata, confirmationType?: VoiceConfirmationType, pending?: { pendingId: string }): string | undefined { const id = narrationId ?? `narration-${++this.narrationCounter}`; - this.requests.push({ sessionId: codingSessionId, kind, text, narrationId: id, ...(pending ? { pendingId: pending.pendingId } : {}) }); + this.requests.push({ sessionId: codingSessionId, kind, text, narrationId: id, ...(pending ? { pendingId: pending.pendingId } : {}), ...(checkpoint ? { checkpoint } : {}), ...(confirmationType ? { confirmationType } : {}) }); + this.wireEvents.push({ type: 'request_narration', kind, text, ...(confirmationType ? { confirmationType } : {}) }); return id; } @@ -108,6 +130,10 @@ class TestVoiceClientService extends mock<IVoiceClientService>() { this.narrationInterruptedEmitter.fire(event); } + fireNarrationAck(event: IVoiceNarrationAck): void { + this.narrationAckEmitter.fire(event); + } + fireNarrationUnblocked(event: IVoiceNarrationSignal): void { this.narrationUnblockedEmitter.fire(event); } @@ -117,15 +143,21 @@ class TestVoiceClientService extends mock<IVoiceClientService>() { this.connectionStateEmitter.fire(connected); } + fireSessionInit(): void { + this.sessionInitEmitter.fire({ sessionId: 'voice-session' }); + } + dispose(): void { this.audioResponseEmitter.dispose(); this.bargeInEmitter.dispose(); this.transcriptionEmitter.dispose(); this.toolCallEmitter.dispose(); this.speechStartedEmitter.dispose(); + this.narrationAckEmitter.dispose(); this.narrationUnblockedEmitter.dispose(); this.narrationInterruptedEmitter.dispose(); this.connectionStateEmitter.dispose(); + this.sessionInitEmitter.dispose(); } } @@ -186,6 +218,19 @@ class TestTtsPlaybackService extends mock<ITtsPlaybackService>() { } } +class DeferredFirstTtsPlaybackService extends TestTtsPlaybackService { + private deferNextStart = true; + + override playAudioChunk(audio: string): void { + if (audio && this.deferNextStart) { + this.deferNextStart = false; + this.playedAudio.push(audio); + return; + } + super.playAudioChunk(audio); + } +} + class TestMicCaptureService extends mock<IMicCaptureService>() { override readonly onPttStart = Event.None; override readonly onPttAudioChunk = Event.None; @@ -239,7 +284,13 @@ function agentSessionEntry(id: string, label: string | undefined, status: AgentS class TestChatService extends mock<IChatService>() { override readonly chatModels = observableValue('chatModels', []); + readonly sendRequestOptions: (IChatSendRequestOptions | undefined)[] = []; override getSession(): undefined { return undefined; } + override async sendRequest(_sessionResource: URI, _message: string, options?: IChatSendRequestOptions): Promise<ChatSendResult> { + this.sendRequestOptions.push(options); + return { kind: 'rejected', reason: 'test' }; + } + /** A session that never loads: the controller eagerly loads models for waiting sessions. */ override async acquireOrLoadSession(): Promise<undefined> { return undefined; } } @@ -297,6 +348,21 @@ function pendingConfirmationModel(resource: URI): IChatModel { } as unknown as IChatModel; } +function pendingResponsePartModel(resource: URI, part: IChatProgressResponseContent, detail = 'Needs approval', reportPending = true): IChatModel { + const response = { + isPendingConfirmation: observableValue<{ detail?: string } | undefined>('pending', reportPending ? { detail } : undefined), + isIncomplete: observableValue('incomplete', false), + response: { value: [part], getMarkdown: () => '' }, + }; + const lastRequest = { response }; + return { + sessionResource: resource, + title: 'Chat', + getRequests: () => [lastRequest], + lastRequestObs: observableValue('lastRequest', lastRequest), + } as unknown as IChatModel; +} + function completedResponseModel(markdown: string, errorMessage?: string, isCanceled = false): IChatModel { const response = { isPendingConfirmation: observableValue('pending', undefined), @@ -362,7 +428,7 @@ suite('VoiceSessionController', () => { commandService: ICommandService = new TestCommandService(), telemetryService: NullTelemetryServiceShape = NullTelemetryService, micCaptureService: IMicCaptureService = new TestMicCaptureService(), - configurationService: IConfigurationService = new TestConfigurationService({ 'agents.voice.handsFree': false }), + configurationService: IConfigurationService = new TestConfigurationService({ 'agents.voice.handsFree': false, [VOICE_AGENT_PROGRESS_SETTING]: true }), chatService: IChatService = new TestChatService(), promptsService: IPromptsService = new class extends mock<IPromptsService>() { override async getVoiceInstructions(): Promise<undefined> { return undefined; } @@ -406,6 +472,20 @@ suite('VoiceSessionController', () => { )); } + function createVoiceProgressResponse(id: string, requestId = `request-${id}`) { + const changeEmitter = store.add(new Emitter<{ reason: 'other' }>()); + const parts: { kind: 'voiceProgress'; id: string; value: string }[] = []; + const state = { + id, + requestId, + isComplete: false, + isCanceled: false, + onDidChange: changeEmitter.event, + response: { value: parts }, + }; + return { changeEmitter, parts, response: state as unknown as IChatResponseModel, state }; + } + test('includes response errors in the summary sent to the voice backend', () => { const controller = createController(new TestVoiceClientService()); const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { state: string; last_response_summary?: string }; @@ -469,6 +549,2416 @@ suite('VoiceSessionController', () => { }); }); + test('narrates visible questionnaire prompts and choices immediately without internal ids', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionResource = URI.parse('chat-session:/mars-questionnaire'); + const carousel = new ChatQuestionCarouselData([ + { + id: 'mars_feature_scope', + type: 'singleSelect', + title: 'mars_feature_scope', + message: new MarkdownString('Which Mars features should the experience include?'), + description: 'Choose the main exploration scope.', + options: [ + { id: 'surface_only', label: 'Surface explorer - Drive between landmarks', value: 'surface_only' }, + { id: 'science_missions', label: 'Science missions - Collect samples and run experiments', value: 'science_missions' }, + ], + }, + { + id: 'mars_navigation_mode', + type: 'singleSelect', + title: 'mars_navigation_mode', + message: 'How should people navigate Mars?', + options: [ + { id: 'guided', label: 'Guided route', value: 'guided' }, + { id: 'free_roam', label: 'Free roam', value: 'free_roam' }, + ], + }, + { + id: 'mars_data_approach', + type: 'multiSelect', + title: 'mars_data_approach', + message: 'Which Mars data should be available?', + options: [ + { id: 'terrain', label: 'Terrain maps', value: 'terrain' }, + { id: 'weather', label: 'Weather readings', value: 'weather' }, + ], + }, + { + id: 'mars_rendering_style', + type: 'singleSelect', + title: 'mars_rendering_style', + message: 'What visual style should Mars use?', + options: [ + { id: 'realistic', label: 'Photorealistic', value: 'realistic' }, + { id: 'illustrated', label: 'Illustrated', value: 'illustrated' }, + ], + allowFreeformInput: true, + }, + ], true, 'mars_internal_resolve_id', undefined, false, new MarkdownString('Help shape the Mars experience.')); + const model = pendingResponsePartModel(sessionResource, carousel, 'questions: mars_feature_scope, mars_navigation_mode, mars_data_approach, mars_rendering_style'); + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { state: string; detail?: string; confirmation_type?: VoiceConfirmationType }; + const handleStateChange = Reflect.get(controller, '_handleNarratableStateChange') as (sessionId: string, state: string, detail: string | undefined, summary: string | undefined, shown: string, confirmationType?: VoiceConfirmationType) => void; + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + const progress = createVoiceProgressResponse('mars-progress'); + + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, progress.response); + progress.parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating the Mars experience.' }); + progress.changeEmitter.fire({ reason: 'other' }); + const stateInfo = getAgentStateInfo.call(controller, model); + handleStateChange.call(controller, sessionResource.toString(), stateInfo.state, stateInfo.detail, undefined, sessionResource.toString(), stateInfo.confirmation_type); + const immediateRequestCount = voiceClientService.requests.length; + clock.tick(5_000); + + assert.deepStrictEqual({ + stateInfo, + immediateRequestCount, + request: voiceClientService.requests.map(request => ({ kind: request.kind, text: request.text, confirmationType: request.confirmationType })), + containsInternalIds: ['mars_feature_scope', 'mars_navigation_mode', 'mars_data_approach', 'mars_rendering_style', 'surface_only', 'free_roam'] + .some(id => stateInfo.detail?.includes(id)), + }, { + stateInfo: { + state: 'waiting_for_confirmation', + confirmation_type: 'questionnaire', + detail: [ + 'questionnaire: 4 questions', + 'context: Help shape the Mars experience.', + '1. Which Mars features should the experience include?', + 'details: Choose the main exploration scope.', + 'options: Surface explorer - Drive between landmarks; Science missions - Collect samples and run experiments; a custom response is also available', + '2. How should people navigate Mars?', + 'options: Guided route; Free roam; a custom response is also available', + '3. Which Mars data should be available?', + 'options: Terrain maps; Weather readings; a custom response is also available', + '4. What visual style should Mars use?', + 'options: Photorealistic; Illustrated; a custom response is also available', + 'The questionnaire is open in GitHub Copilot.', + ].join('\n'), + }, + immediateRequestCount: 1, + request: [{ + kind: 'confirmation', + confirmationType: 'questionnaire', + text: [ + 'questionnaire: 4 questions', + 'context: Help shape the Mars experience.', + '1. Which Mars features should the experience include?', + 'details: Choose the main exploration scope.', + 'options: Surface explorer - Drive between landmarks; Science missions - Collect samples and run experiments; a custom response is also available', + '2. How should people navigate Mars?', + 'options: Guided route; Free roam; a custom response is also available', + '3. Which Mars data should be available?', + 'options: Terrain maps; Weather readings; a custom response is also available', + '4. What visual style should Mars use?', + 'options: Photorealistic; Illustrated; a custom response is also available', + 'The questionnaire is open in GitHub Copilot.', + ].join('\n'), + }], + containsInternalIds: false, + }); + }); + + test('extracts visible runtime askQuestions data before carousel persistence', () => { + const voiceClientService = new TestVoiceClientService(); + const chatService = new ControllableChatService(); + const controller = createController(voiceClientService, undefined, undefined, undefined, undefined, undefined, chatService); + const sessionResource = URI.parse('chat-session:/runtime-mars-questionnaire'); + const rawQuestions: { + header: string; + question: string; + message?: string; + options: { label: string; description: string }[]; + multiSelect?: boolean; + }[] = [ + { + header: 'mars_scope', + question: 'What\'s the scope for Mars integration?', + message: 'This optional detail appears only after the carousel is appended.', + options: [ + { label: 'Full parallel system', description: 'Mars as a complete alternative view with its own layers, data, and panels (like a separate mode)' }, + { label: 'Comparison view', description: 'Earth and Mars side-by-side for comparison purposes' }, + { label: 'Solar system integration', description: 'Mars as part of an expandable planetary system (Earth, Mars, potentially others)' }, + { label: 'Just 3D Mars visualization', description: 'Focus on rendering Mars with minimal data layers for now' }, + ], + }, + { + header: 'mars_data', + question: 'What data should Mars display?', + options: [ + { label: 'Rovers & missions', description: 'Show NASA/international rovers, landing sites, and active missions' }, + { label: 'Geological features', description: 'Volcanoes, canyons, polar caps, water ice deposits' }, + { label: 'Real-time data', description: 'Current rover telemetry, atmospheric data, dust storms' }, + { label: 'Habitability layers', description: 'Radiation, temperature, water availability zones' }, + { label: 'All of the above', description: 'Full comprehensive Mars visualization' }, + ], + multiSelect: true, + }, + { + header: 'mars_textures', + question: 'How should Mars be textured?', + options: [ + { label: 'Procedurally generated (like Earth)', description: 'Canvas-based procedural generation matching current Earth approach' }, + { label: 'Real NASA imagery', description: 'Use actual Mars satellite imagery (requires downloading/hosting image files)' }, + { label: 'Simplified stylized', description: 'Simple color palette (red/orange) like a simplified Earth' }, + ], + }, + { + header: 'mars_timeline', + question: 'Should Mars have historical/future data?', + options: [ + { label: 'Current only', description: 'Show current rovers and active missions' }, + { label: 'Historical missions', description: 'Include past rovers (Spirit, Opportunity, etc.) and historical landing sites' }, + { label: 'Future missions', description: 'Include planned future missions and colonization zones' }, + { label: 'All timeframes', description: 'Full timeline from first landing to future missions' }, + ], + }, + ]; + const backingTool = new class extends mock<IChatToolInvocation>() { + override readonly kind = 'toolInvocation' as const; + override readonly toolId = AskQuestionsToolId; + override readonly toolCallId = 'toolu_runtime'; + override readonly invocationMessage = 'Asked 4 questions (mars_scope, mars_data, mars_textures, mars_timeline)'; + override readonly state = observableValue<IChatToolInvocation.State>('toolState', { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: { questions: rawQuestions }, + confirmationMessages: undefined, + confirm: () => { }, + }); + }(); + const parts: IChatProgressResponseContent[] = [backingTool]; + const pendingConfirmation = observableValue<{ detail?: string } | undefined>('pending', { detail: 'Asked 4 questions' }); + const response = { + isPendingConfirmation: pendingConfirmation, + isIncomplete: observableValue('incomplete', false), + response: { value: parts, getMarkdown: () => '' }, + }; + const lastRequest = { id: 'request-runtime-questionnaire', response }; + const model = { + sessionResource, + title: 'Chat', + lastMessageDate: Date.now(), + getRequests: () => [lastRequest], + lastRequestObs: observableValue('lastRequest', lastRequest), + } as unknown as IChatModel; + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { + state: string; + detail?: string; + confirmation_type?: VoiceConfirmationType; + }; + const checkSessionStateChanges = Reflect.get(controller, '_checkSessionStateChanges') as () => void; + const previousStates = Reflect.get(controller, '_prevSessionStates') as Map<string, { + state: string; + detail: string; + confirmationType?: VoiceConfirmationType; + lastResponseSummary: string; + }>; + + controller.setActiveSessionShown(sessionResource); + chatService.setModels([model]); + previousStates.set(sessionResource.toString(), { state: 'thinking', detail: '', lastResponseSummary: '' }); + const pendingInfo = getAgentStateInfo.call(controller, model); + checkSessionStateChanges.call(controller); + const requestsBeforeCarousel = voiceClientService.requests.length; + const narrationBeforeCarousel = voiceClientService.requests.at(-1); + + const runtimeCarousel = new ChatQuestionCarouselData(rawQuestions.map((question, index) => ({ + id: `toolu_runtime:${index}`, + type: question.multiSelect ? 'multiSelect' : 'singleSelect', + title: question.header, + message: question.question, + detailedMessage: question.message, + options: question.options.map(option => ({ + id: option.label, + label: `${option.label} - ${option.description}`, + value: option.label, + })), + allowFreeformInput: true, + })), true, 'toolu_runtime'); + parts.push(runtimeCarousel); + checkSessionStateChanges.call(controller); + const requestsAfterCarousel = voiceClientService.requests.length; + const narrationAfterCarousel = voiceClientService.requests.at(-1); + + assert.deepStrictEqual({ + pendingState: pendingInfo.state, + pendingType: pendingInfo.confirmation_type, + pendingHasVisibleDetail: pendingInfo.detail?.startsWith('questionnaire: 4 questions'), + requestsBeforeCarousel, + requestsAfterCarousel, + initialNarrationKind: narrationBeforeCarousel?.kind, + initialNarrationType: narrationBeforeCarousel?.confirmationType, + initialHasQuestionCount: narrationBeforeCarousel?.text.startsWith('questionnaire: 4 questions'), + initialHasFirstPrompt: narrationBeforeCarousel?.text.includes('1. What\'s the scope for Mars integration?'), + initialHasLastPrompt: narrationBeforeCarousel?.text.includes('4. Should Mars have historical/future data?'), + followupNarrationKind: narrationAfterCarousel?.kind, + followupHasVisibleOptionDescription: narrationAfterCarousel?.text.includes('Full parallel system - Mars as a complete alternative view'), + includesLateDetails: narrationAfterCarousel?.text.includes('This optional detail appears only after the carousel is appended.'), + usedFallback: narrationBeforeCarousel?.text === 'I need your input in the open questionnaire.', + containsHiddenIds: ['mars_scope', 'mars_data', 'mars_textures', 'mars_timeline', 'toolu_runtime'] + .some(value => narrationBeforeCarousel?.text.includes(value) || narrationAfterCarousel?.text.includes(value)), + }, { + pendingState: 'waiting_for_confirmation', + pendingType: 'questionnaire', + pendingHasVisibleDetail: true, + requestsBeforeCarousel: 1, + requestsAfterCarousel: 2, + initialNarrationKind: 'confirmation', + initialNarrationType: 'questionnaire', + initialHasQuestionCount: true, + initialHasFirstPrompt: true, + initialHasLastPrompt: true, + followupNarrationKind: 'question', + followupHasVisibleOptionDescription: true, + includesLateDetails: false, + usedFallback: false, + containsHiddenIds: false, + }); + }); + + test('defers runtime askQuestions narration until visible parameters populate', () => { + const voiceClientService = new TestVoiceClientService(); + const chatService = new ControllableChatService(); + const controller = createController(voiceClientService, undefined, undefined, undefined, undefined, undefined, chatService); + const sessionResource = URI.parse('chat-session:/late-runtime-questionnaire'); + const toolState = observableValue<IChatToolInvocation.State>('toolState', { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: { questions: [] }, + confirmationMessages: undefined, + confirm: () => { }, + }); + const backingTool = new class extends mock<IChatToolInvocation>() { + override readonly kind = 'toolInvocation' as const; + override readonly toolId = AskQuestionsToolId; + override readonly invocationMessage = 'Asking a clarifying question'; + override readonly state = toolState; + }(); + const pendingConfirmation = observableValue<{ detail?: string } | undefined>('pending', { detail: 'Asking a clarifying question' }); + const response = { + isPendingConfirmation: pendingConfirmation, + isIncomplete: observableValue('incomplete', false), + response: { value: [backingTool], getMarkdown: () => '' }, + }; + const lastRequest = { id: 'request-late-questionnaire', response }; + const model = { + sessionResource, + title: 'Chat', + lastMessageDate: Date.now(), + getRequests: () => [lastRequest], + lastRequestObs: observableValue('lastRequest', lastRequest), + } as unknown as IChatModel; + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { + state: string; + detail?: string; + confirmation_type?: VoiceConfirmationType; + }; + const checkSessionStateChanges = Reflect.get(controller, '_checkSessionStateChanges') as () => void; + const previousStates = Reflect.get(controller, '_prevSessionStates') as Map<string, { + state: string; + detail: string; + confirmationType?: VoiceConfirmationType; + lastResponseSummary: string; + }>; + + controller.setActiveSessionShown(sessionResource); + chatService.setModels([model]); + previousStates.set(sessionResource.toString(), { state: 'thinking', detail: '', lastResponseSummary: '' }); + const pendingInfo = getAgentStateInfo.call(controller, model); + checkSessionStateChanges.call(controller); + const requestsBeforePopulation = voiceClientService.requests.length; + + toolState.set({ + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: { + questions: [{ + header: 'internal_scope', + question: 'Which Mars scope should GitHub Copilot use?', + options: [{ + label: 'Comparison view', + description: 'Show Earth and Mars side-by-side', + value: 'hidden-value', + }], + recommended: true, + }], + }, + confirmationMessages: undefined, + confirm: () => { }, + }, undefined); + checkSessionStateChanges.call(controller); + const narration = voiceClientService.requests.at(-1); + + assert.deepStrictEqual({ + pendingInfo, + requestsBeforePopulation, + narration: narration ? { + kind: narration.kind, + confirmationType: narration.confirmationType, + text: narration.text, + } : undefined, + containsHiddenMetadata: ['internal_scope', 'hidden-value', 'recommended'] + .some(value => narration?.text.includes(value)), + }, { + pendingInfo: { + state: 'waiting_for_confirmation', + confirmation_type: 'questionnaire', + }, + requestsBeforePopulation: 0, + narration: { + kind: 'confirmation', + confirmationType: 'questionnaire', + text: [ + 'questionnaire: 1 question', + '1. Which Mars scope should GitHub Copilot use?', + 'options: Comparison view - Show Earth and Mars side-by-side; a custom response is also available', + 'The questionnaire is open in GitHub Copilot.', + ].join('\n'), + }, + containsHiddenMetadata: false, + }); + }); + + test('carries questionnaire type in session context and clears it when resolved', () => { + const chatService = new ControllableChatService(); + const controller = createController(new TestVoiceClientService(), undefined, undefined, undefined, undefined, undefined, chatService); + const sessionResource = URI.parse('chat-session:/durable-questionnaire'); + const carousel = new ChatQuestionCarouselData([{ + id: 'hidden-question-id', + type: 'singleSelect', + title: 'Hidden title key', + message: 'Which deployment should GitHub Copilot use?', + options: [{ id: 'hidden-option-id', label: 'Preview deployment', value: 'hidden-option-value' }], + }], true); + const pendingConfirmation = observableValue<{ detail?: string } | undefined>('pending', { detail: 'Needs approval' }); + const response = { + isPendingConfirmation: pendingConfirmation, + isIncomplete: observableValue('incomplete', false), + response: { value: [carousel], getMarkdown: () => '' }, + }; + const lastRequest = { id: 'request-questionnaire', response }; + const model = { + sessionResource, + title: 'Chat', + lastMessageDate: Date.now(), + getRequests: () => [lastRequest], + lastRequestObs: observableValue('lastRequest', lastRequest), + } as unknown as IChatModel; + const buildSessionContext = Reflect.get(controller, '_buildSessionContext') as () => { sessions: readonly Record<string, unknown>[] }; + + controller.setActiveSessionShown(sessionResource); + chatService.setModels([model]); + const pendingContext = buildSessionContext.call(controller).sessions[0]; + carousel.isUsed = true; + carousel.isUsed = true; + pendingConfirmation.set(undefined, undefined); + const resolvedContext = buildSessionContext.call(controller).sessions[0]; + + const pending = pendingContext?.['pending'] as Record<string, unknown> | undefined; + assert.deepStrictEqual({ + pendingContext: pendingContext ? { + id: pendingContext['id'], + is_active: pendingContext['is_active'], + agent_state: pendingContext['agent_state'], + agent_state_detail: pendingContext['agent_state_detail'], + confirmation_type: pendingContext['confirmation_type'], + pending: pending ? { + type: pending['type'], + request_id: pending['request_id'], + pendingIdMatchesRequest: typeof pending['pending_id'] === 'string' && pending['pending_id'].startsWith('request-questionnaire#'), + allow_skip: pending['allow_skip'], + questions: pending['questions'], + } : undefined, + } : undefined, + resolvedContext, + }, { + pendingContext: { + id: sessionResource.toString(), + is_active: true, + agent_state: 'waiting_for_confirmation', + agent_state_detail: [ + 'questionnaire: 1 question', + '1. Which deployment should GitHub Copilot use?', + 'options: Preview deployment; a custom response is also available', + 'The questionnaire is open in GitHub Copilot.', + ].join('\n'), + confirmation_type: 'questionnaire', + pending: { + type: 'questions', + request_id: 'request-questionnaire', + pendingIdMatchesRequest: true, + allow_skip: true, + questions: [{ + id: 'hidden-question-id', + type: 'singleSelect', + title: 'Which deployment should GitHub Copilot use?', + allow_freeform: true, + options: [{ label: 'Preview deployment', value: 'hidden-option-value' }], + }], + }, + }, + resolvedContext: { + id: sessionResource.toString(), + label: 'Chat', + is_active: true, + agent_state: 'idle', + }, + }); + }); + + test('routes structured pending responses to the same action that is narrated', () => { + const chatService = new ControllableChatService(); + const controller = createController(new TestVoiceClientService(), undefined, undefined, undefined, undefined, undefined, chatService); + const buildSessionContext = Reflect.get(controller, '_buildSessionContext') as () => { sessions: readonly { pending?: { type: string; pending_id: string; message?: string } }[] }; + const waitingTool = (id: string, postApproval = false) => new class extends mock<IChatToolInvocation>() { + override readonly kind = 'toolInvocation' as const; + override readonly toolId = id; + override readonly invocationMessage = `Run ${id}`; + override readonly state = observableValue<IChatToolInvocation.State>(`${id}State`, postApproval ? { + type: IChatToolInvocation.StateKind.WaitingForPostApproval, + parameters: {}, + confirmationMessages: { title: `Approve ${id}?`, message: `Review ${id}.` }, + confirmed: { type: ToolConfirmKind.UserAction }, + resultDetails: undefined, + confirm: () => { }, + contentForModel: [], + } : { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: {}, + confirmationMessages: { title: `Approve ${id}?`, message: `Review ${id}.` }, + confirm: () => { }, + }); + }(); + const pendingFor = (resource: URI, requestId: string, parts: IChatProgressResponseContent[]) => { + const response = { + isPendingConfirmation: observableValue<{ detail?: string } | undefined>(`${requestId}Pending`, { detail: 'Needs input' }), + isIncomplete: observableValue(`${requestId}Incomplete`, false), + response: { value: parts, getMarkdown: () => '' }, + }; + const lastRequest = { id: requestId, response }; + const model = { + sessionResource: resource, + title: 'Chat', + lastMessageDate: Date.now(), + getRequests: () => [lastRequest], + lastRequestObs: observableValue(`${requestId}LastRequest`, lastRequest), + } as unknown as IChatModel; + controller.setActiveSessionShown(resource); + chatService.setModels([model]); + return buildSessionContext.call(controller).sessions[0]?.pending; + }; + + const questionnaire = new ChatQuestionCarouselData([{ + id: 'region', + type: 'singleSelect', + title: 'Region', + message: 'Which region?', + options: [{ id: 'west', label: 'West US', value: 'westus' }], + }], true); + const unrelatedTool = waitingTool('unrelated'); + const questionnairePending = pendingFor(URI.parse('chat-session:/questionnaire-route'), 'request-questionnaire-route', [questionnaire, unrelatedTool]); + + const plan = new ChatPlanReviewData('Review plan', 'Plan body', [{ id: 'implement', label: 'Implement Plan' }], true); + const olderTool = waitingTool('older'); + const planPending = pendingFor(URI.parse('chat-session:/plan-route'), 'request-plan-route', [olderTool, plan]); + + const postApprovalTool = waitingTool('post-approval', true); + const postApprovalPending = pendingFor(URI.parse('chat-session:/post-route'), 'request-post-route', [postApprovalTool]); + + const askQuestionsTool = waitingTool(AskQuestionsToolId); + const olderQuestionnaire = new ChatQuestionCarouselData([{ + id: 'older-region', + type: 'singleSelect', + title: 'Older region', + message: 'Which older region?', + options: [{ id: 'east', label: 'East US', value: 'eastus' }], + }], true); + const askQuestionsPending = pendingFor(URI.parse('chat-session:/ask-route'), 'request-ask-route', [olderQuestionnaire, askQuestionsTool]); + + assert.deepStrictEqual({ + questionnaire: { + type: questionnairePending?.type, + idMatches: questionnairePending?.pending_id === peekPendingId('request-questionnaire-route', questionnaire), + }, + plan: { + type: planPending?.type, + idMatches: planPending?.pending_id === peekPendingId('request-plan-route', olderTool), + }, + postApproval: { + type: postApprovalPending?.type, + idMatches: postApprovalPending?.pending_id === peekPendingId('request-post-route', postApprovalTool), + }, + askQuestionsBeforeCarousel: { + type: askQuestionsPending?.type, + idMatches: askQuestionsPending?.pending_id === peekPendingId('request-ask-route', olderQuestionnaire), + }, + }, { + questionnaire: { type: 'questions', idMatches: true }, + plan: { type: 'approval', idMatches: true }, + postApproval: { type: 'approval', idMatches: true }, + askQuestionsBeforeCarousel: { type: 'questions', idMatches: true }, + }); + }); + + test('flushes exact typed context before fresh and changed confirmation narration', () => { + const scenarios: { + name: string; + part: IChatProgressResponseContent; + fromState: string; + fromDetail: string; + fromType?: VoiceConfirmationType; + expectedType: VoiceConfirmationType; + expectedDetail: string; + }[] = [ + { + name: 'fresh-generic', + part: { + kind: 'confirmation', + title: 'Install extensions?', + message: 'Review the visible extension approval.', + data: {}, + }, + fromState: 'thinking', + fromDetail: '', + expectedType: 'generic', + expectedDetail: [ + 'confirmation: Install extensions?', + 'Review the visible extension approval.', + ].join('\n'), + }, + { + name: 'plan-to-generic', + part: { + kind: 'confirmation', + title: 'Confirm the revised plan?', + message: 'Review the revised plan confirmation.', + data: {}, + }, + fromState: 'waiting_for_confirmation', + fromDetail: [ + 'plan approval: Review the implementation plan', + 'choices: Implement Plan', + 'The plan is open in GitHub Copilot.', + ].join('\n'), + fromType: 'plan', + expectedType: 'generic', + expectedDetail: [ + 'confirmation: Confirm the revised plan?', + 'Review the revised plan confirmation.', + ].join('\n'), + }, + { + name: 'detail-change', + part: { + kind: 'confirmation', + title: 'Approve the updated extension set?', + message: 'Review the updated visible extension approval.', + data: {}, + }, + fromState: 'waiting_for_confirmation', + fromDetail: 'confirmation: Approve the old extension set?', + fromType: 'generic', + expectedType: 'generic', + expectedDetail: [ + 'confirmation: Approve the updated extension set?', + 'Review the updated visible extension approval.', + ].join('\n'), + }, + ]; + const results: { + name: string; + contextBeforeRequest: boolean; + contextSession: Record<string, unknown> | undefined; + request: { kind: VoiceNarrationKind; text: string; confirmationType?: VoiceConfirmationType } | undefined; + }[] = []; + + for (const scenario of scenarios) { + const voiceClientService = new TestVoiceClientService(); + const chatService = new ControllableChatService(); + const controller = createController(voiceClientService, undefined, undefined, undefined, undefined, undefined, chatService); + const sessionResource = URI.parse(`chat-session:/${scenario.name}`); + const model = pendingResponsePartModel(sessionResource, scenario.part); + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { + state: string; + detail?: string; + confirmation_type?: VoiceConfirmationType; + }; + const pendingChanges = Reflect.get(controller, '_pendingStateChanges') as Map<string, { + sessionId: string; + currentState: string; + label: string; + detail?: string; + confirmationType?: VoiceConfirmationType; + fromState: string; + fromDetail: string; + fromConfirmationType?: VoiceConfirmationType; + fromResponseSummary: string; + pendingId: string; + fromPendingId: string; + }>; + const emitPendingStateChanges = Reflect.get(controller, '_emitPendingStateChanges') as () => void; + const pendingIdFor = Reflect.get(controller, '_pendingIdFor') as (sessionId: string) => string; + + controller.setActiveSessionShown(sessionResource); + chatService.setModels([model]); + voiceClientService.wireEvents.length = 0; + const stateInfo = getAgentStateInfo.call(controller, model); + pendingChanges.set(sessionResource.toString(), { + sessionId: sessionResource.toString(), + currentState: stateInfo.state, + label: 'Chat', + detail: stateInfo.detail, + confirmationType: stateInfo.confirmation_type, + fromState: scenario.fromState, + fromDetail: scenario.fromDetail, + fromConfirmationType: scenario.fromType, + fromResponseSummary: '', + pendingId: pendingIdFor.call(controller, sessionResource.toString()), + fromPendingId: '', + }); + emitPendingStateChanges.call(controller); + + const requestIndex = voiceClientService.wireEvents.findIndex(event => event.type === 'request_narration'); + const contextEvents = voiceClientService.wireEvents.slice(0, requestIndex).filter(event => event.type === 'session_context'); + const contextSession = contextEvents.at(-1)?.context.sessions.find(session => session.id === sessionResource.toString()); + const request = voiceClientService.wireEvents[requestIndex]; + results.push({ + name: scenario.name, + contextBeforeRequest: requestIndex > 0 && contextEvents.length > 0, + contextSession, + request: request?.type === 'request_narration' ? request : undefined, + }); + } + + assert.deepStrictEqual(results.map(result => ({ + name: result.name, + contextBeforeRequest: result.contextBeforeRequest, + contextState: result.contextSession?.['agent_state'], + contextDetail: result.contextSession?.['agent_state_detail'], + contextType: result.contextSession?.['confirmation_type'], + request: result.request, + })), scenarios.map(scenario => ({ + name: scenario.name, + contextBeforeRequest: true, + contextState: 'waiting_for_confirmation', + contextDetail: scenario.expectedDetail, + contextType: scenario.expectedType, + request: { + type: 'request_narration', + kind: 'confirmation', + text: scenario.expectedDetail, + confirmationType: scenario.expectedType, + }, + }))); + }); + + test('same confirmation text with a new type is not deduplicated', async () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionId = 'chat-session:/typed-confirmation-dedup'; + const narrate = Reflect.get(controller, '_narrate') as ( + sessionId: string, + kind: VoiceNarrationKind, + text: string, + reuseId?: string, + checkpoint?: IVoiceCheckpointNarrationMetadata, + confirmationType?: VoiceConfirmationType, + ) => boolean; + await controller.connect(mainWindow); + + const questionnaireSent = narrate.call(controller, sessionId, 'confirmation', 'I need your input.', undefined, undefined, 'questionnaire'); + const duplicateQuestionnaireSent = narrate.call(controller, sessionId, 'confirmation', 'I need your input.', undefined, undefined, 'questionnaire'); + const planSent = narrate.call(controller, sessionId, 'confirmation', 'I need your input.', undefined, undefined, 'plan'); + + assert.deepStrictEqual({ + questionnaireSent, + duplicateQuestionnaireSent, + planSent, + types: voiceClientService.requests.map(request => request.confirmationType), + }, { + questionnaireSent: true, + duplicateQuestionnaireSent: false, + planSent: true, + types: ['questionnaire', 'plan'], + }); + }); + + test('reconnect replays only confirmations matching current text and type', async () => { + const cases: { + name: string; + pending: { kind: 'response' | 'confirmation'; text: string; confirmationType?: VoiceConfirmationType }; + current: { kind: 'response' | 'confirmation'; text: string; confirmationType?: VoiceConfirmationType } | undefined; + }[] = [ + { + name: 'generic-to-plan', + pending: { kind: 'confirmation', text: 'Review this item.', confirmationType: 'generic' }, + current: { kind: 'confirmation', text: 'Review this item.', confirmationType: 'plan' }, + }, + { + name: 'generic-to-idle', + pending: { kind: 'confirmation', text: 'Review this item.', confirmationType: 'generic' }, + current: { kind: 'response', text: 'Done.' }, + }, + { + name: 'legacy-to-generic', + pending: { kind: 'confirmation', text: 'Review this item.' }, + current: { kind: 'confirmation', text: 'Review this item.', confirmationType: 'generic' }, + }, + { + name: 'matching-generic', + pending: { kind: 'confirmation', text: 'Review this item.', confirmationType: 'generic' }, + current: { kind: 'confirmation', text: 'Review this item.', confirmationType: 'generic' }, + }, + { + name: 'matching-legacy', + pending: { kind: 'confirmation', text: 'Legacy confirmation.' }, + current: { kind: 'confirmation', text: 'Legacy confirmation.' }, + }, + { + name: 'response-conflicts-with-generic', + pending: { kind: 'response', text: 'Old final response.' }, + current: { kind: 'confirmation', text: 'Current confirmation.', confirmationType: 'generic' }, + }, + { + name: 'response-summary-changed', + pending: { kind: 'response', text: 'Old final response.' }, + current: { kind: 'response', text: 'New final response.' }, + }, + { + name: 'matching-response', + pending: { kind: 'response', text: 'Final response.' }, + current: { kind: 'response', text: 'Final response.' }, + }, + ]; + const results: { name: string; requests: { kind: VoiceNarrationKind; text: string; confirmationType?: VoiceConfirmationType }[] }[] = []; + + for (const testCase of cases) { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionId = `chat-session:/${testCase.name}`; + await controller.connect(mainWindow); + const retries = Reflect.get(controller, '_pendingNarrationRetries') as Map<string, typeof testCase.pending>; + retries.set(sessionId, testCase.pending); + Reflect.set(controller, '_currentNarratable', () => testCase.current); + controller.setActiveSessionShown(URI.parse(sessionId)); + + voiceClientService.fireSessionInit(); + results.push({ + name: testCase.name, + requests: voiceClientService.requests.map(request => ({ + kind: request.kind, + text: request.text, + ...(request.confirmationType ? { confirmationType: request.confirmationType } : {}), + })), + }); + } + + assert.deepStrictEqual(results, [ + { name: 'generic-to-plan', requests: [] }, + { name: 'generic-to-idle', requests: [] }, + { name: 'legacy-to-generic', requests: [] }, + { name: 'matching-generic', requests: [{ kind: 'confirmation', text: 'Review this item.', confirmationType: 'generic' }] }, + { name: 'matching-legacy', requests: [{ kind: 'confirmation', text: 'Legacy confirmation.' }] }, + { name: 'response-conflicts-with-generic', requests: [] }, + { name: 'response-summary-changed', requests: [] }, + { name: 'matching-response', requests: [{ kind: 'response', text: 'Final response.' }] }, + ]); + }); + + test('busy confirmation retries only when current text and type still match', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionId = 'chat-session:/deferred-confirmation'; + const sessionKey = (Reflect.get(controller, '_sessionKey') as (sessionId: string) => string).call(controller, sessionId); + const deferred = Reflect.get(controller, '_deferredNarrations') as Map<string, { + narrationId: string; + kind: 'confirmation'; + text: string; + reuseNarrationId: boolean; + confirmationType?: VoiceConfirmationType; + }>; + const retry = Reflect.get(controller, '_retryDeferredNarration') as (sessionKey: string) => boolean; + controller.setActiveSessionShown(URI.parse(sessionId)); + + deferred.set(sessionKey, { + narrationId: 'stale-type', + kind: 'confirmation', + text: 'Review this item.', + reuseNarrationId: true, + confirmationType: 'generic', + }); + Reflect.set(controller, '_currentNarratable', () => ({ kind: 'confirmation', text: 'Review this item.', confirmationType: 'plan' })); + const staleTypeRetried = retry.call(controller, sessionKey); + + deferred.set(sessionKey, { + narrationId: 'stale-text', + kind: 'confirmation', + text: 'Old detail.', + reuseNarrationId: true, + confirmationType: 'generic', + }); + Reflect.set(controller, '_currentNarratable', () => ({ kind: 'confirmation', text: 'New detail.', confirmationType: 'generic' })); + const staleTextRetried = retry.call(controller, sessionKey); + + deferred.set(sessionKey, { + narrationId: 'matching', + kind: 'confirmation', + text: 'Current detail.', + reuseNarrationId: true, + confirmationType: 'generic', + }); + Reflect.set(controller, '_currentNarratable', () => ({ kind: 'confirmation', text: 'Current detail.', confirmationType: 'generic' })); + const matchingRetried = retry.call(controller, sessionKey); + + assert.deepStrictEqual({ + staleTypeRetried, + staleTextRetried, + matchingRetried, + requests: voiceClientService.requests.map(request => ({ + narrationId: request.narrationId, + text: request.text, + confirmationType: request.confirmationType, + })), + deferredCount: deferred.size, + }, { + staleTypeRetried: false, + staleTextRetried: false, + matchingRetried: true, + requests: [{ + narrationId: 'matching', + text: 'Current detail.', + confirmationType: 'generic', + }], + deferredCount: 0, + }); + }); + + test('auto-approve ignores questionnaire backing tools', () => { + const controller = createController(new TestVoiceClientService()); + const confirmed: ToolConfirmKind[] = []; + const toolInvocation = new class extends mock<IChatToolInvocation>() { + override readonly kind = 'toolInvocation' as const; + override readonly state = observableValue<IChatToolInvocation.State>('toolState', { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: {}, + confirmationMessages: { + title: 'Submit questionnaire?', + message: 'Submits the questionnaire answers.', + }, + confirm: reason => confirmed.push(reason.type), + }); + override readonly invocationMessage = 'Submit questionnaire'; + }(); + const questionnaire = new ChatQuestionCarouselData([{ + id: 'hidden-question-id', + type: 'singleSelect', + title: 'Choose an option', + options: [{ id: 'hidden-option-id', label: 'Visible option', value: 'hidden-value' }], + }], true); + const pendingConfirmation = observableValue<{ detail?: string } | undefined>('pending', { detail: 'Needs input' }); + const modelWithQuestionnaire = { + getRequests: () => [{ + response: { + isPendingConfirmation: pendingConfirmation, + response: { value: [toolInvocation, questionnaire] }, + }, + }], + } as unknown as IChatModel; + const modelWithTool = { + getRequests: () => [{ + response: { + isPendingConfirmation: pendingConfirmation, + response: { value: [toolInvocation] }, + }, + }], + } as unknown as IChatModel; + const autoApprovePendingTools = Reflect.get(controller, '_autoApprovePendingTools') as (model: IChatModel) => void; + + autoApprovePendingTools.call(controller, modelWithQuestionnaire); + autoApprovePendingTools.call(controller, modelWithTool); + + assert.deepStrictEqual(confirmed, [ToolConfirmKind.UserAction]); + }); + + test('handles freeform and defers empty questionnaire data', () => { + const controller = createController(new TestVoiceClientService()); + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { state: string; detail?: string }; + const freeform = new ChatQuestionCarouselData([{ + id: 'internal_name_key', + type: 'text', + title: 'internal_name_key', + message: 'What should we call the Mars explorer?', + }], false); + const missing = new ChatQuestionCarouselData([], true, 'hidden_resolve_id'); + const internalTitleOnly = new ChatQuestionCarouselData([{ + id: 'internal_prompt_key', + type: 'text', + title: 'internal_prompt_key', + }], true); + const noCustomOption = new ChatQuestionCarouselData([{ + id: 'navigation', + type: 'singleSelect', + title: 'Navigation', + message: 'Choose a navigation mode.', + options: [{ id: 'guided', label: 'Guided route', value: 'guided' }], + allowFreeformInput: false, + }], true); + + assert.deepStrictEqual([ + getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/freeform'), freeform, undefined, false)), + getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/missing'), missing, undefined, false)), + getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/internal-title'), internalTitleOnly, undefined, false)), + getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/no-custom'), noCustomOption, undefined, false)), + ], [ + { + state: 'waiting_for_confirmation', + confirmation_type: 'questionnaire', + detail: [ + 'questionnaire: 1 question', + '1. What should we call the Mars explorer?', + 'response: enter a free-form answer in GitHub Copilot', + 'The questionnaire is open in GitHub Copilot.', + ].join('\n'), + }, + { + state: 'waiting_for_confirmation', + confirmation_type: 'questionnaire' + }, + { + state: 'waiting_for_confirmation', + confirmation_type: 'questionnaire', + detail: [ + 'questionnaire: 1 question', + '1. I need your input in the open questionnaire.', + 'response: enter a free-form answer in GitHub Copilot', + 'The questionnaire is open in GitHub Copilot.', + ].join('\n'), + }, + { + state: 'waiting_for_confirmation', + confirmation_type: 'questionnaire', + detail: [ + 'questionnaire: 1 question', + '1. Choose a navigation mode.', + 'options: Guided route', + 'The questionnaire is open in GitHub Copilot.', + ].join('\n'), + }, + ]); + }); + + test('bounds questionnaire questions and options with omission counts', () => { + const controller = createController(new TestVoiceClientService()); + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { detail?: string }; + const carousel = new ChatQuestionCarouselData(Array.from({ length: 8 }, (_, questionIndex) => ({ + id: `internal_question_${questionIndex}`, + type: 'singleSelect' as const, + title: `Internal question ${questionIndex}`, + message: `Visible question ${questionIndex + 1}?`, + options: Array.from({ length: 8 }, (_, optionIndex) => ({ + id: `internal_option_${questionIndex}_${optionIndex}`, + label: `Visible option ${optionIndex + 1}`, + value: `hidden_value_${optionIndex}`, + })), + })), true); + const detail = getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/bounded'), carousel)).detail ?? ''; + + assert.deepStrictEqual({ + withinLimit: detail.length <= 2_400, + includesOptionOmission: detail.includes('3 more options'), + includesQuestionOmission: detail.includes('2 more questions are open in GitHub Copilot.'), + containsInternalIds: detail.includes('internal_question_') || detail.includes('internal_option_') || detail.includes('hidden_value_'), + }, { + withinLimit: true, + includesOptionOmission: true, + includesQuestionOmission: true, + containsInternalIds: false, + }); + }); + + test('distinguishes plan, elicitation, and tool approval using visible text', () => { + const controller = createController(new TestVoiceClientService()); + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { state: string; detail?: string }; + const plan = new ChatPlanReviewData('Review the Mars implementation plan', '# Hidden plan body', [ + { id: 'internal_implement', label: 'Implement Plan', description: 'Start making the changes' }, + { id: 'internal_autopilot', label: 'Continue in Autopilot', description: 'Proceed automatically' }, + ], true, undefined, 'internal_plan_resolve_id'); + const elicitation = new ChatElicitationRequestPart( + new MarkdownString('Choose a deployment target'), + 'Select where GitHub Copilot should deploy the preview.', + 'Your choice is required before continuing.', + 'Continue', + 'Cancel', + async () => ElicitationState.Accepted, + ); + const confirmation: IChatConfirmation = { + kind: 'confirmation', + title: 'Install recommended extensions?', + message: new MarkdownString('This installs the extensions shown in the open approval.'), + buttons: ['Install', 'Cancel'], + data: { hiddenInternalId: 'extension_install' }, + }; + + assert.deepStrictEqual([ + getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/plan'), plan)), + getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/elicitation'), elicitation)), + getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/confirmation'), confirmation)), + ], [ + { + state: 'waiting_for_confirmation', + confirmation_type: 'plan', + detail: [ + 'plan approval: Review the Mars implementation plan', + 'choices: Implement Plan - Start making the changes; Continue in Autopilot - Proceed automatically', + 'The plan is open in GitHub Copilot.', + ].join('\n'), + }, + { + state: 'waiting_for_confirmation', + confirmation_type: 'elicitation', + detail: [ + 'input request: Choose a deployment target', + 'Your choice is required before continuing.', + 'Select where GitHub Copilot should deploy the preview.', + 'choices: Continue; Cancel', + ].join('\n'), + }, + { + state: 'waiting_for_confirmation', + confirmation_type: 'generic', + detail: [ + 'confirmation: Install recommended extensions?', + 'This installs the extensions shown in the open approval.', + 'choices: Install; Cancel', + ].join('\n'), + }, + ]); + }); + + test('uses visible tool confirmation messages instead of hidden parameters', () => { + const controller = createController(new TestVoiceClientService()); + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { state: string; detail?: string }; + const toolState = observableValue<IChatToolInvocation.State>('toolState', { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: { + command: 'hidden-internal-command', + explanation: 'hidden-internal-explanation', + }, + confirmationMessages: { + title: new MarkdownString('Run the workspace build?'), + message: 'This runs the build task shown in the approval.', + }, + confirm: () => { }, + }); + const toolInvocation = new class extends mock<IChatToolInvocation>() { + override readonly kind = 'toolInvocation' as const; + override readonly state = toolState; + override readonly invocationMessage = 'Run the workspace build'; + }(); + const stateInfo = getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/tool'), toolInvocation)); + + assert.deepStrictEqual({ + stateInfo, + containsHiddenParameters: stateInfo.detail?.includes('hidden-internal'), + }, { + stateInfo: { + state: 'waiting_for_confirmation', + confirmation_type: 'tool', + detail: [ + 'tool approval: Run the workspace build?', + 'This runs the build task shown in the approval.', + ].join('\n'), + }, + containsHiddenParameters: false, + }); + }); + + test('narrates authentication using the visible server name without hidden server metadata', () => { + const controller = createController(new TestVoiceClientService()); + const getAgentStateInfo = Reflect.get(controller, '_getAgentStateInfo') as (model: IChatModel) => { state: string; detail?: string }; + const authenticationState = observableValue<IChatToolInvocation.State>('authenticationState', { + type: IChatToolInvocation.StateKind.WaitingForAuthentication, + parameters: { hiddenParameter: 'secret-internal-value' }, + confirmationMessages: undefined, + confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded }, + server: { + id: 'hidden-server-id', + name: 'Mars Data MCP', + resource: 'hidden-server-resource', + }, + cancel: () => { }, + }); + const toolInvocation = new class extends mock<IChatToolInvocation>() { + override readonly kind = 'toolInvocation' as const; + override readonly state = authenticationState; + override readonly invocationMessage = 'Authenticate the Mars data server'; + }(); + const stateInfo = getAgentStateInfo.call(controller, pendingResponsePartModel(URI.parse('chat-session:/authentication'), toolInvocation, 'Authenticate Mars Data MCP to continue...')); + + assert.deepStrictEqual({ + stateInfo, + containsHiddenMetadata: ['hidden-server-id', 'hidden-server-resource', 'secret-internal-value'] + .some(value => stateInfo.detail?.includes(value)), + }, { + stateInfo: { + state: 'waiting_for_confirmation', + confirmation_type: 'generic', + detail: [ + 'authentication request: MCP authentication required', + 'The MCP server Mars Data MCP requires authentication to continue this tool call.', + 'choices: Authenticate; Cancel', + ].join('\n'), + }, + containsHiddenMetadata: false, + }); + }); + + test('does not watch progress when agent progress is not enabled', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController( + voiceClientService, + undefined, + undefined, + undefined, + undefined, + new TestConfigurationService({ 'agents.voice.handsFree': false }), + ); + const sessionResource = URI.parse('chat-session:/disabled-progress'); + const { changeEmitter, parts, response } = createVoiceProgressResponse('response-disabled'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, response); + parts.push({ kind: 'voiceProgress', id: 'investigating', value: 'Investigating the relevant code.' }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(10_000); + + assert.deepStrictEqual(voiceClientService.requests, []); + }); + + test('marks voice requests only when agent progress is enabled', async () => { + const disabledChatService = new TestChatService(); + const disabledController = createController( + new TestVoiceClientService(), + undefined, + undefined, + undefined, + undefined, + new TestConfigurationService({ 'agents.voice.handsFree': false }), + disabledChatService, + ); + const enabledChatService = new TestChatService(); + const enabledController = createController( + new TestVoiceClientService(), + undefined, + undefined, + undefined, + undefined, + new TestConfigurationService({ 'agents.voice.handsFree': false, [VOICE_AGENT_PROGRESS_SETTING]: true }), + enabledChatService, + ); + const sendVoiceRequest = Reflect.get(disabledController, '_sendVoiceRequest') as (resource: URI, text: string) => Promise<ChatSendResult | undefined>; + + await sendVoiceRequest.call(disabledController, URI.parse('chat-session:/disabled'), 'Check the code.'); + await sendVoiceRequest.call(enabledController, URI.parse('chat-session:/enabled'), 'Check the code.'); + + assert.deepStrictEqual({ + disabled: disabledChatService.sendRequestOptions[0]?.isVoiceModeInput, + enabled: enabledChatService.sendRequestOptions[0]?.isVoiceModeInput, + }, { + disabled: false, + enabled: true, + }); + }); + + test('delays, coalesces, and preserves throttled voice progress for the shown request', () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionResource = URI.parse('chat-session:/voice-progress'); + const { changeEmitter, parts, response } = createVoiceProgressResponse('response-1'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + const sessionKey = (Reflect.get(controller, '_sessionKey') as (sessionId: string) => string).call(controller, sessionResource.toString()); + const lastSpokenAt = Reflect.get(controller, '_lastSpokenAtBySession') as Map<string, number>; + + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, response); + ttsPlaybackService.playAudioChunk('ack'); + Reflect.set(controller, '_currentPlaybackSessionId', sessionResource.toString()); + Reflect.set(controller, '_currentPlaybackResponseId', 'ack-response'); + parts.push({ kind: 'voiceProgress', id: 'investigating', value: 'Investigating the relevant code.' }); + parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating the code.' }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(4_000); + parts.push({ kind: 'voiceProgress', id: 'validating', value: 'Validating the changes.' }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(1_000); + assert.strictEqual(voiceClientService.requests.length, 0); + lastSpokenAt.set(sessionKey, Date.now()); + ttsPlaybackService.stopPlayback(); + clock.tick(4_999); + assert.strictEqual(voiceClientService.requests.length, 0); + clock.tick(1); + + parts.push({ kind: 'voiceProgress', id: 'recovering', value: 'Trying a different approach.' }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(9_999); + assert.strictEqual(voiceClientService.requests.length, 1); + clock.tick(1); + + assert.deepStrictEqual(voiceClientService.requests.map(request => ({ + kind: request.kind, + text: request.text, + checkpoint: request.checkpoint, + })), [ + { + kind: 'checkpoint', + text: 'Validating the changes.', + checkpoint: { requestId: 'request-response-1', checkpointId: 'validating', sequence: 1 }, + }, + { + kind: 'checkpoint', + text: 'Trying a different approach.', + checkpoint: { requestId: 'request-response-1', checkpointId: 'recovering', sequence: 2 }, + }, + ]); + }); + + test('sends the first semantic checkpoint after five seconds without prior speech', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionResource = URI.parse('chat-session:/initial-progress-delay'); + const { changeEmitter, parts, response } = createVoiceProgressResponse('response-initial-delay'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, response); + parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating the code.' }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(4_999); + assert.strictEqual(voiceClientService.requests.length, 0); + clock.tick(1); + + assert.deepStrictEqual(voiceClientService.requests.map(request => request.checkpoint), [{ + requestId: 'request-response-initial-delay', + checkpointId: 'editing', + sequence: 1, + }]); + }); + + test('schedules all five semantic stages once at the existing cadence', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionResource = URI.parse('chat-session:/five-progress-stages'); + const { changeEmitter, parts, response } = createVoiceProgressResponse('response-five-stages'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + const stages = ['investigating', 'planning', 'editing', 'validating', 'recovering'] as const; + + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, response); + for (const [index, stage] of stages.entries()) { + parts.push({ kind: 'voiceProgress', id: stage, value: `${stage} update` }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(index === 0 ? 5_000 : 10_000); + } + parts.push({ kind: 'voiceProgress', id: 'recovering', value: 'duplicate recovery' }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(10_000); + + assert.deepStrictEqual(voiceClientService.requests.map(request => ({ + text: request.text, + checkpoint: request.checkpoint, + })), stages.map((stage, index) => ({ + text: `${stage} update`, + checkpoint: { + requestId: 'request-response-five-stages', + checkpointId: stage, + sequence: index + 1, + }, + }))); + }); + + test('final response cancels pending voice progress', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionResource = URI.parse('chat-session:/final-cancels-progress'); + const { changeEmitter, parts, response } = createVoiceProgressResponse('response-final'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + const handleStateChange = Reflect.get(controller, '_handleNarratableStateChange') as (sessionId: string, state: string, detail: string | undefined, summary: string | undefined, shown: string) => void; + + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, response); + parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating the code.' }); + changeEmitter.fire({ reason: 'other' }); + handleStateChange.call(controller, sessionResource.toString(), 'idle', undefined, 'Finished successfully.', sessionResource.toString()); + clock.tick(5_000); + + assert.deepStrictEqual(voiceClientService.requests.map(request => request.kind), ['response']); + }); + + test('confirmation cancels pending voice progress', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionResource = URI.parse('chat-session:/confirmation-cancels-progress'); + const { changeEmitter, parts, response } = createVoiceProgressResponse('response-confirmation'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + const handleStateChange = Reflect.get(controller, '_handleNarratableStateChange') as (sessionId: string, state: string, detail: string | undefined, summary: string | undefined, shown: string) => void; + + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, response); + parts.push({ kind: 'voiceProgress', id: 'validating', value: 'Validating the changes.' }); + changeEmitter.fire({ reason: 'other' }); + handleStateChange.call(controller, sessionResource.toString(), 'waiting_for_confirmation', 'Approve the command.', undefined, sessionResource.toString()); + clock.tick(5_000); + + assert.deepStrictEqual(voiceClientService.requests.map(request => request.kind), ['confirmation']); + }); + + test('request cancellation and disconnect cancel pending voice progress', () => { + const firstVoiceClient = new TestVoiceClientService(); + const firstController = createController(firstVoiceClient); + const firstSession = URI.parse('chat-session:/cancelled-progress'); + const firstResponse = createVoiceProgressResponse('response-cancelled'); + const firstConnected = Reflect.get(firstController, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const firstWatch = Reflect.get(firstController, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + + firstConnected.set(true, undefined); + firstController.setActiveSessionShown(firstSession); + firstWatch.call(firstController, firstSession, firstResponse.response); + firstResponse.parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating the code.' }); + firstResponse.changeEmitter.fire({ reason: 'other' }); + firstController.markUserCancelled(firstSession.toString()); + + const secondVoiceClient = new TestVoiceClientService(); + const secondController = createController(secondVoiceClient); + const secondSession = URI.parse('chat-session:/disconnected-progress'); + const secondResponse = createVoiceProgressResponse('response-disconnected'); + const secondConnected = Reflect.get(secondController, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const secondWatch = Reflect.get(secondController, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + + secondConnected.set(true, undefined); + secondController.setActiveSessionShown(secondSession); + secondWatch.call(secondController, secondSession, secondResponse.response); + secondResponse.parts.push({ kind: 'voiceProgress', id: 'recovering', value: 'Trying another approach.' }); + secondResponse.changeEmitter.fire({ reason: 'other' }); + secondController.disconnect('explicit'); + clock.tick(5_000); + + assert.deepStrictEqual({ + cancelledRequests: firstVoiceClient.requests, + disconnectedRequests: secondVoiceClient.requests, + }, { + cancelledRequests: [], + disconnectedRequests: [], + }); + }); + + test('transient disconnect retains the latest pending checkpoint until reconnect', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionResource = URI.parse('chat-session:/reconnect-progress'); + const { changeEmitter, parts, response } = createVoiceProgressResponse('response-reconnect'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, response); + parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating the code.' }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(4_000); + isConnected.set(false, undefined); + clock.tick(1_000); + assert.strictEqual(voiceClientService.requests.length, 0); + isConnected.set(true, undefined); + + assert.deepStrictEqual(voiceClientService.requests.map(request => request.checkpoint), [{ + requestId: 'request-response-reconnect', + checkpointId: 'editing', + sequence: 1, + }]); + }); + + test('a new voice request cancels only the shown session checkpoint', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const shownSession = URI.parse('chat-session:/shown-progress'); + const backgroundSession = URI.parse('chat-session:/background-progress'); + const shownResponse = createVoiceProgressResponse('response-shown'); + const backgroundResponse = createVoiceProgressResponse('response-background'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + + isConnected.set(true, undefined); + controller.setActiveSessionShown(shownSession); + watchVoiceProgress.call(controller, shownSession, shownResponse.response); + watchVoiceProgress.call(controller, backgroundSession, backgroundResponse.response); + shownResponse.parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating shown code.' }); + backgroundResponse.parts.push({ kind: 'voiceProgress', id: 'validating', value: 'Validating background code.' }); + shownResponse.changeEmitter.fire({ reason: 'other' }); + backgroundResponse.changeEmitter.fire({ reason: 'other' }); + controller.pttDown('explicit'); + controller.setActiveSessionShown(backgroundSession); + clock.tick(5_000); + + assert.deepStrictEqual(voiceClientService.requests.map(request => request.checkpoint?.requestId), ['request-response-background']); + }); + + test('barge-in and a new explicit voice request cancel pending voice progress', () => { + const bargeVoiceClient = new TestVoiceClientService(); + const bargeController = createController(bargeVoiceClient); + const bargeSession = URI.parse('chat-session:/barge-progress'); + const bargeResponse = createVoiceProgressResponse('response-barge'); + const bargeConnected = Reflect.get(bargeController, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const bargeWatch = Reflect.get(bargeController, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + const handleBargeIn = Reflect.get(bargeController, '_handleBargeIn') as (event: IVoiceBargeIn) => void; + + bargeConnected.set(true, undefined); + bargeController.setActiveSessionShown(bargeSession); + bargeWatch.call(bargeController, bargeSession, bargeResponse.response); + bargeResponse.parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating the code.' }); + bargeResponse.changeEmitter.fire({ reason: 'other' }); + handleBargeIn.call(bargeController, { turnId: 'new-turn', interruptedTurnId: 'old-turn' }); + + const pttVoiceClient = new TestVoiceClientService(); + const pttController = createController(pttVoiceClient); + const pttSession = URI.parse('chat-session:/ptt-progress'); + const pttResponse = createVoiceProgressResponse('response-ptt'); + const pttConnected = Reflect.get(pttController, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const pttWatch = Reflect.get(pttController, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + + pttConnected.set(true, undefined); + pttController.setActiveSessionShown(pttSession); + pttWatch.call(pttController, pttSession, pttResponse.response); + pttResponse.parts.push({ kind: 'voiceProgress', id: 'validating', value: 'Validating the changes.' }); + pttResponse.changeEmitter.fire({ reason: 'other' }); + pttController.pttDown('explicit'); + clock.tick(5_000); + + assert.deepStrictEqual({ + bargeRequests: bargeVoiceClient.requests, + pttRequests: pttVoiceClient.requests, + }, { + bargeRequests: [], + pttRequests: [], + }); + }); + + test('busy, invalid, and legacy suppressed checkpoints are never retried', () => { + const dispositions = ['busy', 'invalid', 'suppressed'] as const; + const results: boolean[] = []; + for (const disposition of dispositions) { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionId = `chat-session:/${disposition}`; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + const handleAck = Reflect.get(controller, '_handleNarrationAck') as (event: IVoiceNarrationAck) => void; + const retryDeferred = Reflect.get(controller, '_retryDeferredNarration') as (sessionKey: string, narrationId?: string) => boolean; + const sessionKey = (Reflect.get(controller, '_sessionKey') as (sessionId: string) => string).call(controller, sessionId); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: `request-${disposition}`, + checkpointId: 'editing', + sequence: 1, + }); + const request = voiceClientService.requests[0]; + handleAck.call(controller, { + narrationId: request.narrationId, + codingSessionId: sessionId, + disposition, + }); + results.push(retryDeferred.call(controller, sessionKey, request.narrationId)); + } + + assert.deepStrictEqual(results, [false, false, false]); + }); + + test('active checkpoint playback is preempted when final response audio starts', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-final'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const checkpointId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: checkpointId, + }); + narrate.call(controller, sessionId, 'response', 'Everything is complete.'); + assert.strictEqual(ttsPlaybackService.stopCount, 0); + const finalId = voiceClientService.requests[1].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'final', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: finalId, + }); + voiceClientService.fireAudioResponse({ + audio: 'stale-checkpoint', + isFirstChunk: false, + isFinal: true, + codingSessionId: sessionId, + responseId: checkpointId, + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + playbackCompletions: voiceClientService.playbackCompletions, + }, { + stopCount: 1, + playedAudio: ['checkpoint', 'final'], + playbackCompletions: [], + }); + }); + + test('empty final response does not preempt active checkpoint playback', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-empty-response'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const checkpointId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: checkpointId, + }); + narrate.call(controller, sessionId, 'response', 'Progress-only final summary.'); + const responseId = voiceClientService.requests[1].narrationId; + voiceClientService.fireAudioResponse({ + audio: '', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId, + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + isPlaying: ttsPlaybackService.isPlaying, + }, { + stopCount: 0, + playedAudio: ['checkpoint'], + isPlaying: true, + }); + }); + + test('completed checkpoint playback acknowledges the correlated playback id', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-complete'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const narrationId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: narrationId, + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + narrationKind: 'checkpoint', + playbackId: 'playback-1', + }); + ttsPlaybackService.stopPlayback(); + + assert.deepStrictEqual(voiceClientService.playbackCompletions, [{ + sessionId, + narrationId, + playbackId: 'playback-1', + }]); + }); + + test('dropped re-narration does not preempt active checkpoint playback', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-reread'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + const recentlyRead = Reflect.get(controller, '_recentlyReadResponse') as Map<string, { transcript: string; at: number }>; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const checkpointId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: checkpointId, + }); + recentlyRead.set(sessionId, { transcript: 'already heard', at: Date.now() }); + voiceClientService.fireAudioResponse({ + audio: 'duplicate', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: 'duplicate-response', + transcript: 'Already heard.', + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + }, { + stopCount: 0, + playedAudio: ['checkpoint'], + }); + }); + + test('active checkpoint playback is preempted by confirmation', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-confirmation'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Validating the changes.', undefined, { + requestId: 'request-1', + checkpointId: 'validating', + sequence: 1, + }); + const checkpointId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: checkpointId, + }); + narrate.call(controller, sessionId, 'confirmation', 'Approve the command.'); + const confirmationId = voiceClientService.requests[1].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'confirmation', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: confirmationId, + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + }, { + stopCount: 1, + playedAudio: ['checkpoint', 'confirmation'], + }); + }); + + test('direct substantive audio preempts active checkpoint playback', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-direct-reply'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const checkpointId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: checkpointId, + }); + voiceClientService.fireAudioResponse({ + audio: 'direct-reply', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: 'direct-response', + transcript: 'Here is the substantive result.', + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + }, { + stopCount: 1, + playedAudio: ['checkpoint', 'direct-reply'], + }); + }); + + test('cross-session substantive audio preempts active checkpoint playback', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const checkpointSessionId = 'chat-session:/checkpoint-background'; + const responseSessionId = 'chat-session:/response-foreground'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(checkpointSessionId)); + + narrate.call(controller, checkpointSessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const checkpointId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: checkpointSessionId, + responseId: checkpointId, + }); + controller.setActiveSessionShown(URI.parse(responseSessionId)); + voiceClientService.fireAudioResponse({ + audio: 'substantive-response', + isFirstChunk: true, + isFinal: true, + codingSessionId: responseSessionId, + responseId: 'direct-response', + transcript: 'The foreground task is complete.', + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + }, { + stopCount: 1, + playedAudio: ['checkpoint', 'substantive-response'], + }); + }); + + test('newer checkpoint preempts active older checkpoint and discards stale chunks', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-replacement'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const firstId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'editing', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: firstId, + }); + narrate.call(controller, sessionId, 'checkpoint', 'Validating the result.', undefined, { + requestId: 'request-1', + checkpointId: 'validating', + sequence: 2, + }); + const secondId = voiceClientService.requests[1].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'stale-editing', + isFirstChunk: false, + isFinal: true, + codingSessionId: sessionId, + responseId: firstId, + }); + voiceClientService.fireAudioResponse({ + audio: 'validating', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: secondId, + }); + + assert.deepStrictEqual(ttsPlaybackService.playedAudio, ['editing', 'validating']); + }); + + test('cross-session checkpoint replaces active checkpoint', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const firstSessionId = 'chat-session:/checkpoint-first-session'; + const secondSessionId = 'chat-session:/checkpoint-second-session'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(firstSessionId)); + + narrate.call(controller, firstSessionId, 'checkpoint', 'Updating the first task.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const firstId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'first-checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: firstSessionId, + responseId: firstId, + }); + narrate.call(controller, secondSessionId, 'checkpoint', 'Validating the second task.', undefined, { + requestId: 'request-2', + checkpointId: 'validating', + sequence: 1, + }); + const secondId = voiceClientService.requests[1].narrationId; + controller.setActiveSessionShown(URI.parse(secondSessionId)); + voiceClientService.fireAudioResponse({ + audio: 'second-checkpoint', + isFirstChunk: true, + isFinal: true, + codingSessionId: secondSessionId, + responseId: secondId, + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + }, { + stopCount: 1, + playedAudio: ['first-checkpoint', 'second-checkpoint'], + }); + }); + + test('pre-decode checkpoint preemption does not poison replacement completion', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new DeferredFirstTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-predecode'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const firstId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'decoding-checkpoint', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: firstId, + narrationKind: 'checkpoint', + playbackId: 'playback-1', + }); + narrate.call(controller, sessionId, 'checkpoint', 'Validating the result.', undefined, { + requestId: 'request-1', + checkpointId: 'validating', + sequence: 2, + }); + const secondId = voiceClientService.requests[1].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'replacement-checkpoint', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: secondId, + narrationKind: 'checkpoint', + playbackId: 'playback-2', + }); + ttsPlaybackService.stopPlayback(); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playbackCompletions: voiceClientService.playbackCompletions, + }, { + stopCount: 2, + playbackCompletions: [{ sessionId, narrationId: secondId, playbackId: 'playback-2' }], + }); + }); + + test('scheduled newer checkpoint replaces active checkpoint at the cadence boundary', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionResource = URI.parse('chat-session:/scheduled-checkpoint-replacement'); + const { changeEmitter, parts, response } = createVoiceProgressResponse('response-scheduled-replacement'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + + await controller.connect(mainWindow); + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, response); + parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating the code.' }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(5_000); + const firstId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'editing', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionResource.toString(), + responseId: firstId, + }); + + parts.push({ kind: 'voiceProgress', id: 'validating', value: 'Validating the result.' }); + changeEmitter.fire({ reason: 'other' }); + clock.tick(10_000); + const secondId = voiceClientService.requests[1].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'stale-editing', + isFirstChunk: false, + isFinal: true, + codingSessionId: sessionResource.toString(), + responseId: firstId, + }); + voiceClientService.fireAudioResponse({ + audio: 'validating', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionResource.toString(), + responseId: secondId, + }); + + assert.deepStrictEqual({ + checkpoints: voiceClientService.requests.map(request => request.checkpoint), + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + }, { + checkpoints: [ + { requestId: 'request-response-scheduled-replacement', checkpointId: 'editing', sequence: 1 }, + { requestId: 'request-response-scheduled-replacement', checkpointId: 'validating', sequence: 2 }, + ], + stopCount: 1, + playedAudio: ['editing', 'validating'], + }); + }); + + test('request cancellation preempts active checkpoint playback and discards trailing chunks', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/active-checkpoint-cancellation'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const narrationId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: narrationId, + }); + controller.markUserCancelled(sessionId); + voiceClientService.fireAudioResponse({ + audio: 'stale-checkpoint', + isFirstChunk: false, + isFinal: true, + codingSessionId: sessionId, + responseId: narrationId, + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + }, { + stopCount: 1, + playedAudio: ['checkpoint'], + }); + }); + + test('explicit PTT retires checkpoint tracking before clearing playback correlation', async () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionId = 'chat-session:/checkpoint-ptt-tracking'; + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + isConnected.set(true, undefined); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const firstId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: firstId, + }); + controller.pttDown('explicit'); + const sentNextCheckpoint = narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-2', + checkpointId: 'editing', + sequence: 1, + }); + + assert.deepStrictEqual({ + sentNextCheckpoint, + requestIds: voiceClientService.requests.map(request => request.checkpoint?.requestId), + }, { + sentNextCheckpoint: true, + requestIds: ['request-1', 'request-2'], + }); + }); + + test('barge-in stops active checkpoint playback and discards trailing chunks', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-barge'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + const handleBargeIn = Reflect.get(controller, '_handleBargeIn') as (event: IVoiceBargeIn) => void; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const checkpointId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: checkpointId, + turnId: 'checkpoint-turn', + }); + handleBargeIn.call(controller, { turnId: 'user-turn', interruptedTurnId: checkpointId }); + voiceClientService.fireAudioResponse({ + audio: 'stale-checkpoint', + isFirstChunk: false, + isFinal: true, + codingSessionId: sessionId, + responseId: checkpointId, + turnId: 'checkpoint-turn', + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + }, { + stopCount: 1, + playedAudio: ['checkpoint'], + }); + }); + + test('backend interruption stops only the matching active checkpoint', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-server-interruption'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const narrationId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: narrationId, + }); + voiceClientService.fireNarrationInterrupted({ + narrationId, + codingSessionId: sessionId, + retryable: false, + reason: 'superseded_by_response', + }); + voiceClientService.fireAudioResponse({ + audio: 'stale-checkpoint', + isFirstChunk: false, + isFinal: true, + codingSessionId: sessionId, + responseId: narrationId, + }); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playedAudio: ttsPlaybackService.playedAudio, + playbackCompletions: voiceClientService.playbackCompletions, + }, { + stopCount: 1, + playedAudio: ['checkpoint'], + playbackCompletions: [], + }); + }); + + test('late backend interruption does not stop a replacement checkpoint', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-late-server-interruption'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const firstId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'first-checkpoint', + isFirstChunk: true, + isFinal: false, + codingSessionId: sessionId, + responseId: firstId, + }); + narrate.call(controller, sessionId, 'checkpoint', 'Validating the result.', undefined, { + requestId: 'request-1', + checkpointId: 'validating', + sequence: 2, + }); + const secondId = voiceClientService.requests[1].narrationId; + voiceClientService.fireAudioResponse({ + audio: 'second-checkpoint', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: secondId, + narrationKind: 'checkpoint', + playbackId: 'playback-2', + }); + voiceClientService.fireNarrationInterrupted({ + narrationId: firstId, + codingSessionId: sessionId, + retryable: false, + reason: 'superseded_by_checkpoint', + }); + ttsPlaybackService.stopPlayback(); + + assert.deepStrictEqual({ + stopCount: ttsPlaybackService.stopCount, + playbackCompletions: voiceClientService.playbackCompletions, + }, { + stopCount: 2, + playbackCompletions: [{ sessionId, narrationId: secondId, playbackId: 'playback-2' }], + }); + }); + + test('checkpoint sequence restarts for the next chat request', () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionResource = URI.parse('chat-session:/sequence-reset'); + const first = createVoiceProgressResponse('response-sequence-1', 'request-1'); + const second = createVoiceProgressResponse('response-sequence-2', 'request-2'); + const isConnected = Reflect.get(controller, '_isConnected') as { set(value: boolean, tx: undefined): void }; + const watchVoiceProgress = Reflect.get(controller, '_watchVoiceProgress') as (resource: URI, response: IChatResponseModel) => void; + const handleAck = Reflect.get(controller, '_handleNarrationAck') as (event: IVoiceNarrationAck) => void; + + isConnected.set(true, undefined); + controller.setActiveSessionShown(sessionResource); + watchVoiceProgress.call(controller, sessionResource, first.response); + first.parts.push({ kind: 'voiceProgress', id: 'editing', value: 'Updating the first request.' }); + first.changeEmitter.fire({ reason: 'other' }); + clock.tick(5_000); + handleAck.call(controller, { + narrationId: voiceClientService.requests[0].narrationId, + codingSessionId: sessionResource.toString(), + disposition: 'suppressed', + }); + first.state.isComplete = true; + first.changeEmitter.fire({ reason: 'other' }); + + watchVoiceProgress.call(controller, sessionResource, second.response); + second.parts.push({ kind: 'voiceProgress', id: 'validating', value: 'Validating the second request.' }); + second.changeEmitter.fire({ reason: 'other' }); + clock.tick(5_000); + + assert.deepStrictEqual(voiceClientService.requests.map(request => request.checkpoint), [ + { requestId: 'request-1', checkpointId: 'editing', sequence: 1 }, + { requestId: 'request-2', checkpointId: 'validating', sequence: 1 }, + ]); + }); + + test('first-and-final empty checkpoint clears without acknowledging playback', async () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionId = 'chat-session:/checkpoint-empty-final'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const narrationId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: '', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: narrationId, + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + narrationKind: 'checkpoint', + playbackId: 'playback-empty', + }); + + assert.deepStrictEqual({ + pending: [...(Reflect.get(controller, '_pendingSolicitedNarrations') as Map<string, unknown>).keys()], + deferred: [...(Reflect.get(controller, '_deferredNarrations') as Map<string, unknown>).keys()], + playbackCompletions: voiceClientService.playbackCompletions, + }, { + pending: [], + deferred: [], + playbackCompletions: [], + }); + }); + + test('empty checkpoint terminal without playback id clears without acknowledgement', async () => { + const voiceClientService = new TestVoiceClientService(); + const controller = createController(voiceClientService); + const sessionId = 'chat-session:/checkpoint-empty-final-no-playback'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const narrationId = voiceClientService.requests[0].narrationId; + voiceClientService.fireAudioResponse({ + audio: '', + isFirstChunk: true, + isFinal: true, + codingSessionId: sessionId, + responseId: narrationId, + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + narrationKind: 'checkpoint', + }); + + assert.deepStrictEqual({ + pending: [...(Reflect.get(controller, '_pendingSolicitedNarrations') as Map<string, unknown>).keys()], + playbackCompletions: voiceClientService.playbackCompletions, + }, { + pending: [], + playbackCompletions: [], + }); + }); + + test('checkpoint audio prefix followed by empty failure final acknowledges after playback drains', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + const sessionId = 'chat-session:/checkpoint-partial-failure'; + const narrate = Reflect.get(controller, '_narrate') as (sessionId: string, kind: VoiceNarrationKind, text: string, reuseId?: string, checkpoint?: IVoiceCheckpointNarrationMetadata) => boolean; + await controller.connect(mainWindow); + controller.setActiveSessionShown(URI.parse(sessionId)); + + narrate.call(controller, sessionId, 'checkpoint', 'Updating the code.', undefined, { + requestId: 'request-1', + checkpointId: 'editing', + sequence: 1, + }); + const narrationId = voiceClientService.requests[0].narrationId; + const correlation = { + codingSessionId: sessionId, + responseId: narrationId, + requestId: 'request-1', + checkpointId: 'editing' as const, + sequence: 1, + narrationKind: 'checkpoint' as const, + playbackId: 'playback-partial', + }; + voiceClientService.fireAudioResponse({ + ...correlation, + audio: 'checkpoint-prefix', + isFirstChunk: true, + isFinal: false, + }); + voiceClientService.fireAudioResponse({ + ...correlation, + audio: '', + isFirstChunk: false, + isFinal: true, + }); + + assert.deepStrictEqual(voiceClientService.playbackCompletions, []); + ttsPlaybackService.stopPlayback(); + assert.deepStrictEqual(voiceClientService.playbackCompletions, [{ + sessionId, + narrationId, + playbackId: 'playback-partial', + }]); + }); + test('explicit disconnect clears routing target and pending confirmations and the tracker cannot repopulate them before reconnect', () => { const voiceClientService = new TestVoiceClientService(); const chatService = new ControllableChatService(); @@ -642,7 +3132,8 @@ suite('VoiceSessionController', () => { const info = getAgentStateInfo.call(controller, model); assert.strictEqual(info.state, 'waiting_for_confirmation'); - assert.strictEqual(info.detail, 'questions: Which region?'); + assert.ok(info.detail?.includes('Which region?')); + assert.ok(!info.detail?.includes('Which tier?')); assert.deepStrictEqual(buildPendingPayload.call(controller, model)?.questions?.map(question => question.title), ['Which region?']); }); @@ -721,7 +3212,7 @@ suite('VoiceSessionController', () => { const model = pendingPartsModel([approval, form], 'req-1', 'Run command?'); assert.strictEqual(buildPendingPayload.call(controller, model)?.type, 'approval'); - assert.strictEqual(getAgentStateInfo.call(controller, model).detail, 'command: docker push myapp:latest'); + assert.ok(getAgentStateInfo.call(controller, model).detail?.includes('Run a command')); }); test('an older confirmation suppresses a newer form payload but still speaks', () => { @@ -735,7 +3226,7 @@ suite('VoiceSessionController', () => { const model = pendingPartsModel([confirmation, form], 'req-1', 'Delete the branch?'); assert.strictEqual(buildPendingPayload.call(controller, model), undefined); - assert.strictEqual(getAgentStateInfo.call(controller, model).detail, 'Delete the branch?'); + assert.ok(getAgentStateInfo.call(controller, model).detail?.includes('Delete the branch?')); }); test('a newer form answered by mouse leaves the focused form untouched', () => { @@ -750,7 +3241,9 @@ suite('VoiceSessionController', () => { const model = pendingPartsModel([older, newerAnswered], 'req-1', 'Answer questions to continue...'); assert.deepStrictEqual(buildPendingPayload.call(controller, model)?.questions?.map(question => question.id), ['region']); - assert.strictEqual(getAgentStateInfo.call(controller, model).detail, 'questions: Which region?'); + const detail = getAgentStateInfo.call(controller, model).detail; + assert.ok(detail?.includes('Which region?')); + assert.ok(!detail?.includes('Which tier?')); }); test('fatal disconnect clears routing target and pending confirmations and the tracker cannot repopulate them before reconnect', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceToolDispatchService.test.ts b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceToolDispatchService.test.ts index 83c82feb83c..e17ba5ba5e2 100644 --- a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceToolDispatchService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceToolDispatchService.test.ts @@ -4,16 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { observableValue } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; import { mock } from '../../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IAgentSessionsModel } from '../../../browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../browser/agentSessions/agentSessionsService.js'; import { VoiceToolDispatchService } from '../../../browser/voiceClient/voiceToolDispatchService.js'; -import { IChatQuestionAnswers, IChatService } from '../../../common/chatService/chatService.js'; +import { IChatQuestionAnswers, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../../common/chatService/chatService.js'; import { IChatModel } from '../../../common/model/chatModel.js'; +import { ChatPlanReviewData } from '../../../common/model/chatProgressTypes/chatPlanReviewData.js'; import { ChatQuestionCarouselData } from '../../../common/model/chatProgressTypes/chatQuestionCarouselData.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; +import { AskQuestionsToolId } from '../../../common/tools/builtinTools/askQuestionsTool.js'; import { derivePendingId, IVoiceToolCall } from '../../../common/voiceClient/voiceClientService.js'; suite('VoiceToolDispatchService - respondToSession', () => { @@ -120,6 +123,75 @@ suite('VoiceToolDispatchService - respondToSession', () => { assert.strictEqual(part.isUsed, undefined); }); + test('an approval spoken at the ask-questions tool is refused rather than applied', async () => { + const confirmations: ToolConfirmKind[] = []; + const part = new class extends mock<IChatToolInvocation>() { + override readonly kind = 'toolInvocation' as const; + override readonly toolId = AskQuestionsToolId; + override readonly state = observableValue<IChatToolInvocation.State>('state', { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: { questions: [{ question: 'Which region?', options: [{ label: 'West US' }] }] }, + confirmationMessages: { + title: 'Answer questions?', + message: 'The questionnaire is open.', + }, + confirm: reason => confirmations.push(reason.type), + }); + }(); + + const result = await serviceFor(part).respondToSession(approvalCall(part, 'approve')); + + assert.deepStrictEqual({ result, confirmations }, { + result: { ok: false, reason: 'unsupported' }, + confirmations: [], + }); + }); + + test('tool and plan confirmations remain voice-approvable', async () => { + const confirmations: ToolConfirmKind[] = []; + const tool = new class extends mock<IChatToolInvocation>() { + override readonly kind = 'toolInvocation' as const; + override readonly toolId = 'testTool'; + override readonly state = observableValue<IChatToolInvocation.State>('state', { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: {}, + confirmationMessages: { + title: 'Run the build?', + message: 'Runs the visible build task.', + }, + confirm: reason => confirmations.push(reason.type), + }); + }(); + const plan = new ChatPlanReviewData('Review plan', 'Plan body', [ + { id: 'implement', label: 'Implement Plan', default: true }, + ], true); + + const toolResult = await serviceFor(tool).respondToSession(approvalCall(tool, 'approve')); + const planResult = await serviceFor(plan).respondToSession(approvalCall(plan, 'approve')); + + assert.deepStrictEqual({ + toolResult, + confirmations, + planResult, + planData: plan.data, + planCompletion: await plan.completion.p, + }, { + toolResult: { ok: true }, + confirmations: [ToolConfirmKind.UserAction], + planResult: { ok: true }, + planData: { + action: 'Implement Plan', + actionId: 'implement', + rejected: false, + }, + planCompletion: { + action: 'Implement Plan', + actionId: 'implement', + rejected: false, + }, + }); + }); + test('a skip is refused when the form forbids it', async () => { const part = carousel(); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts index 5d729a4840b..215fd253afe 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -237,6 +237,26 @@ suite('ChatService', () => { }); ensureNoDisposablesAreLeakedInTestSuite(); + test('propagates Agents Voice Mode input to the participant request', async () => { + const captured = new DeferredPromise<boolean | undefined>(); + testDisposables.add(chatAgentService.registerAgent('voiceAgent', getAgentData('voiceAgent'))); + testDisposables.add(chatAgentService.registerAgentImplementation('voiceAgent', { + async invoke(request) { + captured.complete(request.isVoiceModeInput); + return {}; + }, + })); + const service = createChatService(); + const model = startSessionModel(service).object; + + await service.sendRequest(model.sessionResource, 'voice request', { + agentId: 'voiceAgent', + isVoiceModeInput: true, + }); + + assert.strictEqual(await captured.p, true); + }); + test('slash commands can share ids across non-overlapping session types', async () => { const slashCommandService = testDisposables.add(instantiationService.createInstance(ChatSlashCommandService)); const executions: string[] = []; diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts index 07f01b361cb..4c0d14c53e8 100644 --- a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts @@ -8,6 +8,7 @@ import * as sinon from 'sinon'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { observableValue } from '../../../../../../base/common/observable.js'; +import { hasKey } from '../../../../../../base/common/types.js'; import { URI } from '../../../../../../base/common/uri.js'; import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; @@ -26,7 +27,7 @@ import { TestExtensionService, TestStorageService } from '../../../../../test/co import { CellUri } from '../../../../notebook/common/notebookCommon.js'; import { IChatRequestImplicitVariableEntry, IChatRequestStringVariableEntry, IChatRequestFileEntry, StringChatContextValue } from '../../../common/attachments/chatVariableEntries.js'; import { ChatAgentService, IChatAgentService } from '../../../common/participants/chatAgents.js'; -import { ChatModel, ChatRequestModel, ChatResponseResource, IChatRequestModeInfo, IExportableChatData, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, ISerializableChatModelInputState, isExportableSessionData, isSerializableSessionData, normalizeSerializableChatData, Response, serializeSendOptions } from '../../../common/model/chatModel.js'; +import { ChatModel, ChatRequestModel, ChatResponseResource, IChatRequestModeInfo, IExportableChatData, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, ISerializableChatModelInputState, isExportableSessionData, isSerializableSessionData, normalizeSerializableChatData, Response, serializeSendOptions, toChatHistoryContent } from '../../../common/model/chatModel.js'; import { ChatToolInvocation } from '../../../common/model/chatProgressTypes/chatToolInvocation.js'; import { ChatRequestTextPart } from '../../../common/requestParser/chatParserTypes.js'; import { ChatRequestQueueKind, IChatService, IChatTerminalToolInvocationData, IChatToolInvocation, ResponseModelState } from '../../../common/chatService/chatService.js'; @@ -211,6 +212,31 @@ suite('ChatModel', () => { }); }); + test('voice progress is live-only response metadata', () => { + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); + const text = 'hello'; + const request = model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); + + model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('Before ') }); + model.acceptResponseProgress(request, { kind: 'voiceProgress', id: 'investigating', value: 'Investigating the relevant code.' }); + model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('after') }); + + const response = request.response!.response; + assert.deepStrictEqual({ + responseKinds: response.value.map(part => part.kind), + historyKinds: toChatHistoryContent(response.value).map(part => part.kind), + markdown: response.getMarkdown(), + copyText: response.toString(), + persistedKinds: model.toExport().requests[0].response?.map(part => hasKey(part, { kind: true }) ? part.kind : 'markdown'), + }, { + responseKinds: ['markdownContent', 'voiceProgress', 'markdownContent'], + historyKinds: ['markdownContent', 'markdownContent'], + markdown: 'Before after', + copyText: 'Before after', + persistedKinds: ['markdown', 'markdown'], + }); + }); + test('a refinement of the same model call updates usage without recounting its tokens', () => { const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); const text = 'hello'; @@ -1819,16 +1845,23 @@ suite('ChatModel - Pending Requests', () => { suite('serializeSendOptions', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('preserves userSelectedModelConfiguration so per-editor config survives persist/restore (issue #320393)', () => { + test('preserves request-scoped options through persist/restore', () => { // A pending/queued request is serialized and later restored (e.g. window // reload). The editor-scoped model configuration must round-trip, otherwise // the restored request falls back to the profile-global value. const serialized = serializeSendOptions({ userSelectedModelId: 'copilot/gpt', userSelectedModelConfiguration: { thinkingEffort: 'high', contextSize: 2000 }, + isVoiceModeInput: true, }); - assert.deepStrictEqual(serialized.userSelectedModelConfiguration, { thinkingEffort: 'high', contextSize: 2000 }); + assert.deepStrictEqual({ + modelConfiguration: serialized.userSelectedModelConfiguration, + isVoiceModeInput: serialized.isVoiceModeInput, + }, { + modelConfiguration: { thinkingEffort: 'high', contextSize: 2000 }, + isVoiceModeInput: true, + }); }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts b/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts index 8229a6ae779..ae6bafb092b 100644 --- a/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts @@ -18,6 +18,16 @@ function content(str: string): IChatMarkdownContent { suite('Annotations', function () { ensureNoDisposablesAreLeakedInTestSuite(); + test('voice progress is not renderable', () => { + assert.deepStrictEqual( + annotateSpecialMarkdownContent([ + { kind: 'voiceProgress', id: 'investigating', value: 'Investigating the relevant code.' }, + content('Visible response'), + ]), + [content('Visible response')] + ); + }); + suite('extractVulnerabilitiesFromText', () => { test('single line', async () => { const before = 'some code '; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index f0fad8b9533..85f68a55699 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -126,6 +126,11 @@ declare module 'vscode' { */ readonly hasHooksEnabled: boolean; + /** + * Whether this request was submitted through Agents Voice Mode. + */ + readonly isVoiceModeInput?: boolean; + /** * When true, this request was initiated by the system (e.g. a terminal * command completion notification) rather than by the user typing a @@ -135,6 +140,41 @@ declare module 'vscode' { readonly isSystemInitiated?: boolean; } + /** + * A transient progress update intended for Voice Mode narration. + */ + export type ChatResponseVoiceProgressStage = 'investigating' | 'planning' | 'editing' | 'validating' | 'recovering'; + + export class ChatResponseVoiceProgressPart { + /** + * A stable identifier used to de-duplicate the progress update. + */ + readonly id: ChatResponseVoiceProgressStage; + /** + * The concise text to narrate. + */ + readonly value: string; + /** + * Creates a Voice Mode progress update. + * @param id A stable identifier used to de-duplicate the update. + * @param value The concise text to narrate. + */ + constructor(id: ChatResponseVoiceProgressStage, value: string); + } + + export interface ExtendedChatResponseParts { + ChatResponseVoiceProgressPart: ChatResponseVoiceProgressPart; + } + + export interface ChatResponseStream { + /** + * Reports transient progress for Voice Mode narration. + * @param id A stable identifier used to de-duplicate the update. + * @param value The concise text to narrate. + */ + voiceProgress(id: ChatResponseVoiceProgressStage, value: string): void; + } + export enum ChatRequestEditedFileEventKind { Keep = 1, Undo = 2, From 95743110acf8cc11ce8ef42d68512e6d0230dd3e Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:30:17 -0700 Subject: [PATCH 76/86] Browser: Element Commenting (#328316) * Browser: Element Commenting * feedback --- .../accessibility/browser/accessibleView.ts | 1 + .../browser/menuEntryActionViewItem.ts | 50 +- src/vs/platform/actions/common/actions.ts | 5 + .../browserView/common/browserView.ts | 62 +- .../electron-browser/preload-browserView.ts | 1078 ++++++++++++++++- .../browserView/electron-main/browserView.ts | 2 +- .../browserViewFrameInspector.ts | 107 +- .../electron-main/browserViewInspector.ts | 148 ++- .../electron-main/browserViewMainService.ts | 22 +- .../browser/accessibilityConfiguration.ts | 7 +- .../contrib/browserView/common/browserView.ts | 34 +- .../browserViewWorkbenchService.ts | 17 +- .../features/browserEditorChatFeatures.ts | 422 ++++++- 13 files changed, 1795 insertions(+), 160 deletions(-) diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts index 0c35df41341..746a18d19b2 100644 --- a/src/vs/platform/accessibility/browser/accessibleView.ts +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -50,6 +50,7 @@ export const enum AccessibleViewProviderId { SessionsChanges = 'sessionsChanges', Survey = 'survey', Automations = 'automations', + BrowserElementCommenting = 'browserElementCommenting', } export const enum AccessibleViewType { diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index ab5ff348a98..e4c87d04312 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -23,6 +23,7 @@ import { localize } from '../../../nls.js'; import { IAccessibilityService } from '../../accessibility/common/accessibility.js'; import { ICommandAction, isICommandActionToggleInfo } from '../../action/common/action.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { ICommandService } from '../../commands/common/commands.js'; import { IContextKeyService } from '../../contextkey/common/contextkey.js'; import { IContextMenuService, IContextViewService } from '../../contextview/browser/contextView.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; @@ -425,6 +426,7 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { export interface IDropdownWithDefaultActionViewItemOptions extends IDropdownMenuActionViewItemOptions { renderKeybindingWithDefaultActionLabel?: boolean; togglePrimaryAction?: boolean; + primaryActionIds?: readonly string[]; } export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { @@ -448,7 +450,8 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { @IContextMenuService protected _contextMenuService: IContextMenuService, @IMenuService protected _menuService: IMenuService, @IInstantiationService protected _instaService: IInstantiationService, - @IStorageService protected _storageService: IStorageService + @IStorageService protected _storageService: IStorageService, + @ICommandService protected _commandService: ICommandService, ) { super(null, submenuAction); this._options = options; @@ -458,10 +461,10 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { let defaultAction: IAction | undefined; const defaultActionId = options?.togglePrimaryAction ? _storageService.get(this._storageKey, StorageScope.WORKSPACE) : undefined; if (defaultActionId) { - defaultAction = submenuAction.actions.find(a => defaultActionId === a.id); + defaultAction = submenuAction.actions.find(a => defaultActionId === a.id && this._canBePrimaryAction(a)); } if (!defaultAction) { - defaultAction = submenuAction.actions[0]; + defaultAction = submenuAction.actions.find(action => this._canBePrimaryAction(action)) ?? submenuAction.actions[0]; } this._defaultAction = this._defaultActionDisposables.add(this._instaService.createInstance(MenuEntryActionViewItem, <MenuItemAction>defaultAction, { keybinding: this._getDefaultActionKeybindingLabel(defaultAction), hoverDelegate: options?.hoverDelegate })); @@ -481,16 +484,31 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { } private registerTogglePrimaryActionListener(): void { - this._primaryActionListener.value = this._dropdown.actionRunner.onDidRun((e: IRunEvent) => { - if (e.action instanceof MenuItemAction) { - this.update(e.action); - } - }); + this._primaryActionListener.value = this._options?.primaryActionIds?.length + ? this._commandService.onDidExecuteCommand(event => { + const action = (<SubmenuItemAction>this._action).actions.find(action => action.id === event.commandId); + if (action instanceof MenuItemAction && this._canBePrimaryAction(action)) { + this.update(action); + } + }) + : this._dropdown.actionRunner.onDidRun((e: IRunEvent) => { + if (e.action instanceof MenuItemAction) { + this.update(e.action); + } + }); } private update(lastAction: MenuItemAction): void { + if (!this._canBePrimaryAction(lastAction)) { + return; + } if (this._options?.togglePrimaryAction) { - this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); + if (this._storageService.get(this._storageKey, StorageScope.WORKSPACE) !== lastAction.id) { + this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + } + if (this._defaultAction.action.id === lastAction.id) { + return; } this._defaultActionDisposables.clear(); @@ -506,6 +524,10 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { } } + private _canBePrimaryAction(action: IAction): boolean { + return !this._options?.primaryActionIds?.length || this._options.primaryActionIds.includes(action.id); + } + private _getDefaultActionKeybindingLabel(defaultAction: IAction) { let defaultActionKeybinding: string | undefined; if (this._options?.renderKeybindingWithDefaultActionLabel) { @@ -527,15 +549,10 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { super.actionRunner = actionRunner; this._defaultAction.actionRunner = actionRunner; - // When togglePrimaryAction is enabled, keep the dropdown's private - // action runner so that the onDidRun listener only fires for actions - // originating from the dropdown, not from unrelated toolbar buttons. - if (!this._options?.togglePrimaryAction) { + // Without an allowlist, retain the private runner so only dropdown executions become primary. + if (!this._options?.togglePrimaryAction || this._options.primaryActionIds?.length) { this._dropdown.actionRunner = actionRunner; } - if (this._primaryActionListener.value) { - this.registerTogglePrimaryActionListener(); - } } override get actionRunner(): IActionRunner { @@ -635,6 +652,7 @@ export function createActionViewItem(instaService: IInstantiationService, action return instaService.createInstance(DropdownWithDefaultActionViewItem, action, { ...options, togglePrimaryAction: typeof action.item.isSplitButton !== 'boolean' ? action.item.isSplitButton.togglePrimaryAction : false, + primaryActionIds: typeof action.item.isSplitButton !== 'boolean' ? action.item.isSplitButton.primaryActionIds : undefined, }); } else { return instaService.createInstance(SubmenuEntryActionViewItem, action, options); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c775a60c48a..7d9168a21a8 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -51,6 +51,11 @@ export interface ISubmenuItem { * on the action that was last run. */ togglePrimaryAction: true; + /** + * Restricts which submenu commands can become the primary action. + * Running an eligible command outside the submenu also updates the primary action. + */ + primaryActionIds?: readonly string[]; }; } diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index c9488802dbe..88f8969d37c 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -42,6 +42,7 @@ export enum BrowserViewCommandId { // Chat actions AddElementToChat = `${commandPrefix}.addElementToChat`, + AddElementCommentToChat = `${commandPrefix}.addElementCommentToChat`, AddConsoleLogsToChat = `${commandPrefix}.addConsoleLogsToChat`, AddScreenshotToChat = `${commandPrefix}.addScreenshotToChat`, AddAreaScreenshotToChat = `${commandPrefix}.addAreaScreenshotToChat`, @@ -68,12 +69,26 @@ export interface IElementAncestor { readonly classNames?: string[]; } +export enum BrowserElementSelectionMode { + Select = 'select', + Comment = 'comment' +} + export interface IBrowserElementSelectionOptions { readonly highlightFocusedElement?: boolean; + readonly continuous?: boolean; + readonly mode?: BrowserElementSelectionMode; +} + +export interface IBrowserElementSelectionState { + readonly active: boolean; + readonly options: IBrowserElementSelectionOptions; } export interface IElementData { readonly url?: string; + readonly elementId?: string; + readonly comment?: string; readonly outerHTML: string; readonly computedStyle: string; readonly bounds: { readonly x: number; readonly y: number; readonly width: number; readonly height: number }; @@ -84,6 +99,16 @@ export interface IElementData { readonly innerText?: string; } +export interface IBrowserElementComment { + readonly elementId: string; + readonly body: string; +} + +export interface IBrowserElementCommentsUpdate { + readonly comments?: readonly IBrowserElementComment[]; + readonly pendingCommentIdsToDiscard?: readonly string[]; +} + export interface IBrowserViewRect { readonly x: number; readonly y: number; @@ -91,11 +116,31 @@ export interface IBrowserViewRect { readonly height: number; } +export interface IBrowserViewPreloadLocalizedStrings { + readonly addComment: string; + readonly addCommentPlaceholder: string; + readonly commentOnSelectedElement: string; + readonly elementComment: string; + readonly elementCommentWithBody: string; + readonly emptyElementComment: string; + readonly removeComment: string; + readonly removeElementComment: string; +} + export interface IBrowserViewTheme { readonly focusBorder?: string; readonly buttonBackground?: string; readonly buttonForeground?: string; + readonly widgetBackground?: string; + readonly widgetForeground?: string; + readonly widgetBorder?: string; + readonly widgetShadow?: string; + readonly contrastBorder?: string; + readonly descriptionForeground?: string; + readonly inputPlaceholderForeground?: string; + readonly toolbarHoverBackground?: string; readonly font?: string; + readonly reducedMotion?: boolean; } /** @@ -250,7 +295,7 @@ export interface IBrowserViewState { storageKeys: IBrowserViewStorageKeys; permissions: ISerializedBrowserPermissionsSnapshot; browserZoomIndex: number; - isElementSelectionActive: boolean; + elementSelectionState: IBrowserElementSelectionState; isRemoteSession: boolean; isAreaSelectionActive: boolean; device: IBrowserDeviceProfile | undefined; @@ -406,7 +451,8 @@ export interface IBrowserViewService { onDynamicDidFindInPage(id: string): Event<IBrowserViewFindInPageResult>; onDynamicDidClose(id: string): Event<void>; onDynamicDidSelectElement(id: string): Event<IElementData>; - onDynamicDidChangeElementSelectionActive(id: string): Event<boolean>; + onDynamicDidRemoveElementComment(id: string): Event<string>; + onDynamicDidChangeElementSelectionState(id: string): Event<IBrowserElementSelectionState>; onDynamicDidPickArea(id: string): Event<IBrowserViewRect | undefined>; onDynamicDidChangeAreaSelectionActive(id: string): Event<boolean>; onDynamicDidChangeDeviceEmulation(id: string): Event<IBrowserDeviceProfile | undefined>; @@ -628,14 +674,22 @@ export interface IBrowserViewService { /** * Toggle element selection mode in a browser view. * Element selections are delivered via {@link onDynamicDidSelectElement}. - * State changes are delivered via {@link onDynamicDidChangeElementSelectionActive}. + * State changes are delivered via {@link onDynamicDidChangeElementSelectionState}. * * @param id The browser view identifier * @param enabled Whether to enable or disable. Omit to toggle. - * @param options Options used when enabling element selection. + * @param options Options to update while enabling or continuing element selection. */ toggleElementSelection(id: string, enabled?: boolean, options?: IBrowserElementSelectionOptions): Promise<void>; + /** + * Synchronize the element comments displayed in a browser view. + * + * @param id The browser view identifier + * @param update The comment state to synchronize + */ + setElementComments(id: string, update: IBrowserElementCommentsUpdate): Promise<void>; + /** * Toggle drag-to-select area picking on the top frame of a browser view. * The pick result (rectangle, or `undefined` on cancellation) is delivered via diff --git a/src/vs/platform/browserView/electron-browser/preload-browserView.ts b/src/vs/platform/browserView/electron-browser/preload-browserView.ts index b37a28d1328..47c565b7a3e 100644 --- a/src/vs/platform/browserView/electron-browser/preload-browserView.ts +++ b/src/vs/platform/browserView/electron-browser/preload-browserView.ts @@ -7,7 +7,19 @@ /* eslint-disable no-restricted-syntax */ // Only `import type` is allowed in preload scripts — Electron preloads cannot resolve module imports at runtime. -import type { IBrowserElementSelectionOptions, IBrowserViewTheme, IBrowserViewRect } from '../common/browserView.js'; +import type { BrowserElementSelectionMode, IBrowserElementCommentsUpdate, IBrowserElementSelectionOptions, IBrowserViewPreloadLocalizedStrings, IBrowserViewTheme, IBrowserViewRect } from '../common/browserView.js'; + +const commentElementSelectionMode = 'comment' as BrowserElementSelectionMode; +let localizedStrings: IBrowserViewPreloadLocalizedStrings = { + addComment: 'Add Comment', + addCommentPlaceholder: 'Add a comment', + commentOnSelectedElement: 'Comment on selected element', + elementComment: 'Element comment {0}', + elementCommentWithBody: 'Element comment {0}: {1}', + emptyElementComment: 'Empty element comment {0}', + removeComment: 'Remove Comment', + removeElementComment: 'Remove element comment', +}; /** * Preload script for pages loaded in Integrated Browser @@ -124,7 +136,12 @@ function init() { }); const elementPicker = new ElementPicker( - el => ipcRenderer.send('vscode:browserView:elementPicked', track(el)), + (el, comment) => { + const elementId = track(el); + ipcRenderer.send('vscode:browserView:elementPicked', { elementId, comment }); + return elementId; + }, + elementId => ipcRenderer.send('vscode:browserView:elementCommentRemoved', elementId), () => ipcRenderer.send('vscode:browserView:elementPickStopped') ); @@ -145,22 +162,24 @@ function init() { return id; } - let contextMenuTargetRef: WeakRef<Element> | undefined; + let contextMenuTarget: { ref: WeakRef<Element>; anchor: { x: number; y: number } } | undefined; window.addEventListener('contextmenu', (event) => { if (!event.isTrusted) { return; } - - const target = event.target; - if (target instanceof Element) { + const target = elementPicker.resolveContextMenuTarget(event); + if (target) { const els = [target]; const selection = window.getSelection(); if (selection && !selection.isCollapsed) { els.push(selection.anchorNode as Element, selection.focusNode as Element); } - contextMenuTargetRef = new WeakRef(findCommonVisibleAncestor(els) ?? target); + contextMenuTarget = { + ref: new WeakRef(findCommonVisibleAncestor(els) ?? target), + anchor: { x: event.clientX, y: event.clientY } + }; } else { - contextMenuTargetRef = undefined; + contextMenuTarget = undefined; } }, { capture: true }); @@ -169,6 +188,10 @@ function init() { elementPicker.setTheme(theme); areaPicker.setTheme(theme); }); + ipcRenderer.on('vscode:browserView:setLocalizedStrings', (_event: unknown, strings: IBrowserViewPreloadLocalizedStrings) => { + localizedStrings = strings; + elementPicker.updateLocalizedStrings(); + }); ipcRenderer.on('vscode:browserView:startElementPicker', (_event: unknown, options: IBrowserElementSelectionOptions) => { elementPicker.start(options); }); @@ -187,16 +210,25 @@ function init() { elementPicker.highlight(element); } }); + ipcRenderer.on('vscode:browserView:showElementComment', (_event: unknown, { elementId }: { elementId: string }) => { + const element = getElement(elementId); + if (element) { + elementPicker.comment(element, elementId === 'context-menu-target' ? contextMenuTarget?.anchor : undefined); + } + }); ipcRenderer.on('vscode:browserView:hideHighlight', (_event: unknown) => { elementPicker.hideHighlight(); }); + ipcRenderer.on('vscode:browserView:setElementComments', (_event: unknown, update: IBrowserElementCommentsUpdate) => { + elementPicker.updateComments(update); + }); const getElement = (id: string): Element | null => { switch (id) { case 'active': return document.activeElement; case 'context-menu-target': - return contextMenuTargetRef?.deref() ?? null; + return contextMenuTarget?.ref.deref() ?? null; default: return trackedElementsById.get(id)?.deref() ?? null; } @@ -316,25 +348,51 @@ class ElementPicker { private _selectionActive = false; private _continuous = false; + private _commentMode = false; // DOM — created once in the constructor, reused across start/stop cycles. private readonly _shadowHost: HTMLDivElement; + private readonly _commentBackdrop: SVGSVGElement; + private readonly _commentBackdropCutout: SVGRectElement; + private readonly _highlightShape: SVGRectElement; private readonly _highlight: HTMLDivElement; + private readonly _commentPreviewRemoveButton: HTMLButtonElement; + private readonly _overlay: HTMLDivElement; private readonly _label: HTMLDivElement; private readonly _labelSelector: HTMLSpanElement; private readonly _labelClasses: HTMLSpanElement; private readonly _labelDims: HTMLSpanElement; + private readonly _commentPreview: HTMLDivElement; + private readonly _commentPreviewBody: HTMLSpanElement; private readonly _dragbox: HTMLDivElement; + private readonly _commentLayer: HTMLDivElement; + private readonly _commentComposer: HTMLDivElement; + private readonly _commentInput: HTMLTextAreaElement; + private readonly _commentSendButton: HTMLButtonElement; + private readonly _comments = new Map<string, { target: Element; pin: HTMLDivElement; numberElement: HTMLSpanElement; body: string; ordinal: number; offset: { x: number; y: number } }>(); + private readonly _pendingComments = new Map<string, { target: Element; anchor: { x: number; y: number }; body: string }>(); // Interaction state (reset on stop) private _dragStart: { x: number; y: number } | undefined; private _dragStartTarget: Element | undefined; private _highlightTarget: Element | undefined; + private _externalHighlightTarget: Element | undefined; private _focusedTarget: Element | undefined; private _cursorStylesheet: HTMLStyleElement | undefined; + private _dismissedCommentOnPointerDown = false; + private _commentTarget: Element | undefined; + private _commentAnchor: { x: number; y: number } | undefined; + private _commentBackdropTarget: Element | undefined; + private _commentBackdropRequest = 0; + private _commentPreviewElementId: string | undefined; + private _commentPreviewHideTimeout: number | undefined; + private _commentPreviewAnimations: Animation[] = []; + private _commentPreviewCollapsing = false; + private _reducedMotion = false; constructor( - private readonly _onPicked: (element: Element) => void, + private readonly _onPicked: (element: Element, comment?: string) => string, + private readonly _onCommentRemoved: (elementId: string) => void, private readonly _onStopped: () => void ) { // Build the shadow DOM tree once. The host is appended/removed from the @@ -346,15 +404,70 @@ class ElementPicker { root.appendChild(ElementPicker._buildStyle()); this._shadowHost = shadowHost; + const svgNamespace = 'http://www.w3.org/2000/svg'; + const commentBackdrop = document.createElementNS(svgNamespace, 'svg'); + commentBackdrop.classList.add('comment-backdrop'); + const backdropMaskId = `vscode-comment-cutout-${Math.random().toString(36).slice(2)}`; + const backdropDefinitions = document.createElementNS(svgNamespace, 'defs'); + const backdropMask = document.createElementNS(svgNamespace, 'mask'); + backdropMask.id = backdropMaskId; + backdropMask.setAttribute('maskUnits', 'userSpaceOnUse'); + backdropMask.setAttribute('x', '0'); + backdropMask.setAttribute('y', '0'); + backdropMask.setAttribute('width', '100%'); + backdropMask.setAttribute('height', '100%'); + const backdropMaskFill = document.createElementNS(svgNamespace, 'rect'); + backdropMaskFill.setAttribute('width', '100%'); + backdropMaskFill.setAttribute('height', '100%'); + backdropMaskFill.setAttribute('fill', 'white'); + const backdropCutout = document.createElementNS(svgNamespace, 'rect'); + backdropCutout.setAttribute('fill', 'black'); + backdropMask.append(backdropMaskFill, backdropCutout); + backdropDefinitions.appendChild(backdropMask); + const backdropFill = document.createElementNS(svgNamespace, 'rect'); + backdropFill.classList.add('comment-backdrop-fill'); + backdropFill.setAttribute('width', '100%'); + backdropFill.setAttribute('height', '100%'); + backdropFill.setAttribute('mask', `url(#${backdropMaskId})`); + const highlightShape = document.createElementNS(svgNamespace, 'rect'); + highlightShape.classList.add('highlight-shape'); + highlightShape.style.display = 'none'; + commentBackdrop.append(backdropDefinitions, backdropFill, highlightShape); + root.appendChild(commentBackdrop); + this._commentBackdrop = commentBackdrop; + this._commentBackdropCutout = backdropCutout; + this._highlightShape = highlightShape; + const highlight = document.createElement('div'); highlight.className = 'highlight'; highlight.style.display = 'none'; root.appendChild(highlight); this._highlight = highlight; + const commentPreviewRemoveButton = document.createElement('button'); + commentPreviewRemoveButton.className = 'comment-preview-remove'; + commentPreviewRemoveButton.type = 'button'; + const commentPreviewRemoveIcon = document.createElementNS(svgNamespace, 'svg'); + commentPreviewRemoveIcon.setAttribute('viewBox', '0 0 16 16'); + commentPreviewRemoveIcon.setAttribute('fill', 'currentColor'); + commentPreviewRemoveIcon.setAttribute('aria-hidden', 'true'); + const commentPreviewRemoveIconPath = document.createElementNS(svgNamespace, 'path'); + commentPreviewRemoveIconPath.setAttribute('d', 'M3.854 3.146a.5.5 0 0 0-.708.708L7.293 8l-4.147 4.146a.5.5 0 0 0 .708.708L8 8.707l4.146 4.147a.5.5 0 0 0 .708-.708L8.707 8l4.147-4.146a.5.5 0 0 0-.708-.708L8 7.293 3.854 3.146Z'); + commentPreviewRemoveIcon.appendChild(commentPreviewRemoveIconPath); + commentPreviewRemoveButton.appendChild(commentPreviewRemoveIcon); + commentPreviewRemoveButton.title = localizedStrings.removeComment; + commentPreviewRemoveButton.setAttribute('aria-label', localizedStrings.removeElementComment); + commentPreviewRemoveButton.addEventListener('click', () => { + if (this._commentPreviewElementId) { + this._removeComment(this._commentPreviewElementId); + } + }); + this._commentPreviewRemoveButton = commentPreviewRemoveButton; + const overlay = document.createElement('div'); overlay.className = 'overlay'; root.appendChild(overlay); + this._overlay = overlay; const label = document.createElement('div'); label.className = 'label'; @@ -381,23 +494,104 @@ class ElementPicker { label.appendChild(labelDims); this._labelDims = labelDims; + const commentPreview = document.createElement('div'); + commentPreview.className = 'comment-surface comment-preview'; + commentPreview.style.display = 'none'; + commentPreview.setAttribute('role', 'note'); + const commentPreviewBody = document.createElement('span'); + commentPreviewBody.className = 'comment-preview-body'; + commentPreview.appendChild(commentPreviewBody); + commentPreview.appendChild(commentPreviewRemoveButton); + root.appendChild(commentPreview); + this._commentPreview = commentPreview; + this._commentPreviewBody = commentPreviewBody; + + for (const element of [highlight, label, commentPreview]) { + element.addEventListener('mouseenter', () => this._cancelCommentPreviewHide()); + element.addEventListener('mouseleave', () => this._scheduleCommentPreviewHide()); + element.addEventListener('focusin', () => this._cancelCommentPreviewHide()); + element.addEventListener('focusout', () => this._scheduleCommentPreviewHide()); + } + const dragbox = document.createElement('div'); dragbox.className = 'dragbox'; dragbox.style.display = 'none'; root.appendChild(dragbox); this._dragbox = dragbox; + const commentLayer = document.createElement('div'); + commentLayer.className = 'comment-layer'; + root.appendChild(commentLayer); + this._commentLayer = commentLayer; + + const commentComposer = document.createElement('div'); + commentComposer.className = 'comment-surface comment-composer'; + commentComposer.style.display = 'none'; + commentComposer.setAttribute('role', 'dialog'); + commentComposer.setAttribute('aria-label', localizedStrings.commentOnSelectedElement); + commentComposer.setAttribute('aria-modal', 'true'); + commentLayer.appendChild(commentComposer); + this._commentComposer = commentComposer; + + const commentInput = document.createElement('textarea'); + commentInput.className = 'comment-input'; + commentInput.rows = 1; + commentInput.placeholder = localizedStrings.addCommentPlaceholder; + commentInput.setAttribute('aria-label', localizedStrings.commentOnSelectedElement); + commentInput.addEventListener('input', () => this._layoutCommentInput()); + commentInput.addEventListener('keydown', event => { + if (event.key === 'Enter' && !event.isComposing) { + event.preventDefault(); + this._submitComment(); + } + }); + commentComposer.appendChild(commentInput); + this._commentInput = commentInput; + + const sendButton = document.createElement('button'); + sendButton.className = 'comment-send'; + sendButton.type = 'button'; + const sendButtonIcon = document.createElementNS(svgNamespace, 'svg'); + sendButtonIcon.setAttribute('viewBox', '0 0 16 16'); + sendButtonIcon.setAttribute('fill', 'currentColor'); + sendButtonIcon.setAttribute('aria-hidden', 'true'); + const sendButtonIconPath = document.createElementNS(svgNamespace, 'path'); + sendButtonIconPath.setAttribute('d', 'M8.5 3a.5.5 0 0 0-1 0v4.5H3a.5.5 0 0 0 0 1h4.5V13a.5.5 0 0 0 1 0V8.5H13a.5.5 0 0 0 0-1H8.5V3Z'); + sendButtonIcon.appendChild(sendButtonIconPath); + sendButton.appendChild(sendButtonIcon); + sendButton.title = localizedStrings.addComment; + sendButton.setAttribute('aria-label', localizedStrings.addComment); + sendButton.addEventListener('click', () => this._submitComment()); + commentComposer.appendChild(sendButton); + this._commentSendButton = sendButton; + + commentComposer.addEventListener('keydown', event => { + if (event.key !== 'Tab') { + return; + } + if (event.shiftKey && event.target === commentInput) { + event.preventDefault(); + sendButton.focus(); + } else if (!event.shiftKey && event.target === sendButton) { + event.preventDefault(); + commentInput.focus(); + } + }); + window.addEventListener('scroll', () => this._onScrollOrResize(), { passive: true, capture: true }); window.addEventListener('resize', () => this._onScrollOrResize()); } start(options: IBrowserElementSelectionOptions): boolean { if (this._selectionActive) { + this._updateSelectionOptions(options); return true; } - this._continuous = false; // for now - document.documentElement.appendChild(this._shadowHost); + this._commentMode = options.mode === commentElementSelectionMode; + this._continuous = options.continuous ?? false; + this._ensureMounted(); this._selectionActive = true; + this._overlay.style.display = 'block'; // Inject a stylesheet into the page to override all cursors while element selection is active, // so the cursor always appears as a normal pointer even when over e.g. links. @@ -418,19 +612,36 @@ class ElementPicker { window.addEventListener('blur', this._onWindowBlur); window.addEventListener('keydown', this._onKeyDown, true); - const focusedElement = this._getFocusedElement(); - this._focusedTarget = options.highlightFocusedElement ? focusedElement : undefined; - this._updateHighlight(this._focusedTarget); + if (!this._externalHighlightTarget) { + const focusedElement = this._getFocusedElement(); + this._focusedTarget = options.highlightFocusedElement ? focusedElement : undefined; + this._updateHighlight(this._focusedTarget); + } return true; } + private _updateSelectionOptions(options: IBrowserElementSelectionOptions): void { + const wasCommentMode = this._commentMode; + this._commentMode = options.mode === commentElementSelectionMode; + this._continuous = options.continuous ?? false; + if (wasCommentMode && !this._commentMode && this._commentTarget) { + this._closeCommentComposer(); + } + if (options.highlightFocusedElement && !this._commentTarget && !this._commentPreviewElementId && !this._externalHighlightTarget) { + this._focusedTarget = this._getFocusedElement(); + this._updateHighlight(this._focusedTarget); + } + } + stop(): void { if (!this._selectionActive) { return; } + this._hideActiveCommentPreview(); this._selectionActive = false; - this._shadowHost.remove(); + this._closeCommentComposer(); + this._overlay.style.display = 'none'; this._cursorStylesheet?.remove(); this._cursorStylesheet = undefined; @@ -451,10 +662,15 @@ class ElementPicker { this._dragbox.style.display = 'none'; this._dragStart = undefined; this._dragStartTarget = undefined; + this._dismissedCommentOnPointerDown = false; this._highlightTarget = undefined; this._focusedTarget = undefined; + if (this._externalHighlightTarget) { + this._updateHighlight(this._externalHighlightTarget); + } this._onStopped(); + this._unmountWhenIdle(); } /** @@ -463,6 +679,20 @@ class ElementPicker { */ setTheme(theme: IBrowserViewTheme): void { ElementPicker._applyTheme(this._shadowHost, theme); + this._reducedMotion = theme.reducedMotion ?? false; + this._shadowHost.classList.toggle('reduce-motion', this._reducedMotion); + } + + updateLocalizedStrings(): void { + this._applyLocalizedStrings(); + } + + resolveContextMenuTarget(event: MouseEvent): Element | undefined { + if (this._commentPreviewElementId && event.composedPath().includes(this._shadowHost)) { + this._hideActiveCommentPreview(); + return this._pickElementAt(event.clientX, event.clientY); + } + return event.target instanceof Element ? event.target : undefined; } /** @@ -470,9 +700,9 @@ class ElementPicker { * Mounts the shadow host if not already in the document. */ highlight(element: Element): void { - if (!this._shadowHost.parentNode) { - document.documentElement.appendChild(this._shadowHost); - } + this._ensureMounted(); + this._externalHighlightTarget = element; + this._hideActiveCommentPreview(); this._updateHighlight(element); } @@ -481,10 +711,63 @@ class ElementPicker { * removes the shadow host from the document. */ hideHighlight(): void { - this._updateHighlight(undefined); - if (!this._selectionActive && this._shadowHost.parentNode) { - this._shadowHost.remove(); + this._externalHighlightTarget = undefined; + if (this._commentTarget) { + return; } + this._updateHighlight(undefined); + this._unmountWhenIdle(); + } + + comment(element: Element, anchor?: { x: number; y: number }): void { + this._externalHighlightTarget = undefined; + if (this._selectionActive) { + this.stop(); + } + this.start({ mode: commentElementSelectionMode }); + const bounds = element.getBoundingClientRect(); + this._showCommentComposer(element, anchor ?? { + x: bounds.left + bounds.width / 2, + y: bounds.top + bounds.height / 2 + }); + } + + updateComments(update: IBrowserElementCommentsUpdate): void { + if (update.comments) { + const incoming = new Map(update.comments.map((comment, index) => [comment.elementId, { body: comment.body, ordinal: index + 1 }])); + for (const [elementId, comment] of this._comments) { + const incomingComment = incoming.get(elementId); + if (!incomingComment) { + this._clearCommentPreview(comment.target); + comment.pin.remove(); + this._comments.delete(elementId); + } else { + comment.ordinal = incomingComment.ordinal; + if (incomingComment.body === comment.body) { + continue; + } + comment.body = incomingComment.body; + if (this._commentPreviewElementId === elementId) { + this._setCommentPreviewBody(incomingComment.body); + this._renderHighlight(comment.target); + } + } + } + for (const [elementId, comment] of incoming) { + if (this._comments.has(elementId)) { + continue; + } + const pending = this._pendingComments.get(elementId); + if (pending) { + this._createCommentPin(elementId, pending.target, pending.anchor, comment.body, comment.ordinal); + } + } + } + for (const elementId of update.pendingCommentIdsToDiscard ?? []) { + this._pendingComments.delete(elementId); + } + this._updateCommentPinNumbers(); + this._unmountWhenIdle(); } // --- Event handlers --- @@ -493,6 +776,9 @@ class ElementPicker { if (!this._selectionActive) { return; } + if (this._commentTarget || this._commentPreviewElementId || this._externalHighlightTarget || e.composedPath().includes(this._shadowHost)) { + return; + } e.preventDefault(); e.stopPropagation(); if (!this._dragStart) { @@ -520,7 +806,7 @@ class ElementPicker { }; private _onPointerLeave = (): void => { - if (!this._selectionActive) { + if (!this._selectionActive || this._commentTarget || this._commentPreviewElementId || this._externalHighlightTarget) { return; } if (!this._dragStart) { @@ -532,6 +818,17 @@ class ElementPicker { if (!this._selectionActive) { return; } + this._dismissedCommentOnPointerDown = false; + if (e.composedPath().includes(this._shadowHost)) { + return; + } + if (this._commentTarget) { + this._dismissedCommentOnPointerDown = true; + this._finishCommentInteraction(); + e.preventDefault(); + e.stopPropagation(); + return; + } this._dragStart = { x: e.clientX, y: e.clientY }; this._dragStartTarget = this._pickElementAt(e.clientX, e.clientY); if (this._cursorStylesheet) { @@ -545,6 +842,14 @@ class ElementPicker { if (!this._selectionActive) { return; } + if (this._dismissedCommentOnPointerDown) { + e.preventDefault(); + e.stopPropagation(); + return; + } + if (e.composedPath().includes(this._shadowHost)) { + return; + } if (!this._dragStart) { return; } @@ -561,7 +866,7 @@ class ElementPicker { const target = this._dragStartTarget ?? this._pickElementAt(e.clientX, e.clientY); this._dragStartTarget = undefined; if (target) { - this._commit(target); + this._commit(target, { x: e.clientX, y: e.clientY }); } } else { // Drag → pick the deepest common ancestor of the region. @@ -574,7 +879,7 @@ class ElementPicker { const top = Math.min(start.y, e.clientY); const ancestor = this._pickRegionAncestor({ x: left, y: top, width: dx, height: dy }); if (ancestor) { - this._commit(ancestor); + this._commit(ancestor, { x: e.clientX, y: e.clientY }); } } e.preventDefault(); @@ -585,12 +890,24 @@ class ElementPicker { if (!this._selectionActive) { return; } + if (this._dismissedCommentOnPointerDown) { + this._dismissedCommentOnPointerDown = false; + e.preventDefault(); + e.stopPropagation(); + return; + } + if (e.composedPath().includes(this._shadowHost)) { + return; + } e.preventDefault(); e.stopPropagation(); }; - private _onFocusIn = (): void => { - if (!this._selectionActive) { + private _onFocusIn = (event: FocusEvent): void => { + if (!this._selectionActive || this._commentTarget || this._externalHighlightTarget) { + return; + } + if (event.composedPath().includes(this._shadowHost)) { return; } const focusedElement = this._getFocusedElement(); @@ -599,7 +916,7 @@ class ElementPicker { }; private _onWindowBlur = (): void => { - if (!this._selectionActive) { + if (!this._selectionActive || this._commentTarget || this._externalHighlightTarget) { return; } this._focusedTarget = undefined; @@ -611,6 +928,14 @@ class ElementPicker { return; } if (e.key === 'Escape') { + if (this._commentTarget) { + const target = this._commentTarget; + this._finishCommentInteraction(); + this._focusCommentTarget(target); + e.preventDefault(); + e.stopPropagation(); + return; + } this.stop(); e.preventDefault(); e.stopPropagation(); @@ -625,9 +950,19 @@ class ElementPicker { }; private _onScrollOrResize(): void { + if (this._commentPreviewCollapsing) { + this._hideActiveCommentPreview(); + } + this._cancelCommentPreviewAnimations(); if (this._highlightTarget) { this._renderHighlight(this._highlightTarget); } + if (this._commentBackdropTarget) { + this._layoutCommentBackdrop(this._commentBackdropTarget); + } + for (const comment of this._comments.values()) { + this._layoutCommentPin(comment); + } } // --- Picking helpers --- @@ -694,6 +1029,8 @@ class ElementPicker { const scrollX = window.scrollX || 0; const scrollY = window.scrollY || 0; const viewportHeight = window.innerHeight; + const viewportWidth = document.documentElement.clientWidth; + const visibleRect = this._getVisibleTargetBounds(rect); const labelHeight = 22; // label height (20) + 2px gap above the box. // Highlight box is in *page* coordinates so it scrolls with the document. @@ -702,6 +1039,12 @@ class ElementPicker { highlight.style.top = `${rect.top + scrollY}px`; highlight.style.width = `${rect.width}px`; highlight.style.height = `${rect.height}px`; + this._highlightShape.style.display = 'block'; + this._highlightShape.setAttribute('x', `${visibleRect.x}`); + this._highlightShape.setAttribute('y', `${visibleRect.y}`); + this._highlightShape.setAttribute('width', `${visibleRect.width}`); + this._highlightShape.setAttribute('height', `${visibleRect.height}`); + this._highlightShape.setAttribute('rx', '2'); // Label is in *viewport* coordinates and sticky-clamped to the viewport. const tagName = String(target.tagName || '').toLowerCase(); @@ -717,7 +1060,6 @@ class ElementPicker { const labelTop = Math.max(0, Math.min(viewportHeight - labelHeight, idealTop)); // Use clientWidth (excludes scrollbar) rather than innerWidth so the // label doesn't extend behind the scrollbar on Windows/Linux. - const viewportWidth = document.documentElement.clientWidth; // Position label at the element's left edge, but push it left if it // would overflow the viewport. Clamp to 0 so it never goes off-screen. label.style.left = '0'; @@ -726,13 +1068,59 @@ class ElementPicker { const labelLeft = Math.max(0, Math.min(idealLeft, viewportWidth - naturalWidth)); label.style.left = `${labelLeft}px`; label.style.top = `${labelTop}px`; + + let commentSurfaceAbove = false; + for (const surface of [this._commentPreview, this._commentComposer]) { + if (surface.style.display !== 'none') { + commentSurfaceAbove = this._layoutCommentSurface(surface, visibleRect, viewportWidth, viewportHeight) === 'above' || commentSurfaceAbove; + } + } + if (commentSurfaceAbove) { + label.style.top = `${Math.max(0, Math.min(viewportHeight - labelHeight, visibleRect.bottom + 2))}px`; + } + } + + private _getVisibleTargetBounds(rect: DOMRect): DOMRect { + const left = Math.max(0, Math.min(rect.left, window.innerWidth)); + const right = Math.max(left, Math.min(rect.right, window.innerWidth)); + const top = Math.max(0, Math.min(rect.top, window.innerHeight)); + const bottom = Math.max(top, Math.min(rect.bottom, window.innerHeight)); + return new DOMRect(left, top, right - left, bottom - top); + } + + private _layoutCommentSurface(surface: HTMLElement, targetBounds: DOMRect, viewportWidth: number, viewportHeight: number): 'above' | 'below' { + if (surface === this._commentPreview) { + const availableWidth = Math.min(320, viewportWidth - 16); + const maximumWidth = Math.min(Math.max(320, targetBounds.width), availableWidth); + surface.style.width = 'max-content'; + surface.style.minWidth = '0'; + surface.style.maxWidth = `${maximumWidth}px`; + } + const surfaceHeight = surface.offsetHeight; + const belowTop = targetBounds.bottom; + const placement = belowTop + surfaceHeight <= viewportHeight - 8 ? 'below' : 'above'; + const surfaceTop = belowTop + surfaceHeight <= viewportHeight - 8 + ? belowTop + : Math.max(0, targetBounds.top - surfaceHeight); + const surfaceWidth = surface.offsetWidth; + const alignLeft = targetBounds.left + surfaceWidth <= viewportWidth; + const alignment = alignLeft ? 'left' : 'right'; + const surfaceLeft = alignLeft + ? Math.max(0, targetBounds.left) + : Math.max(0, targetBounds.right - surfaceWidth); + surface.dataset.attachmentCorner = `${placement === 'below' ? 'top' : 'bottom'}-${alignment}`; + surface.style.left = `${surfaceLeft}px`; + surface.style.top = `${surfaceTop}px`; + return placement; } private _updateHighlight(target: Element | undefined): void { this._highlightTarget = target; if (!target) { this._highlight.style.display = 'none'; + this._highlightShape.style.display = 'none'; this._label.style.display = 'none'; + this._commentPreview.style.display = 'none'; return; } this._renderHighlight(target); @@ -740,10 +1128,18 @@ class ElementPicker { // --- Commit --- - private _commit(target: Element): void { + private _commit(target: Element, anchor?: { x: number; y: number }): void { if (!this._selectionActive) { return; } + if (this._commentMode) { + const bounds = target.getBoundingClientRect(); + this._showCommentComposer(target, anchor ?? { + x: bounds.left + bounds.width / 2, + y: bounds.top + bounds.height / 2, + }); + return; + } // Wait a frame so any pending event handlers can be completed in the selecting active state. requestAnimationFrame(() => { if (!this._continuous) { @@ -757,6 +1153,413 @@ class ElementPicker { }); } + private _showCommentComposer(target: Element, anchor: { x: number; y: number }): void { + this._externalHighlightTarget = undefined; + this._hideActiveCommentPreview(); + this._commentTarget = target; + this._commentAnchor = { + x: anchor.x + window.scrollX, + y: anchor.y + window.scrollY + }; + this._updateHighlight(target); + this._showCommentBackdrop(target); + this._commentLayer.classList.add('composing'); + this._commentInput.value = ''; + this._commentComposer.style.display = 'flex'; + this._layoutCommentComposer(); + this._layoutCommentInput(); + this._animateCommentHighlight( + new DOMRect(anchor.x - 3, anchor.y - 3, 6, 6), + target, + [this._label, this._commentComposer] + ); + this._commentInput.focus({ preventScroll: true }); + requestAnimationFrame(() => { + if (this._commentTarget === target) { + this._commentInput.focus({ preventScroll: true }); + } + }); + } + + private _closeCommentComposer(): void { + this._commentTarget = undefined; + this._commentAnchor = undefined; + this._hideCommentBackdrop(); + this._commentLayer.classList.remove('composing'); + this._commentComposer.style.display = 'none'; + this._commentInput.value = ''; + this._cancelCommentPreviewAnimations(); + this._updateHighlight(undefined); + } + + private _finishCommentInteraction(): void { + if (this._continuous) { + this._closeCommentComposer(); + } else { + this.stop(); + } + } + + private _submitComment(): void { + const target = this._commentTarget; + const anchor = this._commentAnchor; + if (!target || !anchor) { + return; + } + const body = this._commentInput.value.replace(/\r?\n/g, ' '); + const elementId = this._onPicked(target, body); + this._pendingComments.set(elementId, { target, anchor, body }); + this._finishCommentInteraction(); + this._focusCommentTarget(target); + } + + private _focusCommentTarget(target: Element): void { + if (!target.isConnected || !(target instanceof HTMLElement || target instanceof SVGElement)) { + return; + } + + const hadTabIndex = target.hasAttribute('tabindex'); + if (!hadTabIndex) { + target.tabIndex = -1; + } + target.focus({ preventScroll: true }); + if (!hadTabIndex) { + target.removeAttribute('tabindex'); + } + } + + private _createCommentPin(elementId: string, target: Element, anchor: { x: number; y: number }, body: string, ordinal: number): void { + this._ensureMounted(); + const existing = this._comments.get(elementId); + if (existing) { + this._clearCommentPreview(existing.target); + } + existing?.pin.remove(); + this._pendingComments.delete(elementId); + const rect = target.getBoundingClientRect(); + const offset = { + x: anchor.x - (rect.left + window.scrollX), + y: anchor.y - (rect.top + window.scrollY) + }; + + const pin = document.createElement('div'); + pin.className = 'comment-pin'; + pin.tabIndex = 0; + pin.setAttribute('role', 'note'); + const bubble = document.createElement('span'); + bubble.className = 'comment-pin-bubble'; + const numberElement = document.createElement('span'); + numberElement.className = 'comment-pin-number'; + bubble.appendChild(numberElement); + pin.appendChild(bubble); + + const show = () => { + if (this._commentTarget || this._externalHighlightTarget) { + return; + } + this._showCommentPreview(elementId, target, body); + }; + pin.addEventListener('mouseenter', show); + pin.addEventListener('mouseleave', () => this._scheduleCommentPreviewHide()); + pin.addEventListener('focusin', show); + pin.addEventListener('focusout', () => this._scheduleCommentPreviewHide()); + this._commentLayer.appendChild(pin); + const comment = { target, pin, numberElement, body, ordinal, offset }; + this._comments.set(elementId, comment); + this._updateCommentPinNumbers(); + this._layoutCommentPin(comment); + } + + private _updateCommentPinNumbers(): void { + for (const comment of this._comments.values()) { + const numberLabel = String(comment.ordinal); + comment.numberElement.textContent = numberLabel; + comment.pin.title = comment.body || this._formatLocalizedString(localizedStrings.elementComment, numberLabel); + comment.pin.setAttribute( + 'aria-label', + comment.body + ? this._formatLocalizedString(localizedStrings.elementCommentWithBody, numberLabel, comment.body) + : this._formatLocalizedString(localizedStrings.emptyElementComment, numberLabel) + ); + } + } + + private _applyLocalizedStrings(): void { + this._commentPreviewRemoveButton.title = localizedStrings.removeComment; + this._commentPreviewRemoveButton.setAttribute('aria-label', localizedStrings.removeElementComment); + this._commentComposer.setAttribute('aria-label', localizedStrings.commentOnSelectedElement); + this._commentInput.placeholder = localizedStrings.addCommentPlaceholder; + this._commentInput.setAttribute('aria-label', localizedStrings.commentOnSelectedElement); + this._commentSendButton.title = localizedStrings.addComment; + this._commentSendButton.setAttribute('aria-label', localizedStrings.addComment); + this._updateCommentPinNumbers(); + } + + private _formatLocalizedString(template: string, ...values: readonly string[]): string { + return template.replace(/\{(\d+)\}/g, (_, index) => values[Number(index)] ?? ''); + } + + private _layoutCommentPin(comment: { target: Element; pin: HTMLDivElement; offset: { x: number; y: number } }): void { + const rect = comment.target.getBoundingClientRect(); + const x = rect.left + window.scrollX + comment.offset.x; + const y = rect.top + window.scrollY + comment.offset.y; + const scrollingElement = document.scrollingElement ?? document.documentElement; + const halfWidth = comment.pin.offsetWidth / 2; + const halfHeight = comment.pin.offsetHeight / 2; + const clampedX = Math.max(halfWidth, Math.min(x, scrollingElement.scrollWidth - halfWidth)); + const clampedY = Math.max(halfHeight, Math.min(y, scrollingElement.scrollHeight - halfHeight)); + comment.pin.style.left = `${clampedX}px`; + comment.pin.style.top = `${clampedY}px`; + } + + private _showCommentPreview(elementId: string, target: Element, fallbackBody: string): void { + if (this._commentPreviewCollapsing) { + return; + } + if (this._commentPreviewElementId === elementId) { + this._cancelCommentPreviewHide(); + return; + } + this._hideActiveCommentPreview(); + this._commentPreviewElementId = elementId; + const comment = this._comments.get(elementId); + const pinBounds = comment ? this._getCommentPinPointBounds(comment.pin) : undefined; + if (comment) { + comment.pin.classList.add('previewing'); + comment.pin.after(this._commentPreview); + } + const body = comment?.body ?? fallbackBody; + this._setCommentPreviewBody(body); + this._shadowHost.classList.add('comment-preview-active'); + this._updateHighlight(target); + this._showCommentBackdrop(target); + if (pinBounds) { + this._animateCommentHighlight( + pinBounds, + target, + [this._label, this._commentPreview] + ); + } + } + + private _setCommentPreviewBody(body: string): void { + this._commentPreviewBody.textContent = body; + this._commentPreview.title = body; + this._commentPreview.classList.toggle('empty', !body); + this._commentPreview.style.display = 'flex'; + } + + private _getCommentPinPointBounds(pin: HTMLElement): DOMRect { + const pinBounds = pin.getBoundingClientRect(); + return new DOMRect(pinBounds.left + 8, pinBounds.top + 8, 6, 6); + } + + private _animateCommentHighlight(pinBounds: DOMRect, target: Element, supportingElements: readonly HTMLElement[], collapsing = false): Animation | undefined { + if (this._reducedMotion) { + return undefined; + } + const targetBounds = this._getVisibleTargetBounds(target.getBoundingClientRect()); + const duration = 180; + const easing = 'cubic-bezier(0.2, 0, 0, 1)'; + const pinKeyframe: Keyframe = { + x: `${pinBounds.left}px`, + y: `${pinBounds.top}px`, + width: `${pinBounds.width}px`, + height: `${pinBounds.height}px`, + rx: `${pinBounds.width / 2}px` + }; + const targetKeyframe: Keyframe = { + x: `${targetBounds.left}px`, + y: `${targetBounds.top}px`, + width: `${targetBounds.width}px`, + height: `${targetBounds.height}px`, + rx: '2px' + }; + const highlightAnimation = this._highlightShape.animate( + collapsing ? [targetKeyframe, pinKeyframe] : [pinKeyframe, targetKeyframe], + { duration, easing, fill: 'forwards' } + ); + this._commentPreviewAnimations.push(highlightAnimation); + this._commentPreviewAnimations.push(this._commentBackdropCutout.animate( + collapsing ? [targetKeyframe, pinKeyframe] : [pinKeyframe, targetKeyframe], + { duration, easing, fill: 'forwards' } + )); + + for (const element of supportingElements) { + if (element.style.display === 'none') { + continue; + } + const hiddenKeyframe: Keyframe = { opacity: 0, transform: 'translateY(-4px)' }; + const keyframes = collapsing + ? [{ opacity: 1, transform: 'translateY(0)' }, { ...hiddenKeyframe, offset: 0.55 }, hiddenKeyframe] + : [hiddenKeyframe, { ...hiddenKeyframe, offset: 0.45 }, { opacity: 1, transform: 'translateY(0)' }]; + this._commentPreviewAnimations.push(element.animate(keyframes, { duration, easing, fill: 'forwards' })); + } + return highlightAnimation; + } + + private _scheduleCommentPreviewHide(): void { + this._cancelCommentPreviewHide(); + this._commentPreviewHideTimeout = window.setTimeout(() => { + this._commentPreviewHideTimeout = undefined; + const comment = this._commentPreviewElementId ? this._comments.get(this._commentPreviewElementId) : undefined; + if ( + comment?.pin.matches(':hover, :focus-within') || + this._highlight.matches(':hover, :focus-within') || + this._label.matches(':hover, :focus-within') || + this._commentPreview.matches(':hover, :focus-within') || + this._commentPreviewRemoveButton.matches(':hover, :focus-within') + ) { + return; + } + this._collapseActiveCommentPreview(); + }, 80); + } + + private _cancelCommentPreviewHide(): void { + if (this._commentPreviewHideTimeout !== undefined) { + window.clearTimeout(this._commentPreviewHideTimeout); + this._commentPreviewHideTimeout = undefined; + } + } + + private _collapseActiveCommentPreview(): void { + const elementId = this._commentPreviewElementId; + const comment = elementId ? this._comments.get(elementId) : undefined; + if (!elementId || !comment || this._reducedMotion) { + this._hideActiveCommentPreview(); + return; + } + + this._commentPreviewCollapsing = true; + this._shadowHost.classList.add('comment-preview-collapsing'); + this._fadeOutCommentBackdrop(); + let highlightAnimation: Animation | undefined = this._commentPreviewAnimations[0]; + if (highlightAnimation) { + for (const animation of this._commentPreviewAnimations) { + animation.reverse(); + } + } else { + highlightAnimation = this._animateCommentHighlight( + this._getCommentPinPointBounds(comment.pin), + comment.target, + [this._label, this._commentPreview], + true + ); + } + if (!highlightAnimation) { + this._hideActiveCommentPreview(); + return; + } + highlightAnimation.onfinish = () => { + if (this._commentPreviewCollapsing && this._commentPreviewElementId === elementId) { + this._commentPreviewCollapsing = false; + this._hideActiveCommentPreview(); + } + }; + } + + private _cancelCommentPreviewAnimations(): void { + for (const animation of this._commentPreviewAnimations) { + animation.cancel(); + } + this._commentPreviewAnimations = []; + } + + private _hideActiveCommentPreview(): void { + this._cancelCommentPreviewHide(); + this._commentPreviewCollapsing = false; + this._shadowHost.classList.remove('comment-preview-collapsing'); + this._cancelCommentPreviewAnimations(); + if (this._commentPreviewElementId) { + this._comments.get(this._commentPreviewElementId)?.pin.classList.remove('previewing'); + } + this._commentPreviewElementId = undefined; + this._shadowHost.classList.remove('comment-preview-active'); + this._commentPreview.style.display = 'none'; + this._hideCommentBackdrop(); + if (!this._commentTarget) { + this._updateHighlight(this._externalHighlightTarget); + } + } + + private _removeComment(elementId: string): void { + const comment = this._comments.get(elementId); + if (!comment) { + return; + } + this._hideActiveCommentPreview(); + comment.pin.remove(); + this._comments.delete(elementId); + this._updateCommentPinNumbers(); + this._unmountWhenIdle(); + this._onCommentRemoved(elementId); + } + + private _layoutCommentInput(): void { + this._commentInput.style.height = 'auto'; + this._commentInput.style.height = `${Math.min(this._commentInput.scrollHeight, 96)}px`; + this._layoutCommentComposer(); + } + + private _layoutCommentBackdrop(target: Element): void { + const rect = this._getVisibleTargetBounds(target.getBoundingClientRect()); + this._commentBackdropCutout.setAttribute('x', `${rect.x}`); + this._commentBackdropCutout.setAttribute('y', `${rect.y}`); + this._commentBackdropCutout.setAttribute('width', `${rect.width}`); + this._commentBackdropCutout.setAttribute('height', `${rect.height}`); + this._commentBackdropCutout.setAttribute('rx', '2'); + } + + private _showCommentBackdrop(target: Element): void { + const request = ++this._commentBackdropRequest; + this._commentBackdropTarget = target; + this._layoutCommentBackdrop(target); + this._commentBackdrop.classList.remove('visible'); + requestAnimationFrame(() => { + if (this._commentBackdropRequest === request) { + this._commentBackdrop.classList.add('visible'); + } + }); + } + + private _hideCommentBackdrop(): void { + this._commentBackdropRequest++; + this._commentBackdropTarget = undefined; + this._commentBackdrop.classList.remove('visible'); + } + + private _fadeOutCommentBackdrop(): void { + this._commentBackdropRequest++; + this._commentBackdrop.classList.remove('visible'); + } + + private _clearCommentPreview(target: Element): void { + if (this._commentTarget || this._commentBackdropTarget !== target) { + return; + } + this._hideActiveCommentPreview(); + } + + private _layoutCommentComposer(): void { + if (!this._commentTarget) { + return; + } + this._renderHighlight(this._commentTarget); + } + + private _ensureMounted(): void { + if (!this._shadowHost.parentNode) { + document.documentElement.appendChild(this._shadowHost); + } + } + + private _unmountWhenIdle(): void { + if (!this._selectionActive && !this._highlightTarget && this._comments.size === 0) { + this._shadowHost.remove(); + } + } + // --- Static helpers --- /** @@ -777,15 +1580,216 @@ class ElementPicker { } .highlight { position: absolute; box-sizing: border-box; - border: 2px solid var(--vscode-focusBorder, #0078d4); - background: color-mix(in srgb, var(--vscode-focusBorder, #0078d4) 12%, transparent); - border-radius: 2px; + z-index: 2; + } + .comment-backdrop { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 2; + } + .comment-backdrop-fill { + fill: var(--vscode-widget-shadow, transparent); + opacity: 0; + transition: opacity 120ms linear; + } + .comment-backdrop.visible .comment-backdrop-fill { + opacity: 1; + } + .highlight-shape { + fill: color-mix(in srgb, var(--vscode-focusBorder, #0078d4) 12%, transparent); + stroke: var(--vscode-focusBorder, #0078d4); + stroke-width: 2px; } .overlay { position: fixed; inset: 0; background: transparent; box-sizing: border-box; z-index: 1; } + .comment-layer { + position: absolute; inset: 0; pointer-events: none; + } + .comment-surface { + position: fixed; + box-sizing: border-box; + width: min(320px, calc(100vw - 16px)); + border: var(--vscode-strokeThickness, 1px) solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, #454545)); + border-radius: var(--vscode-cornerRadius-large, 8px); + background: var(--vscode-editorWidget-background, #252526); + color: var(--vscode-editorWidget-foreground, #cccccc); + box-shadow: 0 2px 6px var(--vscode-widget-shadow, transparent); + font-size: 13px; + font-weight: 400; + z-index: 3; + } + .comment-surface[data-attachment-corner='top-left'] { + border-top-left-radius: 0; + } + .comment-surface[data-attachment-corner='top-right'] { + border-top-right-radius: 0; + } + .comment-surface[data-attachment-corner='bottom-left'] { + border-bottom-left-radius: 0; + } + .comment-surface[data-attachment-corner='bottom-right'] { + border-bottom-right-radius: 0; + } + .comment-preview { + align-items: flex-start; + gap: 8px; + max-height: 96px; + padding: 6px 8px; + overflow: hidden; + line-height: 20px; + pointer-events: none; + } + .comment-preview.empty { + gap: 0; + padding: 4px; + } + .comment-preview.empty .comment-preview-body { + display: none; + } + .comment-preview.empty .comment-preview-remove { + margin-block: 0; + } + .comment-preview-body { + flex: 1; + min-width: 0; + max-height: 82px; + overflow-x: hidden; + overflow-y: auto; + overflow-wrap: anywhere; + scrollbar-width: thin; + white-space: pre-wrap; + } + :host(.comment-preview-active) .highlight, + :host(.comment-preview-active) .label, + :host(.comment-preview-active) .comment-preview { + pointer-events: auto; + } + :host(.comment-preview-collapsing) .highlight, + :host(.comment-preview-collapsing) .label, + :host(.comment-preview-collapsing) .comment-preview { + pointer-events: none; + } + .comment-preview-remove { + flex: none; + display: grid; + place-items: center; + box-sizing: border-box; + width: 24px; + height: 24px; + margin-block: -2px; + padding: 0; + border: 0; + border-radius: var(--vscode-cornerRadius-small, 4px); + background: transparent; + color: var(--vscode-editorWidget-foreground, inherit); + cursor: pointer; + font-family: inherit; + } + .comment-preview-remove svg { + display: block; + width: var(--vscode-codiconFontSize, 16px); + height: var(--vscode-codiconFontSize, 16px); + } + .comment-preview-remove:hover { + background: var(--vscode-toolbar-hoverBackground, transparent); + } + .comment-composer { + align-items: flex-end; gap: 6px; padding: 6px; + pointer-events: auto; + z-index: 4; + } + .comment-input { + flex: 1; min-width: 0; resize: none; overflow: auto; + scrollbar-width: none; + box-sizing: border-box; margin: 0; padding: 2px 6px; + background: transparent; color: inherit; + border: var(--vscode-strokeThickness, 1px) solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, #454545)); + border-radius: var(--vscode-cornerRadius-small, 4px); + outline: 0; + font: inherit; + line-height: 20px; + caret-color: var(--vscode-focusBorder, currentColor); + } + .comment-input::-webkit-scrollbar { + display: none; + } + .comment-input::placeholder { + color: var(--vscode-input-placeholderForeground, var(--vscode-descriptionForeground, #ccccccb3)); + opacity: 1; + } + .comment-send { + box-sizing: border-box; border: 0; cursor: pointer; font-family: inherit; + } + .comment-send { + flex: none; width: 24px; height: 24px; padding: 0; + border-radius: var(--vscode-cornerRadius-small, 4px); + background: transparent; + color: var(--vscode-editorWidget-foreground, #cccccc); + display: grid; + place-items: center; + } + .comment-send svg { + display: block; + width: var(--vscode-codiconFontSize, 16px); + height: var(--vscode-codiconFontSize, 16px); + } + .comment-send:hover { + background: var(--vscode-toolbar-hoverBackground, transparent); + } + .comment-pin { + position: absolute; + display: grid; + place-items: center; + width: 22px; + height: 22px; + transform: translate(-11px, -11px); + pointer-events: auto; + z-index: 4; + } + .comment-layer.composing .comment-pin { + pointer-events: none; + z-index: auto; + } + .comment-pin:hover, .comment-pin:focus-within { + z-index: 5; + } + .comment-pin.previewing:not(:focus-within) .comment-pin-bubble { + visibility: hidden; + } + .comment-pin-bubble { + box-sizing: border-box; + display: grid; + place-items: center; + width: 22px; + height: 22px; + padding: 0; + border: var(--vscode-strokeThickness, 1px) solid var(--vscode-editorWidget-background, #252526); + border-radius: var(--vscode-cornerRadius-circle, 9999px); + background: var(--vscode-button-background, #0078d4); + color: var(--vscode-button-foreground, white); + box-shadow: 0 2px 6px var(--vscode-widget-shadow, transparent); + } + .comment-pin-number { + display: block; + width: 100%; + font-size: 11px; + font-weight: 600; + line-height: 12px; + text-align: center; + } + .comment-send:focus-visible, .comment-preview-remove:focus-visible, .comment-pin:focus-visible, .comment-input:focus-visible { + outline: 2px solid var(--vscode-focusBorder, #0078d4); + outline-offset: 2px; + } + :host(.reduce-motion) .comment-backdrop-fill { + transition: none; + } .label { position: fixed; box-sizing: border-box; display: inline-flex; align-items: center; gap: 6px; height: 20px; padding: 0 6px; @@ -822,6 +1826,14 @@ class ElementPicker { host.style.setProperty('--vscode-focusBorder', theme?.focusBorder ?? null); host.style.setProperty('--vscode-button-background', theme?.buttonBackground ?? null); host.style.setProperty('--vscode-button-foreground', theme?.buttonForeground ?? null); + host.style.setProperty('--vscode-editorWidget-background', theme?.widgetBackground ?? null); + host.style.setProperty('--vscode-editorWidget-foreground', theme?.widgetForeground ?? null); + host.style.setProperty('--vscode-editorWidget-border', theme?.widgetBorder ?? null); + host.style.setProperty('--vscode-widget-shadow', theme?.widgetShadow ?? null); + host.style.setProperty('--vscode-contrastBorder', theme?.contrastBorder ?? null); + host.style.setProperty('--vscode-descriptionForeground', theme?.descriptionForeground ?? null); + host.style.setProperty('--vscode-input-placeholderForeground', theme?.inputPlaceholderForeground ?? null); + host.style.setProperty('--vscode-toolbar-hoverBackground', theme?.toolbarHoverBackground ?? null); host.style.setProperty('--pick-font', theme?.font ?? null); } } diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index b7f963076bb..aa403231eb4 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -603,7 +603,7 @@ export class BrowserView extends Disposable { storageKeys: { ...this.session.history.storageKeys, ...this.session.permissions.storageKeys }, permissions: this.session.permissions.serialize(), browserZoomIndex: this._browserZoomIndex, - isElementSelectionActive: this.inspector.isElementSelectionActive, + elementSelectionState: this.inspector.elementSelectionState, isRemoteSession: this.session.remote.isRemote, isAreaSelectionActive: this.inspector.isAreaSelectionActive, device: this.emulator.device diff --git a/src/vs/platform/browserView/electron-main/browserViewFrameInspector.ts b/src/vs/platform/browserView/electron-main/browserViewFrameInspector.ts index e35ab51cd8d..ef381a9175a 100644 --- a/src/vs/platform/browserView/electron-main/browserViewFrameInspector.ts +++ b/src/vs/platform/browserView/electron-main/browserViewFrameInspector.ts @@ -5,12 +5,13 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; -import { IBrowserElementSelectionOptions, IElementData, IElementAncestor, IBrowserViewTheme } from '../common/browserView.js'; +import { BrowserElementSelectionMode, IElementData, IElementAncestor, IBrowserElementCommentsUpdate, IBrowserElementSelectionOptions, IBrowserViewTheme } from '../common/browserView.js'; import { collapseToShorthands, formatMatchedStyles, keyComputedProperties, type IMatchedStyles } from '../common/cssHelpers.js'; import { ICDPConnection } from '../common/cdp/types.js'; export interface IFrameElementHandle extends IDisposable { addToChat(): Promise<void>; + addComment(): void; highlight(): Promise<void>; hideHighlight(): Promise<void>; } @@ -42,6 +43,11 @@ interface ILayoutMetricsResult { }; } +interface IActiveInspection extends IDisposable { + readonly mode: 'cdp' | 'preload'; + stop(): Promise<void>; +} + /** Slightly customised CDP debugger inspect highlight colours. */ export const inspectHighlightConfig = { showInfo: true, @@ -107,12 +113,14 @@ export class BrowserViewFrameInspector extends Disposable { private readonly _onDidInspectElement = this._register(new Emitter<IElementData>()); readonly onDidInspectElement: Event<IElementData> = this._onDidInspectElement.event; + private readonly _onDidRemoveElementComment = this._register(new Emitter<string>()); + readonly onDidRemoveElementComment = this._onDidRemoveElementComment.event; private readonly _onDidStopPicking = this._register(new Emitter<void>()); readonly onDidStopPicking: Event<void> = this._onDidStopPicking.event; private _isPaused = false; - private readonly _activeInspection = this._register(new MutableDisposable<IDisposable>()); + private readonly _activeInspection = this._register(new MutableDisposable<IActiveInspection>()); /** Whether this frame's JavaScript execution is currently paused by the debugger. */ get isPaused(): boolean { return this._isPaused; } @@ -177,19 +185,27 @@ export class BrowserViewFrameInspector extends Disposable { })); // Listen for element-picked IPC from this frame's preload - const onPicked = async (event: Electron.IpcMainEvent, pickId: string) => { - if (!pickId || event.senderFrame !== this.frame) { + const onPicked = async (event: Electron.IpcMainEvent, result: { elementId?: string; comment?: string }) => { + if (!result?.elementId || event.senderFrame !== this.frame) { return; } try { - const nodeData = await this.extractNodeDataById(pickId); - this._onDidInspectElement.fire(nodeData); + const nodeData = await this.extractNodeDataById(result.elementId); + this._onDidInspectElement.fire({ ...nodeData, elementId: result.elementId, comment: result.comment }); } catch { + this._updateElementComments({ pendingCommentIdsToDiscard: [result.elementId] }); // Best effort; user can re-pick. } }; frame.ipc.on('vscode:browserView:elementPicked', onPicked); this._register({ dispose: () => frame.ipc.removeListener('vscode:browserView:elementPicked', onPicked) }); + const onCommentRemoved = (event: Electron.IpcMainEvent, elementId: string) => { + if (elementId && event.senderFrame === this.frame) { + this._onDidRemoveElementComment.fire(elementId); + } + }; + frame.ipc.on('vscode:browserView:elementCommentRemoved', onCommentRemoved); + this._register({ dispose: () => frame.ipc.removeListener('vscode:browserView:elementCommentRemoved', onCommentRemoved) }); // Listen for pick-stopped IPC from this frame's preload const onPickStopped = (event: Electron.IpcMainEvent) => { @@ -234,37 +250,63 @@ export class BrowserViewFrameInspector extends Disposable { * Stores a disposable so stop always tears down the correct mode. */ async startInspection(options: IBrowserElementSelectionOptions): Promise<void> { - if (this._isPaused) { + const mode = this._isPaused && options.mode !== BrowserElementSelectionMode.Comment ? 'cdp' : 'preload'; + if (this._activeInspection.value?.mode === mode) { + if (mode === 'preload') { + this.frame.postMessage('vscode:browserView:startElementPicker', options); + } + return; + } + + await this._stopInspection(); + if (mode === 'cdp') { await this.connection.sendCommand('Overlay.setInspectMode', { mode: 'searchForNode', highlightConfig: inspectHighlightConfig, }); + const stop = async () => { + if (this.frame.isDestroyed()) { + return; + } + try { + await this.connection.sendCommand('Overlay.setInspectMode', { + mode: 'none', + highlightConfig: { showInfo: false, showStyles: false } + }); + await this.connection.sendCommand('Overlay.hideHighlight'); + } catch { + // Best effort. + } + }; this._activeInspection.value = { - dispose: async () => { - if (this.frame.isDestroyed()) { - return; - } - try { - await this.connection.sendCommand('Overlay.setInspectMode', { - mode: 'none', - highlightConfig: { showInfo: false, showStyles: false } - }); - await this.connection.sendCommand('Overlay.hideHighlight'); - } catch { - // Best effort. - } + mode, + stop, + dispose: () => { + void stop(); } }; } else { this.frame.postMessage('vscode:browserView:startElementPicker', options); - this._activeInspection.value = { - dispose: () => { - if (this.frame.isDestroyed()) { - return; - } + const stop = async () => { + if (!this.frame.isDestroyed()) { this.frame.postMessage('vscode:browserView:stopElementPicker', {}); } }; + this._activeInspection.value = { + mode, + stop, + dispose: () => { + void stop(); + } + }; + } + } + + private async _stopInspection(): Promise<void> { + const activeInspection = this._activeInspection.value; + if (activeInspection) { + this._activeInspection.clearAndLeak(); + await activeInspection.stop(); } } @@ -272,7 +314,17 @@ export class BrowserViewFrameInspector extends Disposable { * Stop element inspection on this frame. */ async stopInspection(): Promise<void> { - this._activeInspection.clear(); + await this._stopInspection(); + } + + setElementComments(update: IBrowserElementCommentsUpdate): void { + this._updateElementComments(update); + } + + private _updateElementComments(update: IBrowserElementCommentsUpdate): void { + if (!this.frame.isDestroyed()) { + this.frame.postMessage('vscode:browserView:setElementComments', update); + } } /** @@ -328,6 +380,9 @@ export class BrowserViewFrameInspector extends Disposable { const nodeData = await this.extractNodeDataById(elementId); this._onDidInspectElement.fire(nodeData); }, + addComment: () => { + this.frame.postMessage('vscode:browserView:showElementComment', { elementId }); + }, highlight: async () => { this.frame.postMessage('vscode:browserView:highlightElement', { elementId }); }, diff --git a/src/vs/platform/browserView/electron-main/browserViewInspector.ts b/src/vs/platform/browserView/electron-main/browserViewInspector.ts index 21c0815cd10..0099fd3d7b0 100644 --- a/src/vs/platform/browserView/electron-main/browserViewInspector.ts +++ b/src/vs/platform/browserView/electron-main/browserViewInspector.ts @@ -5,17 +5,32 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; -import { IBrowserElementSelectionOptions, IElementData, IBrowserViewTheme, IBrowserViewRect } from '../common/browserView.js'; +import { BrowserElementSelectionMode, IBrowserElementCommentsUpdate, IBrowserElementSelectionOptions, IBrowserElementSelectionState, IElementData, IBrowserViewTheme, IBrowserViewRect, IBrowserViewPreloadLocalizedStrings } from '../common/browserView.js'; import { ICDPConnection } from '../common/cdp/types.js'; import type { BrowserView } from './browserView.js'; import { BrowserViewFrameInspector } from './browserViewFrameInspector.js'; +import { localize } from '../../../nls.js'; + +const localizedStrings: IBrowserViewPreloadLocalizedStrings = { + addComment: localize('browserView.addComment', "Add Comment"), + addCommentPlaceholder: localize('browserView.addCommentPlaceholder', "Add a comment"), + commentOnSelectedElement: localize('browserView.commentOnSelectedElement', "Comment on selected element"), + elementComment: localize('browserView.elementComment', "Element comment {0}"), + elementCommentWithBody: localize('browserView.elementCommentWithBody', "Element comment {0}: {1}"), + emptyElementComment: localize('browserView.emptyElementComment', "Empty element comment {0}"), + removeComment: localize('browserView.removeComment', "Remove Comment"), + removeElementComment: localize('browserView.removeElementComment', "Remove element comment"), +}; interface IActiveSelection extends IDisposable { - readonly options?: IBrowserElementSelectionOptions; + options: IBrowserElementSelectionOptions; } +interface IActiveAreaSelection extends IDisposable { } + export interface IElementHandle extends IDisposable { addToChat(): Promise<void>; + addComment(): void; highlight(): Promise<void>; hideHighlight(): Promise<void>; } @@ -48,14 +63,23 @@ export class BrowserViewInspector extends Disposable { private readonly _onDidSelectElement = this._register(new Emitter<IElementData>()); readonly onDidSelectElement: Event<IElementData> = this._onDidSelectElement.event; + private readonly _onDidRemoveElementComment = this._register(new Emitter<string>()); + readonly onDidRemoveElementComment = this._onDidRemoveElementComment.event; - private readonly _onDidChangeElementSelectionActive = this._register(new Emitter<boolean>()); - readonly onDidChangeElementSelectionActive: Event<boolean> = this._onDidChangeElementSelectionActive.event; + private readonly _onDidChangeElementSelectionState = this._register(new Emitter<IBrowserElementSelectionState>()); + readonly onDidChangeElementSelectionState: Event<IBrowserElementSelectionState> = this._onDidChangeElementSelectionState.event; private _elementSelectionActive = false; get isElementSelectionActive(): boolean { return this._elementSelectionActive; } + get elementSelectionState(): IBrowserElementSelectionState { + return { + active: this._elementSelectionActive, + options: this._activeSelection.value?.options ?? {} + }; + } private readonly _activeSelection = this._register(new MutableDisposable<IActiveSelection>()); + private _inspectionOperation: Promise<void> = Promise.resolve(); private _theme: IBrowserViewTheme = {}; // Area selection — drag-to-select a rectangle on the top frame. @@ -74,7 +98,7 @@ export class BrowserViewInspector extends Disposable { private _areaSelectionActive = false; get isAreaSelectionActive(): boolean { return this._areaSelectionActive; } - private readonly _activeAreaSelection = this._register(new MutableDisposable<IActiveSelection>()); + private readonly _activeAreaSelection = this._register(new MutableDisposable<IActiveAreaSelection>()); private readonly _registry = this._register(new FrameInspectorRegistry()); @@ -108,6 +132,7 @@ export class BrowserViewInspector extends Disposable { // Apply theme immediately regardless of inspector state senderFrame.postMessage('vscode:browserView:setTheme', this._theme); + senderFrame.postMessage('vscode:browserView:setLocalizedStrings', localizedStrings); this._registry.notifyFrameReady(senderFrame, frameToken); @@ -225,7 +250,9 @@ export class BrowserViewInspector extends Disposable { */ private _onInspectorAdopted(inspector: BrowserViewFrameInspector): void { inspector.onDidInspectElement(async nodeData => { - this._activeSelection.clear(); + if (!this._activeSelection.value?.options?.continuous) { + this._activeSelection.clear(); + } try { const offset = await this._getFrameOffsetInPage(inspector.frame); nodeData = this._offsetElementData(nodeData, offset); @@ -234,6 +261,7 @@ export class BrowserViewInspector extends Disposable { } this._onDidSelectElement.fire(nodeData); }); + inspector.onDidRemoveElementComment(elementId => this._onDidRemoveElementComment.fire(elementId)); // When a frame's preload stops picking, stop all other frames too inspector.onDidStopPicking(() => { @@ -241,9 +269,13 @@ export class BrowserViewInspector extends Disposable { }); // If element selection is currently active, start it on the new frame - const activeSelection = this._activeSelection.value; - if (activeSelection) { - inspector.startInspection(activeSelection.options ?? {}).catch(() => { }); + if (this._activeSelection.value) { + void this._queueInspectionOperation(async () => { + const activeSelection = this._activeSelection.value; + if (activeSelection) { + await inspector.startInspection(activeSelection.options); + } + }).catch(() => { }); } inspector.setTheme(this._theme); @@ -262,43 +294,77 @@ export class BrowserViewInspector extends Disposable { */ async toggleElementSelection(enabled?: boolean, options: IBrowserElementSelectionOptions = {}): Promise<void> { const newEnabled = enabled ?? !this._elementSelectionActive; - if (newEnabled === this._elementSelectionActive) { - return; - } - if (!newEnabled) { this._activeSelection.clear(); return; } - // Element and area selection are mutually exclusive — enabling one // cancels the other so both pickers never overlay the page at once. this._activeAreaSelection.clear(); - const start = () => Promise.all([...this._registry.inspectors].map(i => i.startInspection(options))); - const stop = () => Promise.all([...this._registry.inspectors].map(i => i.stopInspection())); + const activeSelection = this._activeSelection.value; + const updatedOptions = activeSelection ? { ...activeSelection.options, ...options } : { mode: BrowserElementSelectionMode.Select, ...options }; + + if (activeSelection) { + activeSelection.options = updatedOptions; + try { + if (await this._startInspection(activeSelection, updatedOptions)) { + this._elementSelectionActive = true; + this._onDidChangeElementSelectionState.fire({ active: true, options: updatedOptions }); + } + } catch { + if (this._activeSelection.value === activeSelection && activeSelection.options === updatedOptions) { + this._activeSelection.clear(); + } + } + return; + } const selection: IActiveSelection = { - options, + options: updatedOptions, dispose: () => { if (this._activeSelection.value === selection) { this._elementSelectionActive = false; - this._onDidChangeElementSelectionActive.fire(false); + this._onDidChangeElementSelectionState.fire({ active: false, options: selection.options }); this._activeSelection.clearAndLeak(); - void stop().catch(() => { }); + void this._queueInspectionOperation(async () => { + await Promise.all([...this._registry.inspectors].map(i => i.stopInspection())); + }).catch(() => { }); } } }; this._activeSelection.value = selection; - try { - await start(); - if (this._activeSelection.value === selection) { + if (await this._startInspection(selection, updatedOptions)) { this._elementSelectionActive = true; - this._onDidChangeElementSelectionActive.fire(true); + this._onDidChangeElementSelectionState.fire({ active: true, options: updatedOptions }); } } catch { - this._activeSelection.clear(); + if (this._activeSelection.value === selection && selection.options === updatedOptions) { + this._activeSelection.clear(); + } + } + } + + private async _startInspection(selection: IActiveSelection, options: IBrowserElementSelectionOptions): Promise<boolean> { + await this._queueInspectionOperation(async () => { + if (this._activeSelection.value !== selection || selection.options !== options) { + return; + } + await Promise.all([...this._registry.inspectors].map(i => i.startInspection(options))); + }); + return this._activeSelection.value === selection && selection.options === options; + } + + private _queueInspectionOperation(operation: () => Promise<void>): Promise<void> { + const result = this._inspectionOperation.then(operation); + this._inspectionOperation = result.catch(() => { }); + return result; + } + + setElementComments(update: IBrowserElementCommentsUpdate): void { + for (const inspector of this._registry.inspectors) { + inspector.setElementComments(update); } } @@ -326,7 +392,7 @@ export class BrowserViewInspector extends Disposable { const start = () => { mainFrame.postMessage('vscode:browserView:startAreaPicker', undefined); }; const stop = () => { try { mainFrame.postMessage('vscode:browserView:stopAreaPicker', undefined); } catch { /* frame may be gone */ } }; - const selection: IActiveSelection = { + const selection: IActiveAreaSelection = { dispose: () => { // External cancellation (toggleAreaSelection(false), navigation, element // selection takeover). The IPC-driven termination paths use clearAndLeak @@ -371,7 +437,37 @@ export class BrowserViewInspector extends Disposable { * Resolve a handle to an element. Routes to the correct frame inspector. */ getElementHandle(id: string, frame: Electron.WebFrameMain): IElementHandle | undefined { - return this._registry.getByFrame(frame)?.getElementHandle(id); + const handle = this._registry.getByFrame(frame)?.getElementHandle(id); + if (!handle) { + return undefined; + } + let commentRequested = false; + return { + addToChat: () => handle.addToChat(), + addComment: () => { + if (commentRequested) { + return; + } + commentRequested = true; + setTimeout(() => { + this._activeAreaSelection.clear(); + this._activeSelection.clear(); + void this._queueInspectionOperation(async () => { + if (!this.browser.webContents.isDestroyed()) { + this.browser.webContents.focus(); + handle.addComment(); + } + }); + }, 0); + }, + highlight: () => handle.highlight(), + hideHighlight: () => handle.hideHighlight(), + dispose: () => { + if (!commentRequested) { + handle.dispose(); + } + } + }; } async getVisualViewportScale(frame: Electron.WebFrameMain = this.browser.webContents.mainFrame): Promise<number> { diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index d56290b93b8..3861f6ee808 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { VSBuffer } from '../../../base/common/buffer.js'; -import { IBrowserElementSelectionOptions, IBrowserViewBounds, IBrowserViewState, IBrowserViewService, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, BrowserViewCommandId, IBrowserViewOwner, IBrowserViewInfo, IBrowserViewCreatedEvent, IBrowserViewOpenOptions, IBrowserViewCreateOptions, IBrowserViewWindowConfiguration, IBrowserDeviceProfile } from '../common/browserView.js'; +import { IBrowserElementCommentsUpdate, IBrowserElementSelectionOptions, IBrowserViewBounds, IBrowserViewState, IBrowserViewService, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, BrowserViewCommandId, IBrowserViewOwner, IBrowserViewInfo, IBrowserViewCreatedEvent, IBrowserViewOpenOptions, IBrowserViewCreateOptions, IBrowserViewWindowConfiguration, IBrowserDeviceProfile } from '../common/browserView.js'; import { clipboard, Menu, MenuItem } from 'electron'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js'; @@ -190,8 +190,12 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa return this._getBrowserView(id).inspector.onDidSelectElement; } - onDynamicDidChangeElementSelectionActive(id: string) { - return this._getBrowserView(id).inspector.onDidChangeElementSelectionActive; + onDynamicDidRemoveElementComment(id: string) { + return this._getBrowserView(id).inspector.onDidRemoveElementComment; + } + + onDynamicDidChangeElementSelectionState(id: string) { + return this._getBrowserView(id).inspector.onDidChangeElementSelectionState; } onDynamicDidPickArea(id: string) { @@ -342,6 +346,10 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa return this._getBrowserView(id).inspector.toggleElementSelection(enabled, options); } + async setElementComments(id: string, update: IBrowserElementCommentsUpdate): Promise<void> { + this._getBrowserView(id).inspector.setElementComments(update); + } + async toggleAreaSelection(id: string, enabled?: boolean): Promise<void> { return this._getBrowserView(id).inspector.toggleAreaSelection(enabled); } @@ -589,15 +597,21 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa })); } - menu.append(new MenuItem({ type: 'separator' })); if (inspectTarget) { + menu.append(new MenuItem({ type: 'separator' })); menu.append(new MenuItem({ label: localize('browser.contextMenu.addElementToChat', 'Add Element to Chat'), click: () => inspectTarget.addToChat() })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.addComment', 'Add Comment...'), + click: () => inspectTarget.addComment() + })); void inspectTarget.highlight().catch(() => { }); menu.on('menu-will-close', () => inspectTarget.dispose()); } + + menu.append(new MenuItem({ type: 'separator' })); menu.append(new MenuItem({ label: localize('browser.contextMenu.inspect', 'Inspect'), click: () => webContents.inspectElement(params.x, params.y) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 5af7e21124f..8fabfbd6a4f 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -72,7 +72,8 @@ export const enum AccessibilityVerbositySettingId { SessionsChanges = 'accessibility.verbosity.sessionsChanges', ChatQuestionCarousel = 'accessibility.verbosity.chatQuestionCarousel', Survey = 'accessibility.verbosity.survey', - Automations = 'accessibility.verbosity.automations' + Automations = 'accessibility.verbosity.automations', + BrowserElementCommenting = 'accessibility.verbosity.browserElementCommenting' } const baseVerbosityProperty: IConfigurationPropertySchema = { @@ -230,6 +231,10 @@ const configuration: IConfigurationNode = { description: localize('verbosity.automations', 'Provide information about how to use the Automations section of the Agent Customizations editor, including keyboard navigation and how to inspect scheduled runs.'), ...baseVerbosityProperty }, + [AccessibilityVerbositySettingId.BrowserElementCommenting]: { + description: localize('verbosity.browserElementCommenting', 'Provide information about how to access element commenting accessibility help in the Integrated Browser.'), + ...baseVerbosityProperty + }, 'accessibility.signalOptions.volume': { 'description': localize('accessibility.signalOptions.volume', "The volume of the sounds in percent (0-100)."), 'type': 'number', diff --git a/src/vs/workbench/contrib/browserView/common/browserView.ts b/src/vs/workbench/contrib/browserView/common/browserView.ts index 7ca29954cf4..3c1da670e5d 100644 --- a/src/vs/workbench/contrib/browserView/common/browserView.ts +++ b/src/vs/workbench/contrib/browserView/common/browserView.ts @@ -44,6 +44,8 @@ import { IBrowserViewVisibilityEvent, IBrowserViewCertificateError, IElementData, + IBrowserElementCommentsUpdate, + IBrowserElementSelectionOptions, IBrowserViewOwner, IBrowserViewOpenOptions, IBrowserViewRect, @@ -52,7 +54,7 @@ import { IBrowserViewState, IBrowserDeviceProfile, IBrowserViewPermissionRequestEvent, - IBrowserElementSelectionOptions, + IBrowserElementSelectionState, } from '../../../../platform/browserView/common/browserView.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { isLocalhostAuthority } from '../../../../platform/url/common/trustedDomains.js'; @@ -371,7 +373,7 @@ export interface IBrowserViewModel extends IDisposable { readonly zoomFactor: number; readonly canZoomIn: boolean; readonly canZoomOut: boolean; - readonly isElementSelectionActive: boolean; + readonly elementSelectionState: IBrowserElementSelectionState; readonly isAreaSelectionActive: boolean; readonly device: IBrowserDeviceProfile | undefined; @@ -390,7 +392,8 @@ export interface IBrowserViewModel extends IDisposable { readonly onDidClose: Event<void>; readonly onWillDispose: Event<void>; readonly onDidSelectElement: Event<IElementData>; - readonly onDidChangeElementSelectionActive: Event<boolean>; + readonly onDidRemoveElementComment: Event<string>; + readonly onDidChangeElementSelectionState: Event<IBrowserElementSelectionState>; readonly onDidPickArea: Event<IBrowserViewRect | undefined>; readonly onDidChangeAreaSelectionActive: Event<boolean>; readonly onDidChangeDevice: Event<IBrowserDeviceProfile | undefined>; @@ -421,6 +424,7 @@ export interface IBrowserViewModel extends IDisposable { resetZoom(): Promise<void>; getConsoleLogs(): Promise<string>; toggleElementSelection(enabled?: boolean, options?: IBrowserElementSelectionOptions): Promise<void>; + setElementComments(update: IBrowserElementCommentsUpdate): Promise<void>; toggleAreaSelection(enabled?: boolean): Promise<void>; setDevice(device: IBrowserDeviceProfile | undefined): Promise<void>; } @@ -444,7 +448,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { private _zoomHost: string | undefined = undefined; private _sharedWithAgent: boolean = false; private _browserZoomIndex: number = browserZoomDefaultIndex; - private _isElementSelectionActive: boolean = false; + private _elementSelectionState: IBrowserElementSelectionState = { active: false, options: {} }; private _isAreaSelectionActive: boolean = false; private _device: IBrowserDeviceProfile | undefined; @@ -498,7 +502,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { this._storageScope = initialState.storageScope; this._isRemoteSession = initialState.isRemoteSession; this._browserZoomIndex = initialState.browserZoomIndex; - this._isElementSelectionActive = initialState.isElementSelectionActive; + this._elementSelectionState = initialState.elementSelectionState; this._isAreaSelectionActive = initialState.isAreaSelectionActive; this._device = initialState.device; this._isEphemeral = this._storageScope === BrowserViewStorageScope.Ephemeral; @@ -601,11 +605,11 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { } })); - this._register(this.onDidChangeElementSelectionActive(active => { - if (active) { + this._register(this.onDidChangeElementSelectionState(state => { + if (state.active && !this._elementSelectionState.active) { this.telemetryService.publicLog2<IntegratedBrowserAddElementToChatStartEvent, IntegratedBrowserAddElementToChatStartClassification>('integratedBrowser.addElementToChat.start', {}); } - this._isElementSelectionActive = active; + this._elementSelectionState = state; })); this._register(this.onDidChangeAreaSelectionActive(active => { @@ -648,7 +652,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { get zoomFactor(): number { return browserZoomFactors[this._browserZoomIndex]; } get canZoomIn(): boolean { return this._browserZoomIndex < browserZoomFactors.length - 1; } get canZoomOut(): boolean { return this._browserZoomIndex > 0; } - get isElementSelectionActive(): boolean { return this._isElementSelectionActive; } + get elementSelectionState(): IBrowserElementSelectionState { return this._elementSelectionState; } get isAreaSelectionActive(): boolean { return this._isAreaSelectionActive; } get device(): IBrowserDeviceProfile | undefined { return this._device; } @@ -855,6 +859,10 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { return this.browserViewService.toggleElementSelection(this.id, enabled, options); } + async setElementComments(update: IBrowserElementCommentsUpdate): Promise<void> { + return this.browserViewService.setElementComments(this.id, update); + } + async toggleAreaSelection(enabled?: boolean): Promise<void> { return this.browserViewService.toggleAreaSelection(this.id, enabled); } @@ -863,8 +871,12 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { return this.browserViewService.onDynamicDidSelectElement(this.id); } - get onDidChangeElementSelectionActive(): Event<boolean> { - return this.browserViewService.onDynamicDidChangeElementSelectionActive(this.id); + get onDidRemoveElementComment(): Event<string> { + return this.browserViewService.onDynamicDidRemoveElementComment(this.id); + } + + get onDidChangeElementSelectionState(): Event<IBrowserElementSelectionState> { + return this.browserViewService.onDynamicDidChangeElementSelectionState(this.id); } get onDidPickArea(): Event<IBrowserViewRect | undefined> { diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts index 9084813d496..7feacaf2d37 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts @@ -25,12 +25,14 @@ import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { IsSessionsWindowContext } from '../../../common/contextkeys.js'; import { ChatConfiguration } from '../../chat/common/constants.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { focusBorder } from '../../../../platform/theme/common/colors/baseColors.js'; -import { buttonForeground, buttonBackground } from '../../../../platform/theme/common/colors/inputColors.js'; +import { contrastBorder, descriptionForeground, focusBorder } from '../../../../platform/theme/common/colors/baseColors.js'; +import { buttonForeground, buttonBackground, inputPlaceholderForeground } from '../../../../platform/theme/common/colors/inputColors.js'; +import { editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, toolbarHoverBackground, widgetShadow } from '../../../../platform/theme/common/colors/editorColors.js'; import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; import { findGroup } from '../../../services/editor/common/editorGroupFinder.js'; import { ChatEditorInput } from '../../chat/browser/widgetHosts/editor/chatEditorInput.js'; import { IChatWidgetService } from '../../chat/browser/chat.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { URI } from '../../../../base/common/uri.js'; import { isEqual } from '../../../../base/common/resources.js'; import { Schemas } from '../../../../base/common/network.js'; @@ -122,6 +124,7 @@ export class BrowserViewWorkbenchService extends Disposable implements IBrowserV @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IThemeService private readonly themeService: IThemeService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { super(); const channel = mainProcessService.getChannel(ipcBrowserViewChannelName); @@ -134,6 +137,7 @@ export class BrowserViewWorkbenchService extends Disposable implements IBrowserV const chatEnabledKeys = new Set(ChatContextKeys.enabled.keys()); this._register(this.keybindingService.onDidUpdateKeybindings(() => this._updateWindowConfiguration())); this._register(this.themeService.onDidColorThemeChange(() => this._updateWindowConfiguration())); + this._register(this.accessibilityService.onDidChangeReducedMotion(() => this._updateWindowConfiguration())); this._register(this.workspaceTrustManagementService.onDidChangeTrustedFolders(() => this._updateWindowConfiguration())); this._register(this.workspaceTrustManagementService.onDidChangeTrust(() => this._updateWindowConfiguration())); this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this._updateWindowConfiguration())); @@ -518,7 +522,16 @@ export class BrowserViewWorkbenchService extends Disposable implements IBrowserV focusBorder: theme.getColor(focusBorder)?.toString(), buttonBackground: theme.getColor(buttonBackground)?.toString(), buttonForeground: theme.getColor(buttonForeground)?.toString(), + widgetBackground: theme.getColor(editorWidgetBackground)?.toString(), + widgetForeground: theme.getColor(editorWidgetForeground)?.toString(), + widgetBorder: theme.getColor(editorWidgetBorder)?.toString(), + widgetShadow: theme.getColor(widgetShadow)?.toString(), + contrastBorder: theme.getColor(contrastBorder)?.toString(), + descriptionForeground: theme.getColor(descriptionForeground)?.toString(), + inputPlaceholderForeground: theme.getColor(inputPlaceholderForeground)?.toString(), + toolbarHoverBackground: theme.getColor(toolbarHoverBackground)?.toString(), font: DEFAULT_FONT_FAMILY, + reducedMotion: this.accessibilityService.isMotionReduced(), }; } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/features/browserEditorChatFeatures.ts b/src/vs/workbench/contrib/browserView/electron-browser/features/browserEditorChatFeatures.ts index f46c3ee4d3d..dabfa9bc59e 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/features/browserEditorChatFeatures.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/features/browserEditorChatFeatures.ts @@ -14,7 +14,7 @@ import { KeyMod, KeyCode } from '../../../../../base/common/keyCodes.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; @@ -26,7 +26,7 @@ import { IChatWidget, IChatWidgetService } from '../../../chat/browser/chat.js'; import { IChatService } from '../../../chat/common/chatService/chatService.js'; import { IChatRequestVariableEntry } from '../../../chat/common/attachments/chatVariableEntries.js'; import { ChatContextKeys } from '../../../chat/common/actions/chatContextKeys.js'; -import { IBrowserElementSelectionOptions, IElementData, IElementAncestor, BrowserViewCommandId } from '../../../../../platform/browserView/common/browserView.js'; +import { BrowserElementSelectionMode, IBrowserElementSelectionOptions, IElementData, IElementAncestor, BrowserViewCommandId } from '../../../../../platform/browserView/common/browserView.js'; import { IBrowserViewModel, BrowserViewSharingState } from '../../../browserView/common/browserView.js'; import { BrowserEditorInput } from '../../common/browserEditorInput.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; @@ -39,6 +39,13 @@ import { Registry } from '../../../../../platform/registry/common/platform.js'; import { PolicyCategory } from '../../../../../base/common/policy.js'; import { Extensions as ConfigurationMigrationExtensions, IConfigurationMigrationRegistry, workbenchConfigurationNodeBase } from '../../../../common/configuration.js'; import { safeSetInnerHtml } from '../../../../../base/browser/domSanitize.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { ChatDynamicVariableModel } from '../../../chat/browser/attachments/chatDynamicVariables.js'; +import { toAttachedContextDynamicVariable } from '../../../chat/common/attachments/chatVariables.js'; +import { isEqual } from '../../../../../base/common/resources.js'; +import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType, IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; +import { AccessibleViewRegistry, IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; // Register tools import '../tools/browserTools.contribution.js'; @@ -98,9 +105,38 @@ function createElementContextValue(elementData: IElementData, displayName: strin const BROWSER_EDITOR_ACTIVE = ContextKeyExpr.equals('activeEditor', BrowserEditorInput.EDITOR_ID); const BrowserCategory = localize2('browserCategory', "Browser"); -const CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE = new RawContextKey<boolean>('browserElementSelectionActive', false, localize('browser.elementSelectionActive', "Whether element selection is currently active")); +const CONTEXT_BROWSER_ELEMENT_SELECTION_MODE = new RawContextKey<BrowserElementSelectionMode | undefined>('browserElementSelectionMode', undefined, localize('browser.elementSelectionMode', "The active element selection mode")); const CONTEXT_BROWSER_AREA_SELECTION_ACTIVE = new RawContextKey<boolean>('browserAreaSelectionActive', false, localize('browser.areaSelectionActive', "Whether area selection is currently active")); +class BrowserElementCommentingAccessibilityHelp implements IAccessibleViewImplementation { + readonly type = AccessibleViewType.Help; + readonly priority = 110; + readonly name = 'browserElementCommenting'; + readonly when = CONTEXT_BROWSER_ELEMENT_SELECTION_MODE.isEqualTo(BrowserElementSelectionMode.Comment); + + getProvider(accessor: ServicesAccessor): AccessibleContentProvider | undefined { + const editorPane = accessor.get(IEditorService).activeEditorPane; + if (!(editorPane instanceof BrowserEditor)) { + return undefined; + } + return new AccessibleContentProvider( + AccessibleViewProviderId.BrowserElementCommenting, + { type: AccessibleViewType.Help }, + () => [ + localize('browser.elementCommentingAccessibilityHelp.overview', "You are in Integrated Browser element commenting mode."), + localize('browser.elementCommentingAccessibilityHelp.navigation', "Use Tab and Shift+Tab to move through focusable page elements. Press Enter to comment on the focused element."), + localize('browser.elementCommentingAccessibilityHelp.composer', "In the comment input, press Enter to add the comment or Escape to cancel it."), + localize('browser.elementCommentingAccessibilityHelp.continuous', "Commenting mode remains active after adding a comment. Press Escape outside the comment input to stop commenting."), + localize('browser.elementCommentingAccessibilityHelp.pins', "Numbered comment pins are in the page tab order. Focus a pin to preview its comment, then Tab to its Remove Comment button."), + ].join('\n'), + () => editorPane.focus(), + AccessibilityVerbositySettingId.BrowserElementCommenting + ); + } +} + +AccessibleViewRegistry.register(new BrowserElementCommentingAccessibilityHelp()); + type IntegratedBrowserAddScreenshotToChatAddedEvent = { screenshotType: 'viewport' | 'area' | 'fullPage'; }; @@ -117,8 +153,14 @@ type IntegratedBrowserAddScreenshotToChatAddedClassification = { * console log attachment to chat, and agent sharing. */ export class BrowserEditorChatIntegration extends BrowserEditorContribution { - private readonly _elementSelectionActiveContext: IContextKey<boolean>; + private readonly _elementSelectionModeContext: IContextKey<BrowserElementSelectionMode | undefined>; private readonly _areaSelectionActiveContext: IContextKey<boolean>; + private _elementSelectionMode: BrowserElementSelectionMode | undefined; + private readonly _commentReferences = new Map<string, { elementId: string; attachmentIds: readonly string[]; widget: IChatWidget; browserModel: IBrowserViewModel }>(); + private readonly _commentReferenceListeners = this._register(new DisposableMap<IChatWidget, DisposableStore>()); + private readonly _commentModelListeners = this._register(new DisposableMap<IBrowserViewModel, DisposableStore>()); + private readonly _disposedCommentModels = new WeakSet<IBrowserViewModel>(); + private readonly _commentSessionsWithComments = new Set<IBrowserViewModel>(); // Share with Agent private readonly _shareButtonContainer: HTMLElement; @@ -137,9 +179,10 @@ export class BrowserEditorChatIntegration extends BrowserEditorContribution { @IStorageService private readonly storageService: IStorageService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IAccessibleViewService private readonly accessibleViewService: IAccessibleViewService, ) { super(editor); - this._elementSelectionActiveContext = CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE.bindTo(contextKeyService); + this._elementSelectionModeContext = CONTEXT_BROWSER_ELEMENT_SELECTION_MODE.bindTo(contextKeyService); this._areaSelectionActiveContext = CONTEXT_BROWSER_AREA_SELECTION_ACTIVE.bindTo(contextKeyService); // Build share toggle button @@ -165,10 +208,26 @@ export class BrowserEditorChatIntegration extends BrowserEditorContribution { })); // Auto-disable element selection when the user sends a chat request. - this._register(this.chatService.onDidSubmitRequest(() => { - if (this.editor.model?.isElementSelectionActive) { + this._register(this.chatService.onDidSubmitRequest(event => { + if (this.editor.model?.elementSelectionState.active) { void this.editor.model.toggleElementSelection(false); } + const submittedComments = [...this._commentReferences] + .filter(([, reference]) => reference.widget.viewModel && isEqual(reference.widget.viewModel.sessionResource, event.chatSessionResource)); + if (submittedComments.length > 0) { + const browserModels = new Set(submittedComments.map(([, reference]) => reference.browserModel)); + const widgets = new Set(submittedComments.map(([, reference]) => reference.widget)); + for (const [attachmentId] of submittedComments) { + this._commentReferences.delete(attachmentId); + } + for (const widget of widgets) { + this._disposeCommentReferenceListenerIfUnused(widget); + } + for (const browserModel of browserModels) { + this._syncElementComments(browserModel); + this._disposeCommentModelListenerIfUnused(browserModel); + } + } })); } @@ -183,20 +242,49 @@ export class BrowserEditorChatIntegration extends BrowserEditorContribution { this._updateSharingState(false); })); store.add(model.onDidSelectElement(async data => { + const tracksComment = data.comment !== undefined && data.elementId !== undefined; + if (tracksComment) { + this._ensureCommentModelListeners(model); + } + let attached = false; try { - await this._attachElementDataToChat(data, model); + attached = await this._attachElementDataToChat(data, model); } catch (error) { this.logService.error('BrowserEditor.addElementToChat: Failed to attach element', error); } + if (!attached && data.comment !== undefined && data.elementId && !this._disposedCommentModels.has(model)) { + this._syncElementComments(model, [data.elementId]); + } + if (tracksComment) { + this._disposeCommentModelListenerIfUnused(model); + } })); // Sync context key with model state - this._elementSelectionActiveContext.set(model.isElementSelectionActive); - store.add(model.onDidChangeElementSelectionActive(active => { - this._elementSelectionActiveContext.set(active); - this.accessibilityService.status(active - ? localize('browser.elementSelectionEnabled', "Element selection enabled. Press Enter to add the focused element to chat.") - : localize('browser.elementSelectionDisabled', "Element selection disabled.")); + this._elementSelectionMode = model.elementSelectionState.active ? model.elementSelectionState.options.mode : undefined; + this._elementSelectionModeContext.set(this._elementSelectionMode); + store.add(model.onDidChangeElementSelectionState(state => { + const wasCommenting = this._elementSelectionMode === BrowserElementSelectionMode.Comment; + this._elementSelectionMode = state.active ? state.options.mode : undefined; + this._elementSelectionModeContext.set(this._elementSelectionMode); + const isCommenting = this._elementSelectionMode === BrowserElementSelectionMode.Comment; + const accessibilityHelpHint = isCommenting && state.active + ? this.accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.BrowserElementCommenting) + : undefined; + this.accessibilityService.status(isCommenting + ? state.active + ? accessibilityHelpHint + ? localize('browser.elementCommentingEnabledWithAccessibilityHelp', "Element commenting enabled. Press Enter to comment on the focused element. {0}", accessibilityHelpHint) + : localize('browser.elementCommentingEnabled', "Element commenting enabled. Press Enter to comment on the focused element.") + : localize('browser.elementCommentingDisabled', "Element commenting disabled.") + : state.active + ? localize('browser.elementSelectionEnabled', "Element selection enabled. Press Enter to add the focused element to chat.") + : localize('browser.elementSelectionDisabled', "Element selection disabled.")); + if (isCommenting && !wasCommenting) { + this._commentSessionsWithComments.delete(model); + } else if (wasCommenting && !isCommenting && this._commentSessionsWithComments.delete(model)) { + this._focusChatInputForComments(model); + } })); this._areaSelectionActiveContext.set(model.isAreaSelectionActive); store.add(model.onDidChangeAreaSelectionActive(active => { @@ -205,7 +293,11 @@ export class BrowserEditorChatIntegration extends BrowserEditorContribution { } override onModelDetached(): void { - this._elementSelectionActiveContext.reset(); + if (this.editor.model) { + this._commentSessionsWithComments.delete(this.editor.model); + } + this._elementSelectionModeContext.reset(); + this._elementSelectionMode = undefined; this._areaSelectionActiveContext.reset(); } @@ -295,8 +387,8 @@ export class BrowserEditorChatIntegration extends BrowserEditorContribution { * {@linkcode IChatWidget.attachmentModel.addContext} so the attachment is * not silently discarded. */ - private async _revealChatWidgetForAttachment(): Promise<IChatWidget | undefined> { - const widget = await this.chatWidgetService.revealWidget() ?? this.chatWidgetService.lastFocusedWidget; + private async _revealChatWidgetForAttachment(preserveFocus = false): Promise<IChatWidget | undefined> { + const widget = await this.chatWidgetService.revealWidget(preserveFocus) ?? this.chatWidgetService.lastFocusedWidget; if (widget && !widget.viewModel) { await Event.toPromise(widget.onDidChangeViewModel); } @@ -318,7 +410,7 @@ export class BrowserEditorChatIntegration extends BrowserEditorContribution { // -- Element Selection ---------------------------------------------- - private async _attachElementDataToChat(elementData: IElementData, model: IBrowserViewModel) { + private async _attachElementDataToChat(elementData: IElementData, model: IBrowserViewModel): Promise<boolean> { const bounds = elementData.bounds; const toAttach: IChatRequestVariableEntry[] = []; @@ -349,7 +441,7 @@ export class BrowserEditorChatIntegration extends BrowserEditorContribution { }) : undefined; - toAttach.push({ + const elementEntry: IChatRequestVariableEntry = { id: 'element-' + Date.now(), name: displayNameShort, fullName: displayNameFull, @@ -364,13 +456,27 @@ export class BrowserEditorChatIntegration extends BrowserEditorContribution { innerText, imageData: screenshotBuffer?.buffer, imageMimeType: screenshotBuffer ? 'image/jpeg' : undefined, - }); + }; + toAttach.push(elementEntry); if (!await this._confirmContentAttachmentRisk(elementData.url ?? model.url)) { - return; + return false; } - if (!await this._attachToChat(toAttach)) { - return; + const widget = await this._revealChatWidgetForAttachment(elementData.comment !== undefined); + if (!widget?.attachmentModel || this._disposedCommentModels.has(model)) { + return false; + } + widget.attachmentModel.addContext(...toAttach); + if (elementData.comment !== undefined && elementData.elementId) { + if (!this._insertElementCommentReference(widget, model, elementEntry, toAttach.map(attachment => attachment.id), elementData.elementId, elementData.comment)) { + widget.attachmentModel.delete(...toAttach.map(attachment => attachment.id)); + return false; + } + if (model.elementSelectionState.active) { + this._commentSessionsWithComments.add(model); + } else { + widget.focusInput(); + } } type IntegratedBrowserAddElementToChatAddedEvent = { @@ -386,6 +492,182 @@ export class BrowserEditorChatIntegration extends BrowserEditorContribution { this.telemetryService.publicLog2<IntegratedBrowserAddElementToChatAddedEvent, IntegratedBrowserAddElementToChatAddedClassification>('integratedBrowser.addElementToChat.added', { attachImages }); + return true; + } + + private _insertElementCommentReference(widget: IChatWidget, browserModel: IBrowserViewModel, attachment: IChatRequestVariableEntry, attachmentIds: readonly string[], elementId: string, comment: string): boolean { + const inputModel = widget.inputEditor.getModel(); + const dynamicVariableModel = widget.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID); + if (!inputModel || !dynamicVariableModel) { + return false; + } + + const insertionPosition = widget.inputEditor.getPosition() ?? inputModel.getFullModelRange().getEndPosition(); + const prefix = insertionPosition.column > 1 ? '\n' : ''; + const suffix = insertionPosition.column < inputModel.getLineMaxColumn(insertionPosition.lineNumber) ? '\n' : ''; + const reference = `@${attachment.name}`; + const commentText = comment ? ` ${comment}` : ''; + const text = `${prefix}${reference}${commentText}${suffix}`; + if (!widget.inputEditor.executeEdits('browserElementComment', [{ range: Range.fromPositions(insertionPosition), text }])) { + return false; + } + const referenceStart = prefix ? { lineNumber: insertionPosition.lineNumber + 1, column: 1 } : insertionPosition; + const referenceRange = new Range(referenceStart.lineNumber, referenceStart.column, referenceStart.lineNumber, referenceStart.column + reference.length); + dynamicVariableModel.addReference(toAttachedContextDynamicVariable(attachment, referenceRange)); + widget.inputEditor.setPosition({ + lineNumber: referenceRange.endLineNumber, + column: referenceRange.endColumn + commentText.length + }); + + this._commentReferences.set(attachment.id, { elementId, attachmentIds, widget, browserModel }); + this._ensureCommentReferenceListeners(widget, dynamicVariableModel); + this._ensureCommentModelListeners(browserModel); + this._syncElementComments(browserModel); + return true; + } + + private _ensureCommentReferenceListeners(widget: IChatWidget, dynamicVariableModel: ChatDynamicVariableModel): void { + if (this._commentReferenceListeners.has(widget)) { + return; + } + const store = new DisposableStore(); + store.add(dynamicVariableModel.onDidChangeReferences(() => this._syncElementCommentsForWidget(widget))); + store.add(widget.inputEditor.onDidChangeModelContent(() => this._syncElementCommentsForWidget(widget))); + store.add(widget.attachmentModel.onDidChange(event => { + for (const [attachmentId, tracked] of this._commentReferences) { + if (tracked.widget === widget && event.deleted.includes(attachmentId)) { + this._removeElementCommentReference(tracked.browserModel, tracked.elementId); + } + } + })); + this._commentReferenceListeners.set(widget, store); + } + + private _ensureCommentModelListeners(browserModel: IBrowserViewModel): void { + if (this._commentModelListeners.has(browserModel)) { + return; + } + const store = new DisposableStore(); + store.add(browserModel.onDidRemoveElementComment(elementId => this._removeElementCommentReference(browserModel, elementId))); + store.add(browserModel.onDidNavigate(() => this._detachElementCommentReferences(browserModel))); + store.add(browserModel.onWillDispose(() => { + this._disposedCommentModels.add(browserModel); + this._detachElementCommentReferences(browserModel, false); + })); + this._commentModelListeners.set(browserModel, store); + } + + private _syncElementCommentsForWidget(widget: IChatWidget): void { + const browserModels = new Set<IBrowserViewModel>(); + for (const reference of this._commentReferences.values()) { + if (reference.widget === widget) { + browserModels.add(reference.browserModel); + } + } + for (const browserModel of browserModels) { + this._syncElementComments(browserModel); + } + } + + private _syncElementComments(browserModel: IBrowserViewModel, pendingCommentIdsToDiscard?: readonly string[]): void { + const comments: { elementId: string; body: string }[] = []; + for (const [attachmentId, tracked] of this._commentReferences) { + if (tracked.browserModel !== browserModel) { + continue; + } + const inputModel = tracked.widget.inputEditor.getModel(); + const dynamicVariableModel = tracked.widget.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID); + if (!inputModel || !dynamicVariableModel) { + continue; + } + const variable = dynamicVariableModel.variables.find(candidate => candidate.id === attachmentId && candidate.isAttachmentReference); + if (!variable) { + this._deleteCommentAttachments(attachmentId, tracked); + continue; + } + const line = inputModel.getLineContent(variable.range.endLineNumber); + comments.push({ + elementId: tracked.elementId, + body: line.slice(variable.range.endColumn - 1).trimStart() + }); + } + void browserModel.setElementComments({ comments, pendingCommentIdsToDiscard }); + } + + private _removeElementCommentReference(browserModel: IBrowserViewModel, elementId: string): void { + for (const [attachmentId, tracked] of this._commentReferences) { + if (tracked.browserModel !== browserModel || tracked.elementId !== elementId) { + continue; + } + const dynamicVariableModel = tracked.widget.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID); + const variable = dynamicVariableModel?.variables.find(candidate => candidate.id === attachmentId && candidate.isAttachmentReference); + const inputModel = tracked.widget.inputEditor.getModel(); + if (variable && inputModel) { + const lineNumber = variable.range.startLineNumber; + const lineRange = lineNumber < inputModel.getLineCount() + ? new Range(lineNumber, 1, lineNumber + 1, 1) + : lineNumber > 1 + ? new Range(lineNumber - 1, inputModel.getLineMaxColumn(lineNumber - 1), lineNumber, inputModel.getLineMaxColumn(lineNumber)) + : inputModel.getFullModelRange(); + tracked.widget.inputEditor.executeEdits('browserElementComment', [{ + range: lineRange, + text: '' + }]); + } + this._deleteCommentAttachments(attachmentId, tracked); + } + } + + private _detachElementCommentReferences(browserModel: IBrowserViewModel, syncComments = true): void { + this._commentSessionsWithComments.delete(browserModel); + const widgets = new Set<IChatWidget>(); + for (const [attachmentId, reference] of this._commentReferences) { + if (reference.browserModel === browserModel) { + widgets.add(reference.widget); + this._commentReferences.delete(attachmentId); + } + } + for (const widget of widgets) { + this._disposeCommentReferenceListenerIfUnused(widget); + } + this._commentModelListeners.deleteAndDispose(browserModel); + if (syncComments) { + void browserModel.setElementComments({ comments: [] }); + } + } + + private _focusChatInputForComments(browserModel: IBrowserViewModel): void { + for (const reference of this._commentReferences.values()) { + if (reference.browserModel === browserModel) { + reference.widget.focusInput(); + return; + } + } + } + + private _deleteCommentAttachments(elementAttachmentId: string, tracked: { attachmentIds: readonly string[]; widget: IChatWidget; browserModel: IBrowserViewModel }): void { + this._commentReferences.delete(elementAttachmentId); + tracked.widget.attachmentModel.delete(...tracked.attachmentIds); + this._disposeCommentReferenceListenerIfUnused(tracked.widget); + this._disposeCommentModelListenerIfUnused(tracked.browserModel); + } + + private _disposeCommentReferenceListenerIfUnused(widget: IChatWidget): void { + for (const reference of this._commentReferences.values()) { + if (reference.widget === widget) { + return; + } + } + this._commentReferenceListeners.deleteAndDispose(widget); + } + + private _disposeCommentModelListenerIfUnused(browserModel: IBrowserViewModel): void { + for (const reference of this._commentReferences.values()) { + if (reference.browserModel === browserModel) { + return; + } + } + this._commentModelListeners.deleteAndDispose(browserModel); } // -- Console Logs --------------------------------------------------- @@ -582,7 +864,7 @@ class AddElementToChatAction extends Action2 { icon: Codicon.inspect, f1: true, precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate(), ChatContextKeys.enabled), - toggled: CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, + toggled: CONTEXT_BROWSER_ELEMENT_SELECTION_MODE.isEqualTo(BrowserElementSelectionMode.Select), menu: { id: MenuId.BrowserChatActionsMenu, group: '1_element', @@ -593,19 +875,82 @@ class AddElementToChatAction extends Action2 { weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over terminal primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC, args: { highlightFocusedElement: true }, - }, { - when: CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.Escape }] }); } - async run(accessor: ServicesAccessor, argument?: IBrowserElementSelectionOptions | BrowserEditor): Promise<void> { + run(accessor: ServicesAccessor, argument?: IBrowserElementSelectionOptions | BrowserEditor): void { const browserEditor = argument instanceof BrowserEditor ? argument : accessor.get(IEditorService).activeEditorPane; if (browserEditor instanceof BrowserEditor) { browserEditor.ensureBrowserFocus(); - void browserEditor.model?.toggleElementSelection(undefined, argument instanceof BrowserEditor ? undefined : argument); + const model = browserEditor.model; + if (model) { + const options = argument instanceof BrowserEditor ? undefined : argument; + const isActiveMode = model.elementSelectionState.active && model.elementSelectionState.options.mode !== BrowserElementSelectionMode.Comment; + void model.toggleElementSelection(!isActiveMode, { ...options, continuous: false, mode: BrowserElementSelectionMode.Select }); + } + } + } +} + +class AddElementCommentToChatAction extends Action2 { + static readonly ID = BrowserViewCommandId.AddElementCommentToChat; + + constructor() { + super({ + id: AddElementCommentToChatAction.ID, + title: localize2('browser.addElementCommentToChatAction', 'Comment on Elements'), + category: BrowserCategory, + icon: Codicon.comment, + f1: true, + precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate(), ChatContextKeys.enabled), + toggled: CONTEXT_BROWSER_ELEMENT_SELECTION_MODE.isEqualTo(BrowserElementSelectionMode.Comment), + menu: { + id: MenuId.BrowserChatActionsMenu, + group: '1_element', + order: 2, + when: ChatContextKeys.enabled + }, + keybinding: [{ + weight: KeybindingWeight.WorkbenchContrib + 50, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC, + args: { continuous: true, mode: BrowserElementSelectionMode.Comment, highlightFocusedElement: true } + }], + }); + } + + run(accessor: ServicesAccessor, argument?: IBrowserElementSelectionOptions | BrowserEditor): void { + const browserEditor = argument instanceof BrowserEditor ? argument : accessor.get(IEditorService).activeEditorPane; + if (browserEditor instanceof BrowserEditor) { + browserEditor.ensureBrowserFocus(); + const options = argument instanceof BrowserEditor ? undefined : argument; + const model = browserEditor.model; + if (model) { + const isActiveMode = model.elementSelectionState.active && model.elementSelectionState.options.mode === BrowserElementSelectionMode.Comment; + void model.toggleElementSelection(!isActiveMode, { ...options, continuous: true, mode: BrowserElementSelectionMode.Comment }); + } + } + } +} + +class StopElementSelectionAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.browser.stopElementSelection', + title: localize2('browser.stopElementSelectionAction', 'Stop Element Selection'), + precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, ContextKeyExpr.has(CONTEXT_BROWSER_ELEMENT_SELECTION_MODE.key)), + keybinding: { + when: ContextKeyExpr.has(CONTEXT_BROWSER_ELEMENT_SELECTION_MODE.key), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape + } + }); + } + + run(accessor: ServicesAccessor): void { + const browserEditor = accessor.get(IEditorService).activeEditorPane; + if (browserEditor instanceof BrowserEditor) { + void browserEditor.model?.toggleElementSelection(false); } } } @@ -623,8 +968,8 @@ class AddConsoleLogsToChatAction extends Action2 { precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate(), ChatContextKeys.enabled), menu: { id: MenuId.BrowserChatActionsMenu, - group: '1_element', - order: 2, + group: '2_logs', + order: 1, when: ChatContextKeys.enabled } }); @@ -650,7 +995,7 @@ class AddScreenshotToChatAction extends Action2 { precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate(), ChatContextKeys.enabled), menu: { id: MenuId.BrowserChatActionsMenu, - group: '2_screenshots', + group: '3_screenshots', order: 1, when: ChatContextKeys.enabled } @@ -678,7 +1023,7 @@ class AddAreaScreenshotToChatAction extends Action2 { toggled: CONTEXT_BROWSER_AREA_SELECTION_ACTIVE, menu: { id: MenuId.BrowserChatActionsMenu, - group: '2_screenshots', + group: '3_screenshots', order: 2, when: ChatContextKeys.enabled } @@ -706,7 +1051,7 @@ class AddFullPageScreenshotToChatAction extends Action2 { precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate(), ChatContextKeys.enabled, enabledSetting), menu: { id: MenuId.BrowserChatActionsMenu, - group: '2_screenshots', + group: '3_screenshots', order: 3, when: ContextKeyExpr.and(ChatContextKeys.enabled, enabledSetting) } @@ -721,6 +1066,8 @@ class AddFullPageScreenshotToChatAction extends Action2 { } registerAction2(AddElementToChatAction); +registerAction2(AddElementCommentToChatAction); +registerAction2(StopElementSelectionAction); registerAction2(AddConsoleLogsToChatAction); registerAction2(AddScreenshotToChatAction); registerAction2(AddAreaScreenshotToChatAction); @@ -735,7 +1082,10 @@ MenuRegistry.appendMenuItem(MenuId.BrowserActionsToolbar, { group: BrowserActionGroup.Tools, order: 1, when: ChatContextKeys.enabled, - isSplitButton: true + isSplitButton: { + togglePrimaryAction: true, + primaryActionIds: [AddElementToChatAction.ID, AddElementCommentToChatAction.ID] + } }); Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({ From 7d2d8fba66131bfdc4cecfffd426c4551d38463f Mon Sep 17 00:00:00 2001 From: Megan Rogge <merogge@microsoft.com> Date: Fri, 31 Jul 2026 12:37:19 -0400 Subject: [PATCH 77/86] Dictation: use multilingual Nemotron 3.5 (#328420) * Dictation: use multilingual Nemotron model Switch on-device dictation to Nemotron 3.5 and resolve its language from the Voice Mode setting and browser locale, with model auto-detection as a fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5f00f75-49d2-4b4f-a503-050b7b6a2f57 * Fix dictation locale allowlist Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com> Copilot-Session: d5f00f75-49d2-4b4f-a503-050b7b6a2f57 --- .../common/localTranscription.ts | 13 ++-- .../node/localTranscriptionService.ts | 5 -- .../browser/agentsVoice.contribution.ts | 2 +- .../chat/browser/chat.shared.contribution.ts | 22 ++++-- .../speechToText/chatSpeechToTextService.ts | 13 +++- .../browser/speechToText/dictationLanguage.ts | 74 +++++++++++++++++++ .../browser/chatSpeechToTextService.test.ts | 27 +++++++ 7 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts diff --git a/src/vs/platform/localTranscription/common/localTranscription.ts b/src/vs/platform/localTranscription/common/localTranscription.ts index d0a2423e3a0..2678c915955 100644 --- a/src/vs/platform/localTranscription/common/localTranscription.ts +++ b/src/vs/platform/localTranscription/common/localTranscription.ts @@ -13,7 +13,7 @@ export const ILocalTranscriptionService = createDecorator<ILocalTranscriptionSer export const localTranscriptionChannelName = 'localTranscription'; /** Default on-device model used for dictation. */ -export const DEFAULT_LOCAL_TRANSCRIPTION_MODEL = 'nemotron-speech-streaming-en-0.6b'; +export const DEFAULT_LOCAL_TRANSCRIPTION_MODEL = 'nemotron-3.5-asr-streaming-0.6b'; export interface ILocalTranscriptionModelImportResult { readonly model: string; @@ -72,12 +72,11 @@ export interface ILocalTranscriptionResult { /** * On-device speech-to-text using a downloaded model. Transcription runs through * Microsoft's Foundry Local streaming ASR engine (onnxruntime + onnxruntime-genai - * native runtime), which handles decoding, VAD and endpointing internally; the - * default model is NVIDIA's `nemotron-speech-streaming-en-0.6b` streaming RNN-T - * (the model the GitHub Copilot app ships for dictation). The model is chosen by - * the `dictation.model` setting. Runs in a utility process. A single - * transcription session is active at a time (dictation is a singleton in the - * renderer). + * native runtime), which handles decoding, VAD and endpointing internally. The + * default model is NVIDIA's multilingual Nemotron 3.5 streaming RNN-T. The model + * is chosen by the `dictation.model` setting. Runs in a utility process. A + * single transcription session is active at a time (dictation is a singleton + * in the renderer). * * The renderer streams PCM16 mono 16 kHz audio via `pushAudio`; the service * emits interim transcripts on `onDidTranscribe` and a final one after `stop`. diff --git a/src/vs/platform/localTranscription/node/localTranscriptionService.ts b/src/vs/platform/localTranscription/node/localTranscriptionService.ts index a036f2a3a6e..4f03794a170 100644 --- a/src/vs/platform/localTranscription/node/localTranscriptionService.ts +++ b/src/vs/platform/localTranscription/node/localTranscriptionService.ts @@ -24,11 +24,6 @@ const SAMPLE_RATE = 16000; const CHANNELS = 1; const BITS_PER_SAMPLE = 16; -/** - * Default on-device model. `nemotron-speech-streaming-en-0.6b` is the NVIDIA - * Nemotron streaming RNN-T model the GitHub Copilot app ships for dictation; it - * runs through Foundry Local's native streaming ASR engine (ORT + ORT-GenAI). - */ /** Application name reported to Foundry Local for logs/telemetry and its data dir. */ const FOUNDRY_APP_NAME = 'vscode-dictation'; diff --git a/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts b/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts index ab03849b92e..eb9dea4dc1c 100644 --- a/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts +++ b/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts @@ -652,7 +652,7 @@ configurationRegistry.registerConfiguration({ nls.localize('agents.voice.language.ko', "Korean"), nls.localize('agents.voice.language.zh', "Chinese"), ], - markdownDescription: nls.localize('agents.voice.language', "The language used for speech recognition and spoken responses. The selectable languages support native voice output. Automatic follows the system or browser locale for speech recognition and uses English voice output when the detected language does not support native voice output. Changing this while voice mode is connected takes effect immediately."), + markdownDescription: nls.localize('agents.voice.language', "The language used for speech recognition, dictation, and spoken responses. The selectable languages support native voice output. Automatic follows the system or browser locale for speech recognition and dictation, and uses English voice output when the detected language does not support native voice output. Changing this while voice mode is connected takes effect immediately."), default: 'auto', scope: ConfigurationScope.APPLICATION, }, 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 78309b6a26e..9221889ef7a 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts @@ -14,6 +14,7 @@ import '../../../../platform/agentHost/browser/agentHostEnablementService.js'; import '../../../../platform/agentHost/common/agentHostStarter.config.contribution.js'; import { AgentHostAhpJsonlLoggingSettingId, AgentHostSdkSandboxEnabledSettingId, ClaudePreferAgentHostAgentsSettingId, ClaudePreferAgentHostEditorSettingId, CodexPreferAgentHostEditorSettingId } from '../../../../platform/agentHost/common/agentService.js'; import { AgentHostCopilotSdkLogLevelSettingId, AgentHostCustomTerminalToolEnabledSettingId, AgentHostModelCapabilityOverridesSettingId, AgentHostOpus48PromptEnabledSettingId, AgentHostReasoningEffortOverrideSettingId, AgentHostToolSearchEnabledSettingId, copilotSdkLogLevelSettingValues } from '../../../../platform/agentHost/common/copilotCliConfig.js'; +import { DEFAULT_LOCAL_TRANSCRIPTION_MODEL } from '../../../../platform/localTranscription/common/localTranscription.js'; import { AgentNetworkFilterService, IAgentNetworkFilterService } from '../../../../platform/networkFilter/common/networkFilterService.js'; import { AgentNetworkDomainSettingId } from '../../../../platform/networkFilter/common/settings.js'; import { COPILOT_ALLOWED_MCP_SERVERS_KEY, COPILOT_DENIED_MCP_SERVERS_KEY, COPILOT_DISABLE_BYPASS_PERMISSIONS_MODE_KEY, COPILOT_ENABLED_PLUGINS_KEY, COPILOT_EXTRA_MARKETPLACES_KEY, COPILOT_MODEL_KEY, COPILOT_STRICT_MARKETPLACES_KEY, managedModelValue, managedSettingValue } from '../../../../platform/policy/common/copilotManagedSettings.js'; @@ -282,19 +283,19 @@ configurationRegistry.registerConfiguration({ 'dictation.model': { type: 'string', enum: [ - 'nemotron-speech-streaming-en-0.6b', + DEFAULT_LOCAL_TRANSCRIPTION_MODEL, 'mai', ], enumItemLabels: [ - nls.localize('dictation.model.nemotronStreaming.label', "Nemotron Streaming (English) — On-Device"), + nls.localize('dictation.model.nemotronStreaming.label', "Nemotron 3.5 ASR (Multilingual) — On-Device"), nls.localize('dictation.model.mai.label', "MAI — Cloud"), ], markdownEnumDescriptions: [ - nls.localize('dictation.model.nemotronStreaming', "NVIDIA Nemotron streaming RNN-T (English), run on-device through Microsoft Foundry Local. Works offline; no audio leaves the device. Downloaded on first use and cached on disk."), + nls.localize('dictation.model.nemotronStreaming', "NVIDIA Nemotron 3.5 multilingual streaming RNN-T, run on-device through Microsoft Foundry Local. Works offline; no audio leaves the device. Automatic language selection follows the Voice Mode language setting and system or browser locale, with model detection as a fallback. Downloaded on first use and cached on disk."), nls.localize('dictation.model.mai', "Cloud transcription through the same Microsoft AI voice service used by Voice Mode. Requires a network connection and GitHub sign-in; audio is streamed to the service."), ], markdownDescription: nls.localize('dictation.model', "The model used for dictation. On-device models download on first use and run locally through Microsoft Foundry Local; the cloud option streams audio to the Microsoft AI voice service."), - default: 'nemotron-speech-streaming-en-0.6b', + default: DEFAULT_LOCAL_TRANSCRIPTION_MODEL, tags: ['experimental'], experiment: { mode: 'auto' } }, @@ -2269,9 +2270,10 @@ Registry.as<IConfigurationMigrationRegistry>(Extensions.ConfigurationMigration). 'onnx-community/whisper-base', 'onnx-community/whisper-small', 'onnx-community/nemotron-3.5-asr-streaming-0.6b-onnx-int4', + 'nemotron-speech-streaming-en-0.6b', ]; const migrated = (typeof value === 'string' && legacyModelIds.includes(value)) - ? 'nemotron-speech-streaming-en-0.6b' + ? DEFAULT_LOCAL_TRANSCRIPTION_MODEL : value; const pairs: ConfigurationKeyValuePairs = [['chat.speechToText.model', { value: undefined }]]; // Never clobber an explicitly configured new key (e.g. after settings @@ -2282,6 +2284,16 @@ Registry.as<IConfigurationMigrationRegistry>(Extensions.ConfigurationMigration). return pairs; } }, + { + // Existing users may have the former English-only default stored + // explicitly. Move them to the multilingual replacement as well. + key: 'dictation.model', + migrateFn: value => ({ + value: value === 'nemotron-speech-streaming-en-0.6b' + ? DEFAULT_LOCAL_TRANSCRIPTION_MODEL + : value + }) + }, { // Dictation settings were regrouped under the top-level `dictation.*` // namespace (they govern dictation across chat, editor, and terminal). diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts b/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts index 37366b0cbc5..717c1897260 100644 --- a/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts +++ b/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts @@ -34,6 +34,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatMessageRole, ILanguageModelsService } from '../../common/languageModels.js'; import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; import { createPcmCaptureNode } from '../pcmCaptureWorklet.js'; +import { resolveDictationLanguage } from './dictationLanguage.js'; export const IChatSpeechToTextService = createDecorator<IChatSpeechToTextService>('chatSpeechToTextService'); @@ -167,7 +168,7 @@ const PCM_CAPTURE_CHUNK_SIZE = 4096; const ENABLED_SETTING = 'dictation.enabled'; /** * Selects the dictation model. On-device model ids (e.g. - * `nemotron-speech-streaming-en-0.6b`) run through {@link ILocalTranscriptionService}; + * `nemotron-3.5-asr-streaming-0.6b`) run through {@link ILocalTranscriptionService}; * the sentinel {@link DICTATION_MAI_MODEL_ID} routes to the cloud voice service instead. */ export const DICTATION_MODEL_SETTING = 'dictation.model'; @@ -827,7 +828,7 @@ export class ChatSpeechToTextService extends Disposable implements IChatSpeechTo if (this._activeBackend === 'mai') { return this._startMaiSession(window); } - return this._startLocalSession(); + return this._startLocalSession(window); } /** @@ -1133,7 +1134,7 @@ export class ChatSpeechToTextService extends Disposable implements IChatSpeechTo * Begin an on-device transcription session in the utility process and pipe * its interim/final results onto the shared cumulative-transcript surface. */ - private async _startLocalSession(): Promise<void> { + private async _startLocalSession(window: Window & typeof globalThis): Promise<void> { const local = this._localTranscription; this._localSessionDisposables.add(local.onDidTranscribe(result => { // The local service returns the full cumulative transcript each time. @@ -1141,7 +1142,11 @@ export class ChatSpeechToTextService extends Disposable implements IChatSpeechTo })); const cacheDir = joinPath(this._environmentService.cacheHome, 'chatDictationModels').fsPath; const model = this._getModelId(); - await local.start({ cacheDir, model }); + const language = resolveDictationLanguage( + this._configurationService.getValue('agents.voice.language'), + window.navigator.language, + ); + await local.start({ cacheDir, model, language }); // The model loads in the utility process in the background (start() // returns immediately). On first use it may download hundreds of MB, so diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts b/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts new file mode 100644 index 00000000000..3b1180256ff --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const NEMOTRON_LOCALES = new Set([ + 'ar-AR', 'bg-BG', 'cs-CZ', 'da-DK', 'de-DE', 'en-GB', 'en-US', 'es-ES', + 'es-US', 'et-EE', 'fi-FI', 'fr-CA', 'fr-FR', 'el-GR', 'he-IL', 'hi-IN', + 'hr-HR', 'hu-HU', 'it-IT', 'ja-JP', 'ko-KR', 'lt-LT', 'lv-LV', 'mt-MT', + 'nb-NO', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-BR', 'pt-PT', 'ro-RO', 'ru-RU', + 'sk-SK', 'sl-SI', 'sv-SE', 'th-TH', 'tr-TR', 'uk-UA', 'vi-VN', 'zh-CN', +]); + +const NEMOTRON_DEFAULT_LOCALE_BY_LANGUAGE: Readonly<Record<string, string>> = { + ar: 'ar-AR', + bg: 'bg-BG', + cs: 'cs-CZ', + da: 'da-DK', + de: 'de-DE', + en: 'en-US', + es: 'es-US', + et: 'et-EE', + el: 'el-GR', + fi: 'fi-FI', + fr: 'fr-FR', + he: 'he-IL', + hi: 'hi-IN', + hr: 'hr-HR', + hu: 'hu-HU', + it: 'it-IT', + ja: 'ja-JP', + ko: 'ko-KR', + lt: 'lt-LT', + lv: 'lv-LV', + mt: 'mt-MT', + nb: 'nb-NO', + nl: 'nl-NL', + nn: 'nn-NO', + pl: 'pl-PL', + pt: 'pt-PT', + ro: 'ro-RO', + ru: 'ru-RU', + sk: 'sk-SK', + sl: 'sl-SI', + sv: 'sv-SE', + th: 'th-TH', + tr: 'tr-TR', + uk: 'uk-UA', + vi: 'vi-VN', + zh: 'zh-CN', +}; + +/** + * Resolve the on-device dictation language using the same setting semantics as + * Voice Mode. Automatic follows the browser locale when Nemotron supports it, + * then falls back to the model's language detection. + */ +export function resolveDictationLanguage(configuredLanguage: unknown, browserLanguage: string | undefined): string { + const configured = typeof configuredLanguage === 'string' ? configuredLanguage.trim() : ''; + const candidate = configured && configured.toLowerCase() !== 'auto' ? configured : browserLanguage; + if (!candidate || typeof Intl.getCanonicalLocales !== 'function') { + return 'auto'; + } + + try { + const canonical = Intl.getCanonicalLocales(candidate)[0]; + if (NEMOTRON_LOCALES.has(canonical)) { + return canonical; + } + return NEMOTRON_DEFAULT_LOCALE_BY_LANGUAGE[canonical.split('-')[0]] ?? 'auto'; + } catch { + return 'auto'; + } +} diff --git a/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts index 9fec4617435..7c0b4bb5f00 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts @@ -5,12 +5,39 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { resolveDictationLanguage } from '../../browser/speechToText/dictationLanguage.js'; import { createDictationCleanupSystemPrompt, createIncrementalDictationTranscript, getIncrementalDictationCleanupRange, stripDictationFillers } from '../../browser/speechToText/chatSpeechToTextService.js'; suite('ChatSpeechToTextService', () => { ensureNoDisposablesAreLeakedInTestSuite(); + test('resolves the dictation language from Voice Mode configuration and browser locale', () => { + assert.deepStrictEqual({ + explicit: resolveDictationLanguage('fr-FR', 'de-DE'), + automatic: resolveDictationLanguage('auto', 'uk-UA'), + regionalAutomatic: resolveDictationLanguage('auto', 'pt-BR'), + additionalSupportedAutomatic: resolveDictationLanguage('auto', 'he-IL'), + unsupportedRegion: resolveDictationLanguage('auto', 'en-AU'), + explicitSpanish: resolveDictationLanguage('es', 'en-US'), + explicitAdaptationReady: resolveDictationLanguage('lt', 'en-US'), + regionalPortugueseFallback: resolveDictationLanguage('auto', 'pt-AO'), + invalidExplicit: resolveDictationLanguage('not a locale', 'de-DE'), + missing: resolveDictationLanguage(undefined, undefined), + }, { + explicit: 'fr-FR', + automatic: 'uk-UA', + regionalAutomatic: 'pt-BR', + additionalSupportedAutomatic: 'he-IL', + unsupportedRegion: 'en-US', + explicitSpanish: 'es-US', + explicitAdaptationReady: 'lt-LT', + regionalPortugueseFallback: 'pt-PT', + invalidExplicit: 'auto', + missing: 'auto', + }); + }); + test('shows each cleaned prefix with the remaining raw transcript', () => { assert.deepStrictEqual( [ From bf83dbbf3b46730423c406023e7fb9d0ecd31f68 Mon Sep 17 00:00:00 2001 From: Megan Rogge <merogge@microsoft.com> Date: Fri, 31 Jul 2026 12:40:08 -0400 Subject: [PATCH 78/86] Localized voice previews for Voice Mode onboarding (#328259) --- .../browser/media/de_marc_neutral.mp3 | Bin 0 -> 134253 bytes .../browser/media/es-ES_maria_neutral.mp3 | Bin 0 -> 111501 bytes .../browser/media/fr_david_neutral.mp3 | Bin 0 -> 182925 bytes .../browser/media/it_eva_neutral.mp3 | Bin 0 -> 182349 bytes .../browser/media/ja_aruha_neutral.mp3 | Bin 0 -> 185229 bytes .../browser/media/ko_jiyon_neutral.mp3 | Bin 0 -> 195021 bytes .../browser/media/pt-BR_gil_neutral.mp3 | Bin 0 -> 137997 bytes .../browser/media/zh_wuzhi_neutral.mp3 | Bin 0 -> 200493 bytes .../browser/voiceModeOnboarding.ts | 193 ++++++++++++++---- .../test/browser/voiceModeOnboarding.test.ts | 47 +++++ 10 files changed, 204 insertions(+), 36 deletions(-) create mode 100644 src/vs/workbench/contrib/agentsVoice/browser/media/de_marc_neutral.mp3 create mode 100644 src/vs/workbench/contrib/agentsVoice/browser/media/es-ES_maria_neutral.mp3 create mode 100644 src/vs/workbench/contrib/agentsVoice/browser/media/fr_david_neutral.mp3 create mode 100644 src/vs/workbench/contrib/agentsVoice/browser/media/it_eva_neutral.mp3 create mode 100644 src/vs/workbench/contrib/agentsVoice/browser/media/ja_aruha_neutral.mp3 create mode 100644 src/vs/workbench/contrib/agentsVoice/browser/media/ko_jiyon_neutral.mp3 create mode 100644 src/vs/workbench/contrib/agentsVoice/browser/media/pt-BR_gil_neutral.mp3 create mode 100644 src/vs/workbench/contrib/agentsVoice/browser/media/zh_wuzhi_neutral.mp3 diff --git a/src/vs/workbench/contrib/agentsVoice/browser/media/de_marc_neutral.mp3 b/src/vs/workbench/contrib/agentsVoice/browser/media/de_marc_neutral.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ea6bb7b1c24f48d2b3e44ccdb7ccec33b33a620d GIT binary patch literal 134253 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 literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/agentsVoice/browser/media/es-ES_maria_neutral.mp3 b/src/vs/workbench/contrib/agentsVoice/browser/media/es-ES_maria_neutral.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e6b6cac8669c452bd577a6672c1035de40df26b0 GIT binary patch literal 111501 zcmcfJbyOQq_$d4&1P>5Aq&UG{i#NEt6nD4cg$fPsZo%E9xYOcJaV@3L0;O1?N{i&e z_rB+M?mhSacTP^So86O4KKtxsGS6((6otWn`@&*qV4!sONCE(0Y1#%li3#xu-rWQQ z|J(Y%?=U~g2mWVO&CBV@-OAnmz%T%?)dqltjYmiXC8wl@(=jl!vUBqA35tk`OUWV? zlvUNWboGrMn^{=fIyk#}c=`AR20sr$g+<21C8nfhWas7=m6li5)HOD@c69aj4-Ah^ zOwG<MuB>ftZSU?Kd_6fk|MC0s>h}K4?<!}1S2-c^|EbB{0s_qV-|AgGdhEnc{?ASS zKQ7<-i~uC^CAJXH$r2QpafczFgQ^VZ!S$jJA__B%SfO@+d~7hnTQ=eZhmcC(i)E|0 zWDG4*HK!eCV8c!+3>C@@C{A<5o?Mlqu}RRj>{7s+T>!9cEmGK^hD5Ex+162lZjqae zqp-^|!cIv!>`CSgtkeX2(PHE%qa1~2b?71zm8xf&ko&14g>9Rfav6^{m3>QvS^iX9 z7X>xywWJMqu%XrNYJiKzeg&I{{RkV0ocvh~VD)lS+Jl%_%s)^AVatidvc$Z%q*K<j za}!)faJ?{2z>0cbSQ96ptgWn_PWNB}#xd-DBq+%!pu8E!we0(>{ITb;5j`naIh<;G z&~;!Ym|!GKemKmQ5p8pHTBdfQpQ@IzlSrU?@53XIAxUegsR~F*;VB&LYJQWemxG)v zHE#PV!7364ekbzgfyID#Orny$$TXssRCT0SHSvqZ2RT~0s`5ojIAJ7ShOZ`H;U2GF zR%S2xd3aHGe-t9z(_t6eyyCo!dzK;cMq~5TPB%~MUHWVMrEBXamI2(ax&Jy-=|x?r z9T6TW*X}=P?0-ekKlGgB?VOMcVV+zs$D8G;3@7Eq{=g$r2o!=1vPIcA0!1wbJU|K9 z0O<kRM>7#B+L=YDQ0d_j&KXPGn6cu<Xe5y(ngKP`N|V^mIU4u8siWf<9RxtXlR&!x z^ROv$^=06z;Iat2ex|tHiFw&Ia_Q}8=!h-HXbB@kDJ1awxi@rrNv{QgYJx-YE4p+c z_dYMo+9YNA3l9NYs27+o{)NQW4JvYJFbE(Wa31kdW3TvBT}`y_sf_q(c|r6o8v=(K zHayTZ^68Kw1RX{&{^&2}-*B8zawv+qbF`Fr&vBg<IKzuQgAExEd%BX8t!g4dh4mI= zeTjLL(;AC|vA$UzMKl+5og*Gn52Gy{N$R*UUof{Xd$+DW_z}H@eSxN1#~)(?KL7Zk zpGD345->>fY93Dn{|1o?1Ok{Z2*MaJ+pjRMt}u;$MGd}bxNr3tZ?%Qgx^Ew!B<+15 zl95Vl{%M1vun{LV53u)TsWo0UY+lP!V%3aY523cRMHAqnn*cJ2R<Qt}u-X25b7Puv zFP;pF-eeM<s$ZE@B?KnJB<khU#B^<)ocBHm<r&E_>x}_W2zg%=`3sRgr#Zg;SAE-K zOvYju{b_`@dC$g&Zhxhb&_8zVHQ=lj(LAbptJO2}acc|)^RIz3UO9g^yj*`FY+b?L zB6(l5Hduo^mIqH{XxTXyK9-l;S6J+^s9yi_IwxNB<K*QJoM$FO#|ajN&unW>-ZFf{ zyux744@?~u>i=wJTV8&kyXwnUapJV`D1G>`n{brcMO$<EjTdo9nMaFfid^tuYL}B5 z(Z1b7*+a_|-Kov%tG_fMW=k(=JgdK!wn&iwfp#h+WU0)JTUL}bIMeG*xZBTRZl1oy zU_Q7@SWIIw_A1tvjC&@^e4=NEr|3QTSL)mX{xO1(wOx3D%vj}E02mA=WSE5gJT^|? z-sb`Gr(_V|tMwkJL=LrspNDUoD;MJ(W`l&Af&+!171Y)<wAc`AEW#8XEEs|fh6E~7 zNx%2y9qwkxG_dOxdfV5w?tF#GXkPn*8UNCIb^7Mj?U~MTmXYOLGh`TFKNAN#R32$> zWief=n)Vf#C8X*NCTXcIFjicv@K0ZT(^dtksVUAmT0xS>MMO?_D}8_Uk9<BcLsgO| zDgcNLt4PCn_0DRtSH3=dbXykYbM@PxM}(5w^@SwGl#v~eF$p`Bi@0{Pl}BGha^^=2 zCim##!hGcC+HFUraKh)8CwSkS6&=G_dkh5C*Dv4l)?UyJ2-X-KW+lD;tn{4F4zJG! z3br+?lx2R%%EB+e0T$Rlz!QE<Hf!mG9e(e_C2uGh1U_RR8&#-Yy$BjW`PbuUW;_QZ zDTfx9k4uE3T4v(1PnnZ+wWvIj0aFx4-%=Ek)(5DSB|;!X6Qwt+PGqq+mW7r{<!|<L zFI-0U9#S)zxUVGXJ%gvfZ5jeh?c<Q;qo)eX&^7$yTtUn&dFrkAwk7isn`0mE$9~(M z_N=Vg0PA>j%qix)weBP4T66ft*Rvlqp@e?rjs9c5oDjn=eIo5>K)JJvPUc2WJ4ati z`5wMWn>K)wAAT5*j_+#Omdbv68S8!*iLc&v@R}Xdv?4UjMdB;`T?opLb|>3=IZx{- zA+lq-n}Jkh>Ev?4-~lKU$41xCj<MYKg`{&zk_dj|zXf5KwvCT&C!L#Pqiw9XeGuT@ zM_9332*q)hdo#9|{J>IJmLz7NIh}(ttfdKWX&Mjyg3e(+2FNSQLSoz6kueOxCQ-Z~ zD5C|_(q&J6#sZAk=;E?0Q^naZM&XgUY4JBzhYRS=P$)+Q0U%cCcrh!)+9rA&X>BuF z>nal_JD^Y5bnbo7Y@&mtda1Ww4?@bJBL>z}!7-K3Vl~R3dwVz9Yy8<Hk~6d06%R3( z-}>;6X6kDAu|p;s%US-D<cE?OUfyY}35$9QTh#WTa9C`R0^^C)Bx&2Max6z~o5{b~ zG^G?R3XeZeeq+9Nyi4OK&IwxoT9cYn%Q}FocGbyQeTC&TW6B5-DN>uTt5Eh0ao-tj zd44Kae<9$eW6&!gj?7@oTKUR6^}Krm`#&Fv5rS0Z_j4mb&1fY|xgCK>tn0I-4@NdG z;Qbr#yL-GcwD!Ci6$L<{<%ADjtfnQxCN38^4gmmY+klM$wj7&?e#P?^8@u9R$3VDU z5|?9&F7e5mlf-$O+OK>0uRW-MVGf*h;sV6a>o%6c=bs$?IIGx6DTy+-oCJMEPKt3} zTvVemS7o^_eJ&>^t9i+ktRh|?7jBb(+aCobiQ*H)gPSGT1va%?n9^tSFqqlv(KA`s zrDSF&Gge9zDE6;!uD&{S%~4p+Orx=O40cie+iilG^W`kWZ@lfI3eIeHL5jr#?K|fo zi}-<qtq5NnwQ+9<?U)ra2WY@8uyJclpHBZqCmt^N?(hulzEv}xUZv9!?WB7tNp$aH zt*|Vask~%x;wg~A<0A1456DiFjF-c@r&?O#&xTS5qQd2gL1ezgEDCT13spyRRv#1L zpGo{>uB@(_`1w72$2F80OFidAUM*P}Ypg`-1<`}GuT|7*cl$nr<QSp`vn({fqEAH4 z+@ET_y`4LfKY*ueDaX*6zQt7L)hsA^=?Sr_fl`M|{7A^LDXX8+^D(YjV=#G$^23F= zQFrlt-CY_1Nt{8kQV<YA6npw9CJq~1_^h~$GBUyju-L76<ft8$$R?D>IqSWn(Ji|L zo@cY8gkpn1>d3xtEo|@4!tn7?A}61qpSoUEtez--@2pY8mCA=#x>H7THjxox8L}Ks zf=M~l6K1wX?cL{A@s(9scfpO)xykoFcH)YXZ`hWo^*JN#a-8Y3I}ja3b%al~k4@lL zeM-jvev#B!^?!a)XY8WR(rz%r*Exr|E7$Fv&$lbKUUM-&b!wZf$HZo1+Ouy?DtH}S ze^mR-#(3!HjT*kRe)Whv3?FsIYt2ZMOqDk7jZ%C@ue`J61~#x4QnT4gL(+ej)vsoc z$7z+)l+#iYzU$u$82#@O>4$)S``7BNXj}3)+9vJ0%=$O=?W18R{G8_~t6T8X4&!x4 zkI%ZC3~C4M=xHTQl^ZJ{*r9@-_9c7c$$R^+{p(>eo|VSKiZ$`-nc`b+({ZogUT=JR zr6KU60<4syMkkrXXo_w_IgBd5WQsnPmb-F}&MN;$G)h}#%z=aKRt{q$y!VM<-T3q_ zY8diU=6GRr-IfJ6L7a_;E||Pt&M#g4e=1#DTN~cji=BhFc+@FIL@fj`0U^*)h#-(; zVs%bxdakOz5iy8oNqa;lc=(Bc;CF9)*Ukr_042~&#>td=0!KnO2bO?fD<iL93A9EH zU6N}uSiqd4>lzQh0kDUn!ddmNwF8)s*yXQPsMXGFeN~j4sx|Q{YwJ_Bc!x5|K{(U_ z50C>;L4}D7P>o!8c>!TSC@*Wu$9~>E$VTyTVw&PwUY?IL`RwQ|oe~u9of!sKf;!%= zasFizbF~Ax55C)8D2eY-r_({D?lCJm(zJ&9pX^i1k9%K6{O%6-9~nbYvQi?V$ofPY zRW<Px$H@_8LfQY?pC59izY~hLagFozZVo1u3@2hu2H|nVJnYgUv9<rF&mGZ!1&jFm zmZa`^8Tt2C91(E%>jRjDV0qTQCMBKdPIED0mLLO8nNx%lmkxvBH}lg5FN6atn_?{^ z!-3o0wj0&yq%7=1__OGV8Aa$pZYeopJ*a46Ufk4D?&tAM?yzBlt(y39184072V9*7 zN%lP9Ywp|RmMC1+ZZ$+rf;p}i+ptEKNM;?Ep7H22{LiF$mDz&SZ%#lXlt&ezMur(` zv_Dh`59&1}$t2m?3T$#(!1?UKl$lCjZJP-7Jig6@{%SX{m>PX^J3})wFL(WRl%F4S zETk)+5DmNB)9$#js_}jO<4Xmbi$3|lef%6h>Jy4ZZsQuN5kA$hCL*-s|L8T!#={}e z_}1q5e|-MGpf@r?0YIUv{UoheeqdF)*Ps+}CbPbFFcY!GQ#vM1Me(wCER6PR6z&2@ zToer)w_g|jj078+7qK%uisr(8NTS(d4}((7k3^A)0e8OfhkCKqEO(!kn+5roaM**8 zm*VM-%j$<*D_rElf8tmI1~uPP{PyM1V0=Jfw<)2u{OMmizM+)Ui;=?L77X(_@GR2W zOgo;iat+e0Xn{Ojy^I`vF5Q*Sv8QH<9RH>X`FvaiXr6zFUX@;XfU|^GYGLzY8Y8_K z;K8#ynKZzxffiQ^WpK}+y1EOYBs+<>S@hA=L4RQehZ2lC6VsIc^$$;xihPj=OAF%K zOytDkRg@12iD^|oCmG%HDbpUYdf3O@I)7E0ls|T*VPxSypDt3ZhyY{=bA~Ff!W1ob z<|RuZx`WKfMQ8F@+O+P<7izg3f$&g$n1I7O%{(k24h~Tu4ge;HrOvp?LhC>TVFoBp zP~b=NR3Z>#G()`c9{9}@;ZdKoV<Bw&WC?plGCpnYw#2><Q&$m;`&9D}VthapD&BGv zNI3IO6wZbZX_U(VFP(mMSu<7rQzs9j8jWA;nay8pCGA#48~VQf=H+S5PUE5==sM=7 z%+&08WE5i{AA3S&94WN<vWNR<tmD%cO3X3F^HS&2mF=&$cQBZ%<Hgh`1&c&BvUY)c zzBQ)#wq4?5Z5*Dh%J=buQHnpZ0UZ=tlv6E#uB=c$)$g&2?A#o4wRS}+6&+}DF$z>W z-tquu;BXrE{NSH<OfU*sd=MloK_2h%vCl69U@nty03i;o0+djRjRoohAypZp_(uKr zRU{TbiR^The!wzTu@Xs-ZOHehgvNBaIrCx3L^eVaRB%M;bM24Fs5zjhSD885Uz9X6 zR`UD#W8V<(DAO^P3=%Oaedd<6zyw`M=g47=KSF1Ex}52-WU77sYcY+j#b(zX`1PW} z{<Fb?dVx&l&&c*@P8Iykp@FISXzq$w;E&Pia0e3p=hSxwB(z&jUe3=(`(JGT=`%5U zHrl28$qM4cHTlxO_-eG(SV=Dsxv1z%`|muxxl^UnuuG7$XJ7K(=Sopi(vbLoKB#1+ zUG;IF`Kx}FS9Kb%U5xslzgJp1GWvf{vAc6k<o&xFag>0Zel-&=`Hl$;pPn?8zG#Km zaMFW|5rijCin~Xdw@k|D-byrGP*svGdPuLp5JnUCPuIZ-E{50{VX}jVNQbn%(wAis zRZjTiLx^8GqTUi!(%Nhe?R?F%8rDkm2zwbemt#MaYH7^smodpwZmOFp@myQVL(;yr zfTQav>J=Rc8P|r9zzW~ThZ1JV`K7BOfsbkX<7HF!)0N~ik}MM)-)~k^5`1M|`7?HB zB$X*EF_9}@8!X89qli}cGi!F&STM(xf5^)}t1&$4leVow8C01se#GLDp;@W9ap$}5 zeZI&hNE%WtElJ~BeDoQsa2JchM?aHha+G(9OZl&TzH5}ROTOwFQrJL!A_gmM)bj}r ze6eYY;}3+PFb6{z+|Bi;6_ByzN&rdA2)1FQ71-qwPoexMfRv%I5Q%*s50IJpg)}sy z8|^|vz;-2F;*x8&>+x0&+Rc4@id<jUqVT@X4~hZ;saPpkA&WgTZQFU|GtcV5Es6_# zI*BXcf2~Ts_!@R{&ZJ$Nr0h^2TLGsI`gM4EzD_URW1EGf7_#p9s0(rvaXIULsCC#C zDkWbA%8Rs)%9ZH4ky9K%Bjw0qOtP42t38+s%L3|Ie%EpJ7z7*`RJ~$v{pT9Pso$yi zuDQAEOkb_L#27AvI3si4Wc$6w?C|s6=Td%4hy(n2NpEpd$EJVIN0FZ1Q%HVEaf(?9 zv4RJ7AuS`DpP)62Fqf#{1rd*LgdJm(t!Dfis#zM!)m;v>#%;yMad^^K%J9VIuJcZ# zNzE<<T|$A17u+IKKdssQTAEKXlq7erq>#`t7p%(9+hUo?h>rlyf447xQFuCuoqn$5 za+vG;BFK=zip+r7LmxphWU?GMxd_*v6nA+1f*UC<^+HM*QQM3@Pdhjxn`A*pk>w_R zlE|_Bj5E7J%v#~t>SOL`pCZfJ;r+~7GmRWO_8l<aPQpFB{7lM+KH5ncil^+vnrz1& zHY8`Ezp2F1s=%shr=<8QEDRZQ6hV%qtn%+Q>C|3~JuZ*7E(?#a@}R2*>mN$ysQ=m@ z6^JAlFukFQNGeRj{ju~`f~nHD6IW<3^xt>4isTaOV*1ImqFT+tdF*ON01(6?sK+L> znm=eZ%wdvL7tQGCh43__Q9z#yb#mb<Xnaq^lQ+(+lH3g)h4Sr0s*>v^_*yLa`*Z5M zwd3D@N^~}Pl#uS4?@i;;luW*XClu+uN3-i9gImfmg~u#%CbNX6i7o&0b<T3-v0m|Z zwh?_CE9dgad`PZG!<ozS^hj{9iM2dM)X{$AcOPw5G8~}QU;juU4!u~aau8Q=zN5Fc zcnuR@%H-T?IVUr@qisT7ZOx^d1r^~Z&3tOa->Ga7stGk&m3aKX2TY|tocO+my>OL; zJ-q;n>~)o?P}6deFhN23y&a3>v_tWGpI^Eok^rDd5Fo?VK@`~~WDsNWjUB6kz7|o- za%vJMIx=x1uA}rlqZ9A1dgh}f=A!_{7;alTCVAOd@jS{ZLj>!iQhy7>7+=;S$bqk6 zfx@aMiGIRAIXpUqg99UUu{bz!s95w|PyouI_cjgouvIEYV7J^;tg_7^ki!{-a?G|F z)0afB*)cwU*WVQZ9U=5mi~-^SIfVILeRw~pBYNYJ8qgrHNy1`83bDhkUy!e9fy;>N z5(F=p+7uN|W3y}HF$GHC=>y^Y)K+%e7+SZ1f6G9Qb}7gBPt$Ar3mj5HC}1<Dv$KeC zGk06x51JRv>2tV*AO)>*r-!x{L`qrM?_PWCO~@A?Y6&#^@lp$s7MLH<EH0|w`;6LZ zN^)@Ktpn=MIz9>gwZOUQ`#vjsj<JxNzbF;f6O<zj$AVpQMnuJ94>L2dAaFzkKbZBx zhmA%3AYR);9YQ_9|7eP8tVkN)&&eh`wDj1qT*6?Jkc1{=%n1_ixN938g+ok1z?Zg3 z1w}}R{`~jb!+8I?JhS4jd1+D1qV>&}!qOJk!*W9I28c6t<m~WwK7)nj{OQLDBW#3x zyWx@OSaFUQU^cgd@~|i*Bc3c<05$`<MEAme-rk0d0B%=Y5{E;Ev%g}5_|QBris{2v z4->R8<kV)*u@vDdb#EG(q~L74exsLBO4}@ZYt>MlxSr9QcFpUoll1h|u(v1Or$;@w zzH!m^sj=v$=Bxg(cJPc>|Ix#arF$QJJqpP<qsall*lezOGDleH=>l2>rh$C+FWuF9 zkNQFwEU&+_!z!P}v;59p=`yYzIKaY1nKgiiGn4D`yO9F8vuFEsiY{P#_@i=D%ZIAp z@UZn~KZCZNeaIPBUU8<>VKKTV_r<;Rm2fqVN*rbTsBDJ=uX6Vu)_7WEB%nSqLNQ7C zo-qk+*O;RGhFTT>3=PA-AU0JU+F}2v!sgCormke)-qy=E^0~N?S)d~>>;yPcVohL? z0qI9ctLi*QF9bZ;Gf)!S%THo?Hn<`+vNQ2MsE)@e0f?k&7aj$VAez^{wuQOcQ#_H1 zg)E7mDYht1yV5A^4IgG2J@ivlIO2ZlJjPTKr>S0)s%y=FNY;16d7wYKUhl7X@588g zEtIAjT==W#JnQl8;LwFOnL53p?TFWl>psOYnyJY0@X7&TiRRw|)qOuD63ZbO{n^P% zC7&6Mb*jj%H%0Pp#dy-BdmIXo{ECzmri2PtDgMwmn?ZEWaxrC)i7B~H7rqq?2l7B) zQ%x&UwRc-qF!Vd8O5qnh6jS3YLyUuuiin4;hDr?m$$U09<(H<*1(e8ZBkU^lHQ((# zDVa>qK~*S|rX7+A755fJ?|P{gIzF>Bv8UmgnBq<<4Y~e~aBJ1<FD|2<+$Uq8Jq!mN zyP8PNy9VOWaWd74U5qZ~)+H0-GAqqK6Vgh#egwl)0LQD^QzY-o_?gmYWYiN1l#&H= z5s;Bge%X=MW!eZmivo|Rhli8xGKk#!Fvv$pW-;0p{v3DMQC8^h4g*2k@RSK}(sr)@ zDS>{vm3}UCDi4v2Z|5{Kf#*LKZd`;UYDOkjk0bC}#xA1@9G90ESWsUdCVf_l{SMKw zD5Ul;?qm(?))pNP=j`hC8?N+=d7^%PV_d4Wy5B)Vv^I-9c^uZ=9mY)-yI;y`Ez-Sj z<=D7kDsqDiDIwMrAn{wz$tCz54%wnrF{72s#+@t_k0LW%RwI|cOEtVxP;_L@BqiDR zuzx8Ra~S>4-k>b^9c#p}l}Axftd+!PdCR(#70^GmD~~y&Gj4xJYhPn_k3tcPiWl{E zD`xO*h9>yBQ{Mvv{GBrhSR|8ZkWEaL;6X~+LawHBB;(FcitwHEl+g2H9!MH9*}V@3 zQbaNpxwM`UXMFi&rXA~ZYN~cTlk*m}iUfTopXZsja%MTloaW0;<BFt1l`8fI%I2<} zBq1cr;PGR=n11-4h*rJXc{wD%oFtv#2o&L43}+>NuOrV?NJE|wH=vr^Q@`R)nx=o3 zqx#p}v*gv0E#bdU*+OfvQgAHa`Jm*-IGgNp1({uUe24KKn_3!!(e+z8^0HACZjGI| z9|4HUvCrQ{MwkwtnQ5_enV7kj>bbfgr1)5o;cc?Ep4nMu3WC!$Zu5MJN|a26h1G%# zy2Mt3sxMWzvH8?nNEb5PLj(x$Uy>Kv-K9OVqiGe;#cZ~y+E6sDh1;lLy9%2x3)+NO znnooY8<OyhIpZqoW0;&15NOc2-Xe4FBdHi66p1{5-4w%2xN@E{@{BDeCcYq|Y?8Mr z35DT6mRKJ`#Q}&Coz9C|g_PmH#8SRFBA`F(ukw`E2rY?bKe`@?WuMfnnz~EGPFYQS zs5v|^K6!F%tMe1ByU-RBKj`Q;YKmQ>l&e{%!YNgdz-oEZM=7gq$n#o4sF$$t>qgFG zLC&;{yUu>++i%}nTiYweu5erXFM6_m=`Z^q)$)?Z1DK?k<_WtNjce|{-wLokw?L~W z%f9tOk>A=uhjtD1y;8hVrg<;@xSuW7GZ;9rsjt7u^!c&G#Xvn0*)ZI1cB1Bhp4G<# z)PK!95N9V(hdBVYY>$-*iO>`D?}~DsX+$o^ECf3$vPMys@A?!{IWe!<Kbcad{%`zH zN<5z}1#m#sPhr-2O2})noOS@|l(&Zu)=RrF`0Ms}pjf#YV|0&WcG{aX^t;Dim3rJf zuwP?u?bUy_Je=zBJ0~Rk#0{*gX80TdfNkoBG6|6$4^0X;#?g5%mM>UsFkHX7>)iji z)@ywti2S=I<(KL1m|PvbxL<)V(1-o&kV#tjTjpw+M2$cveJ{EGS5qZ3MDaIT!DRCD z>zn-B)AS-Wa$_w{-UCX>$+mLx_Aub|1L7DkWF|u!@v^cZ;!IFvxULH#XTP%Vj!-1d zuXzRLr&M3HbX6>gYKEsj-Wlt|XcX0eMM;!PbYouT4`|jz_E43cM2SFYX(=Y*zB;V) zHm*+>_lYdqf+@l7^$iuWC!$rHGZMV_@#8KD#z`4^PhC-fj@madHpzWEE9)1aC5`A( z5E10UD<x$uE$(_rPjH?Ux9>ZDS&(`e#GF2DXc><Yy!+U9<+{ac`C#L*!0ppl0bXcs z76u`HdTrt_iHqu_D)Xv1^}!FQXS?m){g>Vze=GcYl7z0KUFO*!Kd~4XIZm|l3WWRP zmpL+00xH~$OOhVS*&D2g+;x8xGypq0Sh#dTgHT2R8(&lv*-!(;SY)>dd1@ycT~h$` zyH+_ef?Nv-4g~o*tYq`ijgD0l?{;<JmM#=OHTek`Ji*tsX~U7@D3o*=0jmmVC2Rx^ z=i$sfIs71=A(bwfInJ@jt`BB*i(c0^^U#JW%(LSzly8ib%@%wV#hl(OZUo#N)$)-n zk+Of0B<3bSS9<^4YN=%A;0Dn~bE4OueqR;31vK+bDjvssrX}a*)_aC*-f`SYcxt#W zJ{2+4&Vo0S*U-1gfxDqbfS58I0uJ6NdOM~l94M5HmBYaa5JPf6<;`%w+m%wuFrnsz z0LUFsohwy>t7X`5s_B&RQywT2pozw?Oh^uJO|X^myP#tL4xCD6Y40yU8+$A`2&cxS zc?ix~hS2)?GUOv(>%S-+{SJ{YM(^+l2yrOoAwLHI;8lGYS*&=a@W}*#9CvjeLV51F z8WRNs7?uiw5{KiE;n{#{cI-hoJdl0vU$kbsp{G}L&k>r540`_%1I08iTqiB@YKX}} z*?eePsv`*SQ7v;}Jx3>>^d}DLd!JQX3P}pVqIH1D+j$W}5C^$Mu$w$QUeaJNjNl{o z;v+xk&kiZl%2p+z6YV$a-*2k7X7-+%tazkR8+a5DY&oaiL=ICQaO>LgmZxu*mnDtc z9J#q{MTN8{>G(z&sJW-UYIS=|ofogfUW_+92XO`te~-Sk=ExZX6i0rHo1L?6&~K|{ z@m0J;E-g-8w(V<vtq{s}d(pXdp0GRB@kDKJJ}zhTbtk<SL+1G21?(<EC|p&NIJN>N z;(`<4z-&<{q=*T4ViL)_AfE<c1Pn6GkYJY3(iCL^%|^Tr4alyi@I?hxE8A6{=vo=e zJEvJoNOl0!pX|<N4q|1!X|Yjdo@KgRT0G~uWRlq}_DPiGG85aT)%8CvlDqeDFak?P zcIHk1L~PkMxtkpqG8I41`!VXOI0kg5K0MW+Y$xCNqNilL$svwxnhp5#io*g;Y*iVT z1`YdWa6U-SYECRDdI&PL1v53QF*8p;UgF7fC#g^}l#gkJI7YrU7VmveGPe7jad*;K z=<lEJk0Envf+2g{+hLD0&dfec=e&0&R{C;==bAJ1ZB}61z~VLMRNt2CVz}qASNPf0 zbgdH8UlDbCPD4B68~x#@E}xR$`1CyYzIkL19qt<0yiOEgJbW3ZZ{mJ-ZIuoS+4p>3 zm>}UjBO|5$;fccexNxCAkK$*NEp>YW8e_c|9L(PmySwWyoRx{O@Q94F179+0azV$D zgcYoDZ+6n{-Aja00;;eD$pFF<3@Z0NEE3BEXr=dP;AO%a+L$oKk<e&0mmYOM6Y2)l zYFit=4C%qb#bQR;?{R*A@mHKpNx=-UKAoNBn9qhk)cvcQnt4w`5@F#tI;Me6k6@yt zGI1Q$iVfp1<L0su;m;TIpWa)a%u1M$yY^4<g;{A)a;mW+7qAhru{p8SiW^u$#K`GM z*lKS3Tg-B%+6LF^a^aC#PmWg{Pdr~|4GW9k<EJ9ROhobE!CY)arqx8lf)drQ(sqwH z0<&y0)-gegWwfx|bLu(1iHTgVOjOEP<t-r%y>N73)6%h+aiF7t^1ot)rfDVAMxHsX zQn3#ToiP73BT4U~gf*7v+BNqOK;{z*6o**pF%v^X&PNks;}<4OklsBhPq^-Vw8aT; zQ_A1Z)%?*42=Be=%o`1~4uK^+N-DSNHw$UWJ_@AnB0`3R9o4bLG`6PrP^52!Me9w~ z8;UDx7sLtQM54Dzmjz2WS^ccB42RPm@}ziIu<63~_+u_ynP!fn32P`kk8X~5@E4+@ zUSaCE-I5grj~9eFkNn)uq7s!Wa80_t(!?F)wPSuC{>pWYq86L@nJ@j+J60uvYXA>G zHg<c8WbKsdd9c0;)u0qS3(t9*7TrBdK=e>o0S~!|ca2sCK574QH_ok}K__i1KU7Vc z0@e9)wlw8E#(1Ef*>v>8kXOO*8<F1j%1PD}$Btt4i!8E#Otc{hWNig7H43JDAO1QA z-vVO&%iy*u{ZclkpcP6It=gX}^!Gj}p?r9b;nFA_3%tNf@~vi4<DLyQJB6adtR6qi zXu&*pz1oN16ZeW<p48E4iPWDZ)9os$f)#?N+bgo=Y<Yfjdyl{SOYZym(nhe%*~<Wz zfx?snK%BqIj%#Ss3_I_vbV%sgih3ps++AohK@wm#4+&o~0Je5mP0wu*<33hc$*reP zh~D=>*X4?IRc=?l7)}3c`q$sfBVYw9TOd}%=VDCEKLly+G(L><e}FI8#b(VJC%v?& z_MFJbnbkHwt8d`_M4f<+LsU4LOnTFolG&X^umMlrN;MZmDeb8M_2}M5KV7`;PaR#P z!s7IL@@z=mywr{_fmU2>kJ@s_hq22O^MvK%q0BGmZ(AX3nQ=C)O#VxptBbIEp9yV< zWURxIz*d9=x#BMg)j(vyVFZA=_7TK2UvywciMpSb*@)uhlPP=SgRjNnUa14#oQ^&! zw~rYDR+^V0dr74{L3U~2qT~kuw`u(sO5rYEQtT#Z<t_0xpm9&ZLouA(lxwA$L9NVx z5f9Z>d8I#6ytazpP7UNL0hqaQTDGy_r9CMIggsDsL$tN2I>`)iA}zOEZoB4a<U7v3 z_(di9=QR3yBj}(fX!ce731hFP5<J2h+L2_i?AlSwHZc$JUt(1@EhQVBG+EJ{XX<E| zc~<gRjz6yI$#TuXFY?epG2DE=`j5t3hFVo_u#vH-4~kO_P9juB6K1p)@5$Kl=v8GD z>;fX9rwPFF)&7e_aVU-*8fHRVg2;QH(uZ4;xr`c1dkN`yG`Rx;>qieuJ@lJ)YW7>& zGXY4o%2-ml@ZY5Yj284p@N&VbPFOZmqGxn`e6eesKGcV1*ju(kAo$n+GfZ8K($L;t z8i2~hXa@2;2#YCh+%4`R``GD}0~lZiB;q4_zzAY>IZ{g|ksKk4T2p+b%rqJ5Le|Dd zT?dq7I_>z;#E+R#DUTmnMTL0j@M_dFv(PlTU;R7AKH}%M*J|*s+c50a$?9czzV>I= z+3}f~;tzpHHbJ|qqP~r+mY!pM^q~F0ujm<V?R1)0eo14pgfcTkO>14_X$x76TWcj} zW3FhE%&c`)Nuy(?3aG~Iy#?y4pRn6Z!SjbQ-!fOtg}MvxeMUr=h0*{E=**kA=caLw zy|i}YzmzmozF(<bDb4)9KQ&Sh7N0glvzw`d8@-V;uS@`~CVe>xP<9BQp~WSODtWxn zWJqwm-f#L7hJ_5QGYKJ2=@Ht{VI^YH<@Nk5f7<xl_-G?*sjdxM{liDEoty~Gb2(d~ zy&zQ}Y;<JJkr(wg2IWR3*R7pa9z@AV$Z>rmJo3fOBkVzl>kh%{hDNUrLCE4wmFUV> z!YQD<83X@*5~f4UckR6?y@WbPq+{#%xORO<TB8u#*!3I(2`A$QK|w(wq31_u8e*OM zpJq4E{H+^q`<2eJ?Q^*^S307-Jo&O7uYalT*m-mtHUo7i#ZFHDnMHZ7Oq>7qd}L8@ z?{gui$Q3kj_xr0Mq`gk3;3_DOS=jnY`SaD#?_~Vg^t7+BX}X+o_El@IU?W`Ccyccw zdk47KGm<dFC;PR+;Lr$^o{JqGza9B}RN3XFJcXmhqQ>42=u$gNyXrgg{?Vj&$_NM@ zuF<rSNkABxU>;j2nz?|B0szQJSQ83;J#AX^D#c@-%9box<C<C0rCI)n|NIk3wdA#W z(<E(VGq<a!Ooq(sV`o*i^6d)!ob1cEy~mmZgV~XX&%iNTWV}%Wo|7ROMT_Siiq`up z+q3pZZy%Of_=ra(7x^{?)EIyBz<gNy_59mFYnHL>ThAXgt-UhKGa-`o9eq>wpI1KV zU9s~MYi7eLMV%-eHX4&Y%1Bt}Ew*o;y`MX|_rWN(F?*NZ`FtRLV6j4O;&m~Q|9yK; zxN<p#=stcznX)8w6DqMM;++pXd(fPuILZ2w>?Jf_I+9V?Nuz|kc7Zbuj_chUy_I)I zW3bpT0aXW9Sy;ZC;ULT;|B;<<$?j+4;}W$$YU_(7F)LF)MZ(EsK8=to5%?$glG})r z>}T-&=3y4ozyq)|q^26Hj@P?l+gPUwkS}ZCmf(@eyRgNseUWSkY}A!gAdV8}(G2ga zl&AfJO{kXR5+N9|^JqHf5FYBSzUK?}Brt@Nn7n7~b5>-5{*>-4@VI(hN*K;9&_P2G zH5M?W*u3BLm;9mUkr>YUX|?5MhEF~-a%~m1aeH!0p~?NHtWczPCQ$?8`kNzlC1XbG z-sf6jOELyAWtHD9Y0xdcIg=fk`Nus<)4Ff0JkPCB5q!f3QmV=HG;4mRJZFa6*uib- zLFnTr8XDq8YwGp&#^y>pqQdNAVR&=O&R@LNK4R4V1JI;pH`e(0F>e5Ov&fRC{L#W! zAJ_0bd@BEqjcM`5$8Mu#JfbJhJDO@~!`^**BvLiHg-akXn|-{KR@Xe}oqfX7d-z$D z<Snd{^i^g;HHv&feGQG57&gm6mR(ijrRm7`=`{$S!r&C+A;)Z+I4YN_<1BZ;9`&yY zm0Tv!^_m0Uk*TAkit|MRO~l@78dWe<gGj{p#21(*SDwtyr>2l{91%sM{+F;tEO7?= zi0SgRk+7_rCSO4(30Q_rdyaB&)xRSLXY=5{_)!s&Oc-%un7|g;Hmq7tRkHPxTEdT{ zX!4las(nsOR?<)G^qA*t03>T0NkkP{rH`!!mLMKeVp`zw|BF`+7INH77K(yUKKu$7 z^F(jGWoKJT_rw;>_>qxJrxWiJ_#~-)B5bTQ7p9iQx}f#g(8u*IBe^t1nyw_2eyBN- z%c=3+voZbM{Ccy_!RThmWD{3t!i)6GvR@^JLlu%%$^NAOWH<o)gnCo5$SLzl*F`-5 z4&DM6HDWV37e4^HR34U{=09*LuT36ws$3W@!B(FP>4~lJz!JDM&i=yEMXCBDqbU>t z_-SHBUeQjZL#M5}&$Jp*rF(EN3OtG^P0`j&oCIGmNzImZnP)n-u^`&smQ-=Cn7c04 z-241el$P|dd1iI}gnzQNO<sp0G$WYP_a8-Y^8-U$Eq2OZjv_X*0{~2lqq4-{JMjW# z*M)jT4;}OpmL^b%l7KRDG(0^LLPA8vmz!x`y~92!@Bs&U*x=7vOvF1h9yb6|ZQBnE zHIDE}0?CzlC*V<-R4V{<$76(mghSLIm$~wJ05h`@-;M>1?}9LEbPxl3aJ&X|wtz2Y zL?T@^UQ9)=yZdLu;LX!H(zfF~B@)hO%4LK)=8aLYgo#36>{X^hQ9^rX(KM@&;_;%y z=lQ>so$KL^wcl-p0{K1^IV&b~CXg988wESe3*=V?@8#eTb0DDjR1j=Ne$6y8ArF`u zDcrNz*KKc7{%-D<Vla&`7)+|l-~&js9lZDXqogSokg{mqmigGMGVV`~vFIaXV=9^t zn>QyM7q%(^z|5SQt4Ibe2oM`fevJ14OjFiwhcjBM>Zp<yl42LOj<;nC$jM6>dy<C6 zo_7+A%>XeD0aM~}R!kxlQ9Bo*q1>S!Fb8=?0)o(C6cPXfGc%Ke0EZ5HOn)N=Si1!{ z82g*%Q2?7_fr**HT`Xdp8Af~ojtoI7r*qPZRAm`?G>?!2)HMP%FwB&Rs(gS$%Iw_X zk%ljzLCPLh`G9~13oAqTBNTzhv4h^^m_u-drkEBARx7ZYBgSjM*?f8Afh>eKF2&EI z=1lzoN7RC0>IU_l$)-<N=^2~=OR5mlI2<ISGR}LS(&^G(5{r=xQRYNla~g%ji+i7+ zRzHP21P-h{SIZp2`<`5D#9PCh>S40d^V`z=QL)c-)U+N2SI0;6^bl?oJ*^jL2C282 z;h=ZLrRqk#r}-lF!k$E&D-Y1)1g|5pyGn_}=zYhz489DW*JLelpTeoW;cGU!p`vA~ zrXf?SL9fd#nCJFmzOV?MR6pEIiTv!p&P2kj9wW{2q1p5_#`QADD%zQaXF`!q<ca@B zCBrs3?pq4193mGSQJ6S9kN5&bqHI~5Qx1?Ok3fz9{dgjAQ8W5xEi?Kug2ghVb}6s{ z0F>!5zuz|;w#$?p=B(DK9U#h1S?RHW0HriVbtsjifncT*lCgFHfDOU1M5+QnP@n@d z;{YcG4zNDbONr%K*>bw6w|{+v(tq<;;|&p#{(uAPnCH>%qrb7ilPTJGrdwgV>(h#g z=m@LWqYwj0nh1S?9b^hzfv;McgER#{$<{9cN~q|G!~%1G#uQmQ?88t<Xmq)M?2$#< zn|e1s`eVtTAJ<0cFkhb^9~4n;aguJlepV^n+?9BuCyHZ2_zTm{Vy|=+v_xM|q!2M? zPPv6I&KYZ)^`vfmU4o!uXt?yZinswk>C<wK50<sagyr%!Ej<k>kLH8S<IPp(Zv<c1 zSr_=Vo4&c(drofQMf`QVmJzGt#?MtJ5Ej|eN88W|*a1Zl&ZIy>AD>8oFc7EZfT<j( z07QzM({G|BsUyeOG-1btM&^LNmZF`Uf`qcPZRQe8%W!Dl5FHef&1ZYsu3Zz~$B&ju z0$)(^S=KEcj#Fk%O2vD>1#vw&$tIzZPqAcS!d$p@VG+m_u79{dIBi8T%Yf}Hd>Ms6 z$bDuOi1I|W`IM3UHBU27`^q`;rhnY@S8kTEH#$9=W8Z{x-0Dnezub13^pQ{~T!Wf0 z44Vu_Fu{;D1O~w<^!1}t=%Xr^)aYJgZl!A957ndJxXnA{%7nKs9!!chpQ+!pQeL~& ztI3KI5B5<r8Gskgo6PV^V+jnk>5Ql7BH8nMxVqEa0)CZ0efVUi6LWdSX}fhKzAl4U z7S+;MKMr3tQVxYaTb&03l>tqIjMuZ2ii`t$SnJII9BS2SKqYhWA_8ZsKn959v604M z`oh1$mq7rEV+m{RUi%ACR>T$i&qq$xpUVO9+o})rjQP9h{u`P-<Fa>Phco}=PfHw3 z4jC}6g+jPLK-J8ZAXn}|aVl^qG%V3bcSrZbYr*WPe4gMJ6OlM6P%>2WypX4p_;Ard zySj!xcC7TGwMAxI8QFdTQ9SlZ{4v)_VKF=j1snof>Jsb%WtSGZIkgL2eAO?EH&}lh z-(9M3wRSf=d0F8bm5oa&JkIejp2zIf-7|M3nnL93Bm4_xUlCr&cehyC_~wyhapHQW z!_~>;<vL9z=Kgmk7q4#LXdMv}Kf2rhcdPmH%gnIvDJ2SFU0Hlqiq1aeT@5H408kf& zgbetTX|fMxj1oD|^2V7AH{sx*ytzPZ02?tIZQ4pk+m}#9)x|vkU>9{CKSC-z%of;I z6If6{YtQ#_rPSgdI#msmq=w$pc5M`CplWuc#CFC&95GDZ8R=`ht-8-2NIc+nH;4S! z5>HilEmw=BRF3&%M3n|<%u1RVg&bdkS>cTn?b+toDJxyUA57NipFeWOqe<5zy??#x zgUNG5tyZ(zYsIOaxqf`*EG7DJTr%WlTKa=E=H?p)^Zo3_iZf@Gvw4tN;Q{6M?R%bI zMG{X4{%Y__3JZ3YOxUxIy1w?%s?_+HCl$S?n`J~=7vdHO3a<X~<y#--x_;MQs#6fN z`T}#xCi>OYx={JZKB1?*N5ePTEmzp-L&6x-Cjfv;-AI>Bem`qk4w<3~cTj|<DT_T& z-LySSaBk*;kOKh|2vYlmdmj$v_}^aE1p`3cp%jXN9hVqJ3zmU8B>%6BRo<f$JLR?{ zGUyc3?>X(h_g$d^<PW|1)$LZRYfMgleA=pHy2StK)c7N&*<M_tBOg4LEhAJF`s;C; zxX)hpRPNBbVQw;sEG6oUrPa5ljqTZIZDikuzLrtw$i#f)F>7bYP-Ad@$IaKv8}hO9 z0QmGsclWu^OXt3$IqP1x+ZwkocJ?%ZRhSNr-bl*6+ic0AWI@aoTJuIGp}OqFniOU_ z_g())hFPvV{rfM^qv-y|rv^_gUF2Q8!|?6v_aD9Q*Rfm4|8r@Cx%O~-%US=u!>~R0 z>WgUfqi>JZq^mBCj9k?a0$Ga8f+o%KEMio3G+Gb0RAeN?p?rsbo=O*|{^!G}g!QlH zZSDjJOf23{<Q~8;9Ea@{z~cty68DiOJ!LKF5-fE?39MP=L9*es8r0!^>nr&@$O(aw zW`B9R8~-guJ;hcU!89BNer_B0Zy%<i`=*!7R#(H`n$q!V@@a0%vsPcO*zHW2S~;sv z`xlAVw?U+<H0jLG=9>yU<g-o!SK>@-9CJdhFcb8*<QJUoy*TQwPjvd`Pks{V?|s?t z525m@)AvKa!rZpQUvp%8>8ZK-n)B5;g+>1Lm%0nDkE{DdJui2o?{WvVRzqV3TNSZK zr@apfE3)S-+p{Fx-g?jB^|SpL{}qjSYJYw}^M~h6HrmmC-kb{%X-4pH8IQmY{3Vsc zg(VC*u?fZ(*i9GvD)Ay*gBb5Vei$AdcSK;lp9kUCxVN7P06~#mgr6<kl|e#_CEwaL z)tMxnLvn6?Q3KIoz5I1P8jDaB8TDyfVhwk2I2Mk%E8n$L@Dc&sCH-mrUP9Sqp0x5a zu_hc+ef&I=K)OKl81D(cKLv+;TG_9HRL(yCb{n^EzvxxqAO-T9>nHW|Pf1r(j8vZB zdMZ>Hw)KteJtltjM{4W-tipW6U~X!2Fnd?JU$%UwgYTa@h6oSe7J82P@p+wX{2(eQ z?Wc_sQK5I|HXnM<XX|U%ewEt_YZm=*d-{jri)hiS;BOW^Cm9era^XZy_0!|;QC>ET z0GHrm9&IAP$;Mrl1DJp$co#vB7nxZ=>gJ^vIwBx$96M`Z7zLps%%A?R{ZY&p3Id!J z-UuC&`~?9&-<);489Fog!fAG6R!n|CObTTcu$h0~<|a&gu9v;dtIkC+m#0qq)qd^} zTx+3v9(|mRp6N5_qFIia6OByzZ7Ub@K)p-**=wiA#-s06-W8ygYP>Ya>W6cLO7fl= zA9II964nvD$kNH)6)lPi#Ac~?ms*D1SztcpjE{(V(zgGs6v<<ntd?EIz|a3k&q+3a zYVQ&9b|D{LeokN@U)-SnPFAgEQD=VAJl^+5hvSlmXcpKkRlm}pUUr(*PvdGWu->J? z_zmxqU*BH7x_jNb;_-Z&F8~DqOWPMkWRfIy-;g;jUotM_*wBM`qCU{33@EFX<hx<a zQXhqbTsR++^NVvh{lok>{wyTc>Xy2<t$RJT@IkWrPgMe(CZoDZByIe|L&yNYQ?$nX z`c@P*0PA>!%yg_W&TapkY8=QcM{5zY$BPnm_;=mTk%8=ZbJ}lSx*dG{q9I`z^+|En z?RO^M==L{WV>-U7s1N5UVTCbS{UT`hySm=28(Cswq0Tay8(v}hPomlUr}Q{Kefm5! z5(neppo@uG4+#c$6^Hp}cvOErnen#uc__hJC`4uQNLQ^AD;mEB1q6dzL;8M7QesKy zGVQnuUw#<+<jkC_$<^OD$i?OWvR5@RIkEM?C7x^#(fvmuR>UzfEzVn-mYe@_Er{|s z(CF!tX70J4yyWs3OTz%Y?9u()3~emH?c?$&=F%vhIIiAS{vMempyhr1xCy^M%5mC; zUL!YlIg%e&jGre>)zp5eYj2r05$R~Rt2pM{oOpWtq<vrK)SM;%9Hrx`YJs`C2;;(k zHsog=DXUI}HO64pWY+ESrZN4XxRpTEaF9kxMV%Tt-_gu{2a-*6mx#1T`IrGD3@6YT zw?VL{aL~41qLybS@su^PAP<4eXn+8xbZ>GzNWfvq)IwxF>VeP)%5x|}m9jvmWxzi| zgY0BVlJU}BJ`_UeYpKcvA$ZPYXvOx<(0i@ynw-dLKu4gOsFBzZi&4MQ%1*M0FM9!B zBY)=H-zui}`%dHHZE?N)2dwLT4WEj!)*F??X<?mTJ(k8*?gHkdac>#|Zru6mmrrJ# z%rzoKL>S0Gr5w$(ncmy?J`Lhh`Y%9tKKH$kkHihvuNxlz(3+JWuQyv0y<Q6GwN{KS z$FluN5dOLNC8h6V+Ao=IV+UjLA5DhSl>hhm;{Qyt|KAJv#T0Q`WCK7L9F!A!WC|b5 z-%U3OgR!7eQRh^5v*{-zZM4y9IFr!;0<>)y87%~Vz@!*2zS1&`(Me;i5nIQx&>Wm* z(qt35^w}@w9KfObaoRGnL54A$*6CAaT~-oZ1=;|%{4l{iXQSf7;SaJ@bvGT0$_9A0 zma>;pO^wgTN58VnS6)x^eXg_K3iuUJB=IlPukYq_UrXJ)Kff@T<Uc__?i%&AZ?t^Q zk+08!Ez7g_3vxenr*N5HW_;sPY`OOt64j4Gl2}au@Nw-JgXnyx+O5rFy+8iK29-M+ zfx|D-BsgKj;TvHo{OEMBL{bekS`w8Lx)X?23g<+{NawgmeJxY{iI&D1M43p%g<T@$ z(B;^>p}JBuTWwSzNT{YN10H?_BMIEaM<PzSnhgtU9L1Y3EN8Q;7Cz{>24{syxH(`0 zXn0lEi^XDiumV2I4Pa9%wqi$`;s+}sZID>%!}ywBR<}aqHehhH-pmA;g$9I=<wVF4 z7OHGVt<l_2^R|V9KoDBZsA@Cd6Am047{&uZvH}HM`o@MAc6-RjHJyk+_P*)d;(xfJ zz(DrlWpnK68d#Lz{tIF-+JPe44+I(mKvv|G<jm)Yj{h`z^?(1(Rw3<s?}KsSk)#C{ z?f^`c7wJ@e*3%7=!6+2m1qgIubC~c6E^=`uc5M8-g?aW(_{`K`Zkj>Ghxxq|-t=x) zh}Yh3#K57b`AO%5tWB|6jDlKN98o}P15O(@0A@jJ+d}pF2hnlS&CRX(LGY`6G!oh3 zI#wA&62_<;c>H_(&+$jw{bBj8xNq^HN;bNx2`}sfHX;U-GQfS0e}1U`(O*5qKn#=v zK)b{<$jNzq@BTJGOaO>r^tQ$APSqTX6NK6HIb4(eG&t~8pxp+0oTf%#p^kj2HV>Y| ziu^5{!dz3Q#vZ`w0L>9;Ib9Of1QEJ9&{hjj1Pm=$nij5*ux|2cS&I`qn1Gxc$4Roa zMFI^3Xy5=rZ>Vk%jF#uW{Em<53oZ|a{B<BPhe+IKGI>Han!}vr+OQHxMAyg|h$u$M zXvdI<bcU9db_oh%wn<|0QLq9CL-iKYs!JJ@u+TyC1CrvEy;<+;Eat3-h3N!s1Q-S8 z#X0c3+YhpAL!IzQu*#;N)3|U39zLYpKA+c}U(RZGwv^dMWA8+q!Bd=tUv+$&mR&gs zj;i~T-@?j}IEUHlRZtNHYfU+MV^+<(pUGevpBBxn9NKaE)F9#4EvLE9dh(NJ`6;SX z+WD`8`)#)x#y@pft8yLrtG~8=ee3tX$a?Fbw!bItHwh5jEl6<<7J^gS26uNW?o!-~ z1}(0E;_k(Zwzxxa3X~QvP)ea#3vF}Zd++@2GxMC8&m@23pYzI|&7RHfd++K+dQV=M zyq|D6=j$^x8AD$^x8N!7=?;E<Q-`ZpWO46G{25f;*W>~CXF|SwYB&gj9(5Z27r!7! zL(s~!+i01}O_R8>2m4_VV&l(eTn`6Y{kovDUhJHpsJB<o+)bxqkV&z(-XUxQdtsVW zRwKhdHNTLDr;#!pdpU-rv!&aP|6$1G*Jfc!BMtAP_b%^$lYCV@U5!|Pt7Vh!xQN?3 z%Fmqa5A(6G=&}*uSF~_BPBPhK%THXeO0UQ*3Xt!eO*7bL&mK(K)qc1N^L^Vxdy+OL zm4iOM$}La8g;3HjjKvT6O{FY7^y96!9ZNekuvaZPwjTJRm=XPpBD2S)F`yETcb`y~ zY9U%mvyTavxO?8pci6xVK3B^zY|eoO2lwI~>{InHhc#vJDvQRlUZugiJgE<%EH-Xn z6Rh=bY8^*ybB&!m4Y<=PoTD9WD{W8KsRLn-1rjDtxvdhCSRVM;N+KoWK<h~-@UH^2 zJ?8ilRqtRz1S($(aqV250A}x!+dlci6p!cy)po>IzSfTH6Rl3(N&e8(RoV3rrWQya z`ceL5Kr27s;E~?_M$ogl-%O$A(1ciq1Y`LjQZp!qhdUvLpsfH>+p>oWyf2>)GN}R9 z-+%Jj?e9BNzs$|NF%Q==>hiRbqk5Z2l0@=e))qv)_b;hpJVQ>J>^({*V~0l}kbnaw z5dGpn6A_NEv=#=_E)+{d!|;0}fQXVLFUr!Y4n+d{KqlCakT;iCnsVeUDnxv@oA5M? zt8ZbbUil(s_g8DF$DZfBg)^v9-owKnu2gMgMvGgOMpq)2AaJMY-TL}b+>qx-HRmOf zsdg7D=V|tY666D)B#}>&NbrJYe_R@0iH!7DCx%)UJY|AH@51-@o2cs3)<r97>h2E0 zQ1fhh#F4}CT!)?J{N<X860`s6{uQFp-tJD7NlL`i;;BOrC^_yvw!-oMe%6@#S19cq zKVx~3k<i0235b;AM?6+gm`*v9&H$(a)ARGKA$eZ)Cirp^V96`ICKB;mlojTFCM>PM zEQ}I?U?x*$t3<ykFmb?Gr{%CI6tb??o?`p0gn>yh$Acq`MFPg>d6ln@U}N&evD`1h zN77#|Nh3kF$IQuj_IA79Oeq=i9BWo0m~P;Y-Zbcv4^Nb~zeS^Upy;DM`^5W{!JmN) z#N5E<#82B6Yh!H?)9_DKd1{4>ygs+TZfLaa1E2T8iIS;RCdHs1YMb`hI#NE<{hjVF zplK{D+WgB*I0KoZxIiUoSpVVeeKamS%7@jDz_INoS72k_vnj>`^Rh%*rwiFDfmy69 zSry1jd#yAu?P+Xqte5Dq8(-wEbvO<ja$Q?C;bz6}F_*~|&rop+metS13nkJ`n^VGI zW`i>1>@=e?qZTZYxCc%`ubUD)Z0~D3z}gIX#Q--J<D>*jKsX@Vy8qCK%f-K|cC_-l zJ}P9+X_qt^i(^tP_sr4?_B)^Yz*YnTV4)<(#7=ZIg;`3qDwt!T>IWTKl_R9Lb~Z5K zB$q~9sS21_5fQZ}mH_AOU|OFZY`lD*r)<w)b6S)4aT|X4Fa|YtB&ep_Bm*DRUmQpI zzz1zD1y#c++`_1uZ8d7!i-?tJf8Jqd$!QLs|7VqF(uZ*$BB0OiAN0Q)^i`5nC1dCA z25Py-?^$42==VHo1!!!I5eQ2{J8TU7{>_*#&!VdHeAo>EJ8J6u4K(Zd-LmUqm|NIC ziz465+IOc8`7v};Veio2EbdNEmjCV6H^1#NzYR{G(EDngyYF6+ph-XiAm9?5oSYPq z$40ElQmTDmLiAS_T3U~dwgH6goiHll0iM7u?WSuCjEG0Luz~3~e)@E9F9C!PTNnw~ zPCvx4wPpcEfWR!+D0(tEAp$BIJr)@^P@zo!A^~2a=m1Ph&9TvvY0gp{k5B-IhODA^ z2NlOud%#GIk2MVfu;KAWFqec!ff*n8#G779#!<iC0uZ7yiGZM<7%aRvc{yp8C`TNP znQ2TgX*>ug5>%SQj`ux{nc0dO=sxwYR@pE9kj2>w(QW!fIX{!Xv&s0%a->Q=y6~*E zqv_w%LkuD+s+xivTrOmI@@q;K{K)7X7+~&1hTJR;-_-5+S?AB$?JN%c9gn*s`eupj z=FgJoYeiMVkbmC4|0Jq1|GB8jnmX2N$6lK<GH6dS)@WIIC!g5zj_f%9?+x0Z-TCT7 zZ2C^nPm!<ad+-(Gq*7>M;czSN9a{Z%>b&B8mh+W_bNXe=K^3>>>c6+Yy?Fxdo-f~Z zd|TM83rZB+Yp;28iQ}l$Vk7;Xf>t?i7u+(WhAD?hfvIE}UDFjuVys%hT0R3){4ajy zh40puD6XS`TgWXM61p@njVFG@+hV2C2r<A&<T-xrqlpY)%PVAkE}WOEMfM!OTrx=; zF3vlEsl+X5ORXnSE;pP)<)rV&GQpOMh-W)y;+9MtWAY@QDCO4Ek~5S&;CwX-lAzz4 zsz+dp_m+br`19=Gya_AzjbE;p?M`Qp#O}h-e>D=gf^SR@`<7yFOmLuO(^Hz=_SHXs zQ~MvuaTgiAd4Eg$sP7Z^PG$B25trmI0dBe_r&LU!uuCL0f9cP-SULhu?f4+dxYXSI zba(u+D1I=GkP~JaJWCMA7Hk*FZciUd>*JSemFLS)G<JRS$%B3AQ(<y{yGH=`c6C(D z_+e274^gS|`MUQmXXBU9g{hn{VP)P2`|%bO8chT*;>Ry&sOd4tU?Hs=Q)EcUj2zpP zMIy?}7b94Z(Frp_VZX|FcTi<Ui6$(UJd$DeiM}kQ#R>7BMBeuPGF;mqc;glHx#cN` zk*)YXj!@R<gy??;|8vu|wE$1&%taVUzjEwO18VUXE;(F5n~6)J10TDA>s5JSU5Skg z7B@4KeoGCnFi1RJ(WE8`pB#6i@HG~Ae(})UlWtzGS=dPKg>Ou+z^+>WlRUfYAR9sU z_$k)AV3*1l#|iQ0JRI-z8iT%1J~0^kDe6)ip2tJWB1k}XHbJuR?8^bE70os5*`o(P z!L?IPEN!jBr}lh3l%3~GSl}<YloaH?A~x8xSb>$y2o<<akv!P!$6Wrvrvn~b8?CT0 zFZ{@hD~Y3U8AJ0_kxA*SnK<mb7W4mh`2XIh5RQPQh6EftCptlrv<m}AS(#X>knb#z z>SC*Y874D1kerJO_`Y7<RmGAyzO+316H4#hfuw>J$l3k4z6=ip*Gk<HbHt$_VkN-K zn9Y|7LJ~*x2UZ(S7z-HiG=6?}UZGGVo6cFYvJ(qyfmbhsMhFS4p5R9mdvKG!BDm!H z`uIuS;&GQ-mMnfg#m{)IGJed%K~H-D_TS!s8+X6xAt$)J9l1=f(0yLxU)WEYOB{HB zLR?a%+>zd#zsYo!#}|RWKi<C!wzz$d)?|jqRQQ#@R&bo~G_vGZQ3|g%mO<{|iYBq! zxU%~23`8I}9{8L|W5VNOHWo#AINd*o?rX)oxDUvFR5JDPZ)S}A|9nWZL^QHveu_e< zG2SvkxAbUWW(PQy?9BbU2*+`2FW_*6CGH9|#$WLOj{}7n&SCkPm!w2_oKRRqCj#d@ z2oFf$h8V(#ip2kjNrTm7Tk04?5-`DaI<S(5tD9TrTUbkDe`wm0;m17MU6!2LB0gO4 z=C6Y75bCvpb2ws7n@j&=H8fh1jz?9CL*d(?>Dbzb1TlL;3a>48s?bf*+bmK`Ha1Tp z;kvZqF%tT{+F)G0*^M6R%%P4Np^9cf>k;;l#<j`i*QRfoi}#%9i4gsf1koX#ePRE? z-dtU8c=n2hp}!Y>jhQair=oBf8F3>O^35oE;BzHImyIm_tS;dYc14T`*tPiv8rs~k z?bp=0YKH!g&;O?l@ZSpyh(s+TB#KgPprl__RX-tIGZB$x%Br<M5ChJ{=&TO0LT<59 zuoB?n#X#1y(wexm0(hYWR!oE{WShoyGz+|w!=6D@b2;QzxH3RBGAm^pPeZdVXqBO+ z0=wUOl`hYHqqTjnk%7A|)nXKO)b?<t!mz!z!GgwB`|jT5wM$TeSHrDu$@s9Vgi($s z#q_iQZ5tCs__DU;@88h$(<PHy3W-1+4R^WtR%n7gKO*{7*8QnE*)Mpp{|Am!s7=4C z<tD24oF;dly^`B^YP1Sdog+GZz#4Wr%vPbCP{RI(Y`R?kqj6i_1E1q(YLZE(8;g*c zbafGOauN2m^MpltO0`B+In`w~Tg#E(1nHnhibqwC;#pcm5N3-6_y%!-AI--jTTP=d z6)Vyc>g1bCYa9~UfPL25olzebnG6y(*JRzO!dO*luycm69ir1|iOjp`&$A{oN?^84 zjdWRY(!YAUbN3o*Ghta=_|4gj^6+gUSyv~>L`T51)92ZPFMI`K5u|mUO{E)42hJpT zR(9U{%r6CKB}M2*r}yELS%lLyD+Z8xs~NJ)`x@f$w8D`M^5DVYrN{bQt>+{jT#Pcd zW>6;i=kE``U=jV%;uZHAQK6i<vLYZc3-!oX;uTXt>`hP4I+XKD?Lp_?P27@3#zBvs z9#9KeSH76H<MMpuvHj<P&p$acNn6Z~HKY1BWi?+)r#|D@l$A<by<F{&J#_9)Qw71x zW}@GfyGbW6HGW4eHO}zZyf`Y;e0Qv?-q9EmS?3+f98GV^QxT<~G(TDVc7E2JsI|T= zkKEqoyKmPsHQbw+;%tv8TjBKmPYX;VqYD6~N0Lt{kLQfSL?Jj6sYv+Tf~7Mfgf4xi zbivq^k{VMt3Z(6EsLUN%cCOXrhw_(<n-Gl4w&MDIWq7Nuu^IiikLU19)6i^_zwcMI zwg7b&=D3+!Wcr=#fgN&B#!kD|$STu$PeMs4<7lszYb==HuhiDFWQ&qKmF2eId*|uR zDPE)f1zPP#we=qcIzD$lUCUJxYPa$hnO%StGmM^h`lm>9(p*dFtNz!2fmY&`Gy)7U zrYbOpe|F;68~F!6`J-8hQvdZa-lk5&ZS-g*2yj?n^8}mlf2ZxiTgz2kyyQZ1Gt5?b zcQYND)h)tyn}k<V^NP9K8JbF$N9kTN^Vp3rN}F?AR3$H>_Md(Dd0>fec}M0cCY>Yw zyY(+su_Tuf8Q8|h1>@%egzmYrT1>SpXX6lUA%hHZ!2VmI%{E`JlS&;AHuLMmTA$ry zlXB6N^^1==HIX;Baw9E&+fc{H>(aDebqi|+vc#p#6tseT2F|B<YnCWgmDTB(isZz* zb)LvRb}eUZS|j3_R|{C%K3XtRQJ98~#uouY&JWb_*@wwg9A|KeP(k$)j|Bs&v`qhS zxx~4s7-S@UHcfl5pS>qbl6Ht-MwHte{mI9$`AGX~U;MQdB5N%^jpfXCF9?j(Wx7=9 ztd^jEZ&gE0Eua3UBO8ZSud_Uuh{|e<$<n`OsqED_eH;;E>?g6Ks3D!H#mf&Pga<c7 z>N8VTB0oLM$GQf!*q3e<vbvlHnYRW|R-`z15*1E(L&A@Ys}Ndld_kywJG@rP_zYfT zo3+~aKUv~0FeV5!dOTxyxYa*v%GIjwYu}ol4by+`=Ur1}5O-#U#)?s0Ta33_o4rgv zFk#5e&3)gORlZr{<asUGC$U}9QZ6zDsTKX=vSn12rPtE2-<1V8lqsQ=qt4(EB1gu? z<JvI_SiH}(-{sLDrcnR*Oaa;Wy`aGiX^?~zF+)CMRBTlZ+I!%Gmb;X+Wcm#GBdn75 zxgzJ~kFW*R)E@k4agP8`QhfkhWEnEGR>$&QREFW8o(rXg^Xfp9<x!BMqvyShl082N zDvOU76`|U$BQpw6!J9S>*8*f!%VkYgZ!ya?a?*P~$~lOncOhZT;d#kMgi(0{0~-UX z=m<UN00@ARH;D&M_#y{Op4;<WRd7h|m?T@kA`|RT=0M%3S$Y&zl230f4M<2MCJ@yF znVjJWA3GgSABxqhqdn#yneH~V!-DDMlV`tB+_l^dAY|k*Ol-s)UI7ZV?YpcCmg1gI z(SE{`w;4wMq<bu^L>ue;{QM~C`R#{S`S#fzW?H4u&pTC6M`GlE4bBmo<0c!|SvJT- zyb-)Uv+^U|B-G)5{zunV(lh*^@Q*xMYE;f#R+3g0U`q<0W~-6?`^0jk*nROL+HVEd zImczQ#$=_t+*l|M5=i*1&_ZrS1Vk~vf1kQzu|syf_YVJpXE0ht<`-Kw1Ja@Ghz0=n zaQsFdcLFUjFhtlsgVQ6Jhp0@WLaky)9Kp6SCS^EeK-`My_KNK9z=fKxd{(URb)~4Z zCpB{Jd<HQaZ^;_6y(WCwDN6nZ<?n@PU7qTcvqAb(i{m4bNHNMi!(+3nyGkO%?-e|a zU!+|TX7-t8Y>p)IzkL;bpUbCom`wa<=Ojiv68OEWcwGIkoY%^oANwN&k43|bDYe5- z@6MTFdc8oCJj!QEx2xU2k(7PKQs-Z8g`h<W8vP7i_Y1c3@`29~!c{b#Wn;c+Bw*q8 zsngT;By>vR^96~cLP1wgi_jKvOX>TZodQ0?L+FYA8RcdN$CYOk&G@idmc6?)NU=|! zm#I%oFLUpl{`t}n2(D%bFT&vJN^vg>qtx@;WS2~HBcP75omYa|{&Z`!zU{GB4NW(> z%c=7{6sNaCeLse2+{&LcbEDh=v0qT%EZvVHe@M>l)}!jBZzHlPne**K^$5)Op<14E z;r-Czkxw)_J1z_P<=@^F*+i2*A53+K%r?x6b0O<|PFC_wGnr%HUo7*i04iO;e6~)Z zFmB0dHp$GI;oVhA(FJ2z82Z=UP8xUKd3Mv`$(^C^l;$^rAp=|nd<A^mg^4$f0_eYt z_u^is+%#OSJpvDW<|U*gQGmtyYSJ%yagy4TD$*R{9t%Vy(HK#QkJ5m&dL1;5Q-b4> zTvO^Wk-2z5Ez&J5?vLCanLbLzBlN_-C<rmv5S1LywS+O7MoF}sWzKFbTcTiWWDGB- zVF5TA^mwb%B_*U>f9XJ2RCHJ@U2@iG9K!{{uA7xTf}&|u6$Y2~KIVEz11xq-1$rd5 zgzq3734r8G{ws;BHwBYE6VyeC@8blV-7iJZYG<C^wKf5RbJ()96EWfSZhl|%5otfI z5z>kLXEJL)e|tPpHN`3o53tIi)-TGkH1(GcTO}3u_>y1SUXTTC#euQ^@B450Yx8UN zrYOknUgdMYD#i~<|DGG=qjVJYT%=t+k+&kPU|#48^kKEefBr{rTJojBXAAM1DvUsS zUqSLgoI+Vr&l~ly-C-NQ3EW*NrR5jeP3r$3+eH=5^QD6^HH8vtbt+J!=Nb(fokDeb zQ(w2CkLB4Jgf&R5bm@mlGHZv->KVCIs@vqD5xF0WQIiWpkCDLQG6DYNI2M91h#uGq z;hC9X52nw?2lq<SUs6FnVR1}L3W=P%4g#)et*oW<q$2_Shn<5!T~SuGL|ps?0wi4I zwtAAK4DxHvP^c#f!#l=y%~rB5e04;RcAtYD&PhYE#S&hQu-q$&j8xOg)Z@Xl0>(v^ z{zhVI1pw%|m;rUhii^fH(_E|?mLps+H+8mD7KFIBYwCI>?WM;o4YESp(tbM)M1n<z z$q7nC;1>RuUw!wSUeXe9P()8H3MzIOB|x#TI}TwB_G`&l0ARMOOhhJf9kB3GU<49F zPMkN7;;Terp3K7QEgWf}XW68bNv6A;L;P*>h<9^-<qaCWpM$=*O2Nvy{MO<P_8${| zJP<7Ua4>&=W>IW>e_7^5zs!?{%h$~n>%3pzQ-G@&f1xky2T4hDjuoF(*6XR5W>;Qp zv-?O$>0G(Ea8B2D<)qhJlx5a?aoaaMR<l!)7;{P}1G3H^JqplH)ngOkP^VZ{)T>9( z3xDU@iHxGoB`BhLx{*(@FO*6!5@ETI8$3-R3a2H(BZGNxS2FQ(5vlDv;Q)o^nE;27 z5vE}(8jPHM$4Z(YH4p^@utW+#4hPZ?%7lko#YH~YkC8erixKF6#*jmfC?fg;1i>~? zjMaUYF5H(e8LL$?kZP+XRGs5;g7D$R=B3fM=NFGq8}8!5RAnkb><hC;K5`8%i725Z zSn#i65%1JoJYAz$)9ECr^<0nHPwiHsnays#9Pp-N;yJdyOpma#)AeZ$RH?I3<#wtI z{2Di-`kwJX>H*-N3XU+AEaz8Pjv519wqcIlC70~SxAyiy_I7n9jXu-8lfu#U7zrvw zQNZ}xMOZZ}yQFQwSK8q7H+2mN+w-~1&-~^l^KZRYTpHRV{HOGf3QpFi4_0!GI$kvj zr{h3A>C*EP!MQxmE1hxtjt)?Mt_%)pjt>3(T4i7&EMSzvNVps*JT*0Zupe|1WO`Fk zQPuOnNA&69KTGgulD}42f(EFGquhjux+60BG_w(5iAs_Th2rza;oGo;=_}bHXA$Ci z$(9=8%Dl~3HoGcCWS=xopeA3iXj4>`ArTFqdJ{9*t@;w~7~?;AH;skm7Zub!n)Wjt z`BU@|8PSbOv$B2zai_TeN2qz<hi{pWQVBC-y6(}8z9uBR$>i;f0}T8^La18e{NMj7 z`d$44oi-H}IcC&Zn<s4alrRp#29iE^P}&aZw)?TN8tZhI*ACThAdu(k3LaA<q|rs< zZ4cKTd~)BVx2ZcEUgGkybOOeJfk1@np8gIeJQk{Movp}T{yr#A)Ba$eqV1w><r%Ke zV)pbGdtyPMx+g7FM8}-0HTia)gUOs6`3-IV`Db-uDXLWd1w)+`g7Sg6et?1s6M?jv zGM45{m%7nOnL95BLS->M=Ih_4`I8-Dvy%@KV_(p9C4C#*91`XIXVzGNt>hhwFr`pt zlA`;)-oCcUFc=SvI1Hp6`o$VkOvE&ya3DKrZx?g+)gvmk+D6>rA6f%F$ArHqsmA@1 zA3>4T`Ek?Wbt<9wG@}4E&ZCtV9mV5VFEt04Jm)O0rObCea%)vk9UdYja=#JOH`^IB z_a(XZ-+NIVrEFv!D)BY0y`A9X;8ogf2ch-RWwepc;5T1}@^a{Nk7Q6WkO*U2(qN(T zJZzax-^NkxQ548}Xw%UCJuP8?RigUja@gv<=>|0Q=b~sRX(I*hcYyw50UA@)%=GAz zf3Tkvp0c30mBqP`{uqW#*x}fx&E>}lGx`}a4VA%IIv}Zl)Tc%sJB_tjnBjuq2n-Z8 za66)!Btpq(jcIBc3Nq?nBA%uJ)(_+}C$fU_p;$nGttAP7C%-=qOF#O~eD)9r>=C?u zFet}fJ9ccl-JPD3jSVH{;9tvHKZHv}uSLY-M0CDkf@GM#7ooudWvZ~MZH22q`P+uF z*-L#FCfy7QURkbW(B=@hzYpKiyd87giuk5IGHT8cT6gosaMi!{ElsZ1Qa4?sa2&g* z3VeCw<W0rczwV=jolN@_211uQi|3tR(C9bl-n5R7gB9Y1jYd`Frolh{tR9^_15<_F z6Z!2L*&i!BLe5KGW>5H5adp#3kJmw^aqxl9g}g0D3UFQcsh4_!(ge95Wmq>0Rs0X+ zRio(2ayQJ%%^WMH8UM&rej6!0QZ77YG}6%Oz^f)W`Schu5xoc@LP(ck#bJUnC;)TA z1h~(&s_k(TSiKCN&6$!06&)-1+{ih&Og$$-=pNIklO0A{5|aR#Bc!3-nG<XmX~H96 z%!j^d;xiN2uk)a7YitJXAEWv<m8~{1KlW-|Do<LlPuMLAL^T-f*XaMQUY2s8yBpPY zZjj$3Y+rK$${Tf57#_XwKA-w-UXeY!FzVf!^xdBxU36c~_^-~tr_Rve&^C4bDD$z- zv5T=m<<#q`tE5Sn%MQ=`RQ65dkAlqEV|HR)?M^`_y(a|)7if#DhGWSU^UdMP|KjH} zIWsaQ46}Y<#l}+2x}RO=?mf`^b$5DTBkI{j`+>X0-~YCLquEcz5;zTiA=KRq;uEFV z8ZVV4#3f+zB8{8&^A`~>-)+JmCew@Rup<jhl(yGnL4C9@nQ_Mwii`q);Q%I_`Ro!} z+<WEnf||%C8fTi)+*oQPglR1alAGEU9>HJX2n*MbRBB<e{``6)zV_9?8Zad>kZlH6 zjY~v*QwxSb<bh4Fi6Q6n*(9_$N7<zK3`km>b;9W;Wz2zyIlf9ERN#|Whr+bsvMdzr zJhZCtoQt<3kF$0Cz<?+SL@+iMwH9ufo5r0>+YTRUiA$*By&Y=t)vzt*K)~GjIR=9) z0THA}H$+j9fVqRX#z-Mv8u)|#oIC7ET;k_{1Y|pu%mlA_4N%Q%=y^_L4Yf?Y`1Ev? zhKmU^AiR&{Q@IDh;Gm9OfiT8>GZzLig;j^~62*on=gQBRgm4bE9^d*=+m$yT%W}pp zx-h_$h}O&IJE1Ri>3Nwq@bUM$I!7~8CErB(RGZ(&kn6ju!YbRJ3B)=t%zkdyZKbr# zLqE~v$&%9Ao9RxsD2O;@)mmCIdB&fm%AC()9#LZ##{05@?C?*JEN$4{;u1====ffn z1qfiL-!kWNT6a?G;MjZH?!dF0uo78SS^Z6CVN8Gi=QibuEB%ZEcUcY>C+sz5oT-8C zNT=B=a@w}Yh9|;$nAJgXuq(o?<W~d=Wl9C~N?r;2cyz+Glz2Y-hKj-2H*X&J*gmF^ zI0JqR0L*hQ=1Mi%r@Wk>{g8WS(%zoA@0HCf%7|+oVjO=#PX0}ujN+!@ofv<`k}*d- zhg0Vqlg3r!-5G4@tBbWLXWy=7O?x$ItNJ<>pR112>xQzCbm5yM<%8?D5m(h|_7P<3 z|60}KI#Z+|nGr!NM%s`PYgfO=$61ZdY}jYs<T}bJA@Z;A{(gsY%Mm=c5B*$+KlyWK zsOa#%rj{Qn%`S(NuLdiPtVqod<_|=8gTa2nU!%1{T(YZeb8V6(+^)*k>TwM{k$95q zgNUrNHwJe+?7^P{es<8A$v9o%Q&DXSEM47vVqt&Ame2MqP^EM84MSB@mr_>t3g6!C zK*E&KfhZ~SXw%Hal7nJ{G*ivRi^c~&2@-4)hk!vNAAt&H@$_#*X8i4kU}d^6fvOiT zjZA0cXOB6{%py+LpylL{Mc$$D>EE(HdXLGuxj(QOl|-$~rU;`#Yf}pqT1mTf@nJ(? zL9#OsX+Bv8Vmd?M?&%vLDC6<%{}M<4XPWl^de4T=Vyl&<S$Uz6JpDuTpW)`mHy7_l zOuzK3^jS7I;hQX(_HAn3tr}IZx~<z)vAe9{QGZ%-Im&)xI>)G=N&Z%Q@s;ZLM4gIb zsTPiHGs{qq%+Au@HnaEbDLovv?({CQY%{+%f=!=vQhgpgZz1k*7(Acgp<27HanMAj z#4q<UCKEUwO5g{1;b^1SE5C9!XFc#q=eC8DhFHZOjSqEgu9;woWPOc!0yD|OpKIR6 z6JdKLXRBhTO%vzLm^>5`dhm+<oSj*%c~AdSs1GdexxOS{BQp;tU9W$0jbn>7J5^M~ zvMC3rTnla40{6z{0vzuNj+)+k7{Wrjt%Cg9gi%f9t{`>Er_gdTLm>jzvy(?dGfjRi z@8v5E2z?|*Tyw@?hc9S(@x1@8^U<ChYYEnw&AY={iP|Oetp6RaCw5O&3~n1M>@)Y? z3U&Af<x(ZbnXgOvf3PTD?r`lcs3};i=pFGT)WyXMNKhZtAGC`ci~aHOK>NGTuWJLA zOfQLUZMjgD)EKUrBaXM)Gvni!<pxyQ6`ec<Y?;LAr@^vo+Xzp?wM*nW+0U1L=?{F+ z@^TXU9#ae<ao$kFL_0DA4A!p+F``Laj=%p{|LSC-!8Tuxob7x{lOD&zBEgr84J^!9 z!cc9pJ_q_V*c)Wkb7%H|Uh0J+vaC9P%8>k~u-oyz(#1MYy((Z^N|bvm3KxVmKg-`+ zFK}u;KGKUfVM)iy<ozq6q|}+Gpc9xM31K-2W|rq66EyLk{F70Rhf)1fq;cw}(WU^Y z?&P(9y{$xpTk*8Jr(al0NAo)?3H^kxGbCo2-?A3;k;iWK?gA%7*pYYXwJgyZT200T zsb}k)oq3I}hpWs(Wg;q{s;sq!U8IV5;cTuYf>{ZpRCd?CUBXubRt%CB;vLKyuq=;P z?55t(f=Jq|g7x$XaK(jwG95<5QkA>g1E0SNClXstWegBq{x8gUR}1g8J{aWY<mop3 zz+Gsjt&Oh9;<gvTf6T}27gVVC?IS-uGl?!28U7gFz|L9XvtA9`%nFZ3aV$CpFtP=` zbBVL;b#_(4>PA@$`eQ8S;SeZgYWk^@`;@_m+N!P)vrgAGu0C6dgh+$ATif+B6EkM6 z`Lr*cR9MEdo1~!Oh3<rv262yfJ+3NsT2f^R6*Ahfk=*TKK$6Q-Tf655<om$Mt(BK` z+8*<`qILv&gCv<^hFtMb>l&;n)o&ihbLmkm6(!CG^iNH<R6g}q+rwxejN;5LB<V*Z zS9JcS(mncYQ{Qmc8}-HewRKsRK4m<~F0R#Xe}dYrNR678>_Uus2Mx!J_?M+YilqP3 zkGhn{l-L4HF-RqBC1|$1cX?shZC4Z6qpddgvEijEbQRkb!X>}7&gR3dT9n`Nm@IB^ zYc0Xi?r-K>V*<j*&w2ElbCgS&d0CznXFkOZaCampoy``upyuYy8Lq^uVZ@_XGt;$+ zovIUT=E|ENUr=FWRWYSbhXj~d2q5`5^^EcGhHO0d13mUqpX(eR4)$NBa!+~<+Mue^ z^@Y!3rIc!uZALU=6(uK+D;)$bT53wGp38gIRt#`NIlXA4T~(qQKUNs^M(ul0>$~f^ zAlXrWbZM0IgheF}ghlbi5!N!NHX|N}jTEn<=B<L%mY2?QSp=X~<sYa<J3@M`#i=rc z&3QV45d29O%qdkKHarvwt3tRleLwd*P8}cY=RQ7^*ut4&_%w9@x6x1-3{1KM)hIRQ z#$^aTbJZ5AGqFTrcW3SC=T)isvij++b{I=Dxb42^`E$6`_Niv`_GWWD6X4}R7H<V= zCAYr!7EVq+s+tD%&t%A_kU}KA`z^O0i(LnBQtgoof@h`iveD@~G_AJ3c(NsZT;;T( zGo;1&s7qoRy5S(lrkap$Et=^g4))2TZ<jU^@Ml!iyz_x>;-$?R!JB%MmAoadzgiy= z`+4=t_Zv!g=#lD>x|S>RtlB`KFB>Pszz%g^>0>WIcC8L7=O;TonnKh$nCV?CMR7~_ z&9pCk4KZI*o|1ei#h;4rtXFRuw^;f*xIF=h8L;Cb{rc_t`Yjb}h1}Z2A~X-k{mh$t z_kquu9P&T=`Q+z6Uc;E8_PCaf@$0mK@Id0r%iEWIy7nx7ah^tc&}%&eI9?C$cXx0b znW}d1T-_xJZzICvX`S_mud4DR+_YRPoVnBrp?w+sG3Q76tw&^3(11#lP*vrTxT$ZN zRF7-Yg^{8ivp&X^N%~R(v2}?ZhU^T5e0E(mkWl9R_%%1q!yqi$-)3P|&H}G+|G6s) z;75j531y93DJ1F0b^cR+t)dv4L3dd*drELE&ta0Q&rkfC(>?OLDp2yR?p=X9Uu*Mc zT7H9@q#s)XGf1dWN^#S$rXMEsv3L6h#%iPLi_`3&MjMGjwyaC@{=}p0-g-5$x+|b@ zX-&jPv(f0Ajn~6T6p2nDii?t6jr4(58vo_z&t?8d94-to<VS|)C)^NDa8zP-nOTX* z<$`LYU@O%c{XxC`5o7aJ3Cx%&9Spf84Iy)u4z5~>fmte;yN5~{X?8>!EIBU3dIAYp zUCYh6h9q-0<2N&tSWnWSBLhdT1bM!U2DfL~WlTA+L<r|FPORW4woK&L$|f!Mq<_S^ zE<1uWXhJ70zz_;t|D^rq_TT09e9hcTj=Dcj)!buw>pnMXbuDRp_QCgVg9}wyUu@90 zdFCgNQpx7(<hup!WG5z>Qw?(;cXg3PH(r1CKoKO`CVi7|!tF55)gKFd;_CH6mWkV{ zbWTE``Yn4EHfCO(q^fjlJL&DLstF@6zgFyr>R_?qn6|Y5c42Za+3j1UKc4npp|9=y zg&yqZmwdg%7S|L54yX@Lia}9p8d*dAxil#-JLSa9Qz&<a_o&i$QvY@aq||KZG;PDg zqDMuOx#bI{=NufY&fWeXiRU;>yQ``hGdQt@<(eRsG&7Wm4vkY_R}nJv0@W|_M?!2# zkgzj>ZA=;Y_qNT*FPikPY_}nG(m2w>tk$1>V(Oc-2m39^%1^2Uc2_!{3dfebcpYb) zov%kC&no2H=kqGOnz;pkk5pPpRS5pnjI|DzPS%g9eK#gMznI(8B!_4bl3)K4YM?s2 zKa}L!u1asvW@HfPBW!E7zZ-Wu|L!=aC}{~CYqMObY9uRfQ?`?StFlK*`O>gHnEa`A z{l_DenTZ#Y(uCSXpFL*1p*vxgY;XZ>6!MPZfzPGfp2Q)^)W=`d>I^?xIk8={S2L+b z0)G40+EIt96BRmh5U4}DcQW*0#g{rFN&v^TGCI`bVs74vS87gD^K=LWK={rSQ-t}1 z62PENnFp*qe5@wQHEot<5KFGFUbO#0Ei0f%-oZ(^yspKTk1wj2-K_jpyk}wtqe0(e zy1-}DSeYDGYtM@m3)?gzoL8GX4te~>S5SxhPaa$z91W-TB5=|>^S*M1<UGloqkkuJ zB&{_qd3e<=dS1=<BWY2Ur6uIcg#5=%723eRDo4$&hEk!{9o`}3T};hF!2~C&P%DSW z<XyX&BTsS<3+QJnUY9rccQog}@jtOC6#2f<RcoBsRhM4R&B?{?@h4e8Q<=?|SK+%s zqF&X1{#l*v{(fQF56G{1d~@P*=r(eq!b;7zUYudn%sL2(;be}t%gfAuOnkBL{U=Ao z>AO5DdCSeup`|hAMDxzT8;^|%Gcl^HuUVAxm@)`34$gr_GzUGFeZwD)v{DCvEciug z1&F)hU?3raxv-KueekC%iVBEw1OcVV!TqfG0J&5?>r+*%$*kDZ+V!@5=gi(}txe9! zNRk-2;HO&H0C9taBG@9QPByflx&2pq#e4g|mH<oAbG-2+l@acjstLJp2-aUEPv(X1 z6DoGdmu3eOq`tHnCs}w+9m;q)yujwB5kmfrXNj^i<_*+D)U<w^ersLNPH_XRzAYRP zKJ_MuHlHf#Ec<ozG-Wvc=WAQtUrJx9Lho43|GQtzKkt<|Yc3cE)Fp1c=gU(%HhamU zLB21LIzTF3wn{Kb8PW8Pc)cpf<ap7)Os)IE!p<Ys?LwNx_d|R2qc1MWcfRpr4DV;M zmwx6BvWtE(puy~Pif7Fcs#w@=j84vQZ|RGC9gyK^N9ftqVK}psq*MX75BMt}xKoy( ze&fqFXS1{|sa@F)l=>}SrlZ1v*^^<c+0*v(kOLQ~%5bv-|73C4;c+60o>h8266Jf+ zuMRgVM&RIpF>UUr2gccpCt7LO(=wq#5Hv&=5HHZZL^+sI0gR2!_6WOUy%PJr+rX0R z4-BC%`l%V#FDj`Dh)=e*S%FA5fn2A(B|L4mm>ui!n23lFu6Dq1FeXF_&>k0!+391C ziTj`akCWbssMgDZIRF{kBdJ7`C4$6j00Ppo0n7BK0|v0LFg?wEi%sNz0l?+bqa?qJ z`W#=<x*0FJxz*N$lvun+y*sNrY8);T#80~++;QdGb>TGk9C5Wz{u62;u;V*~IYFiB zbw34d##&=TGMPQ+)R6J?z0Yq=bTC&epVgPm`nqkw`wnIOo4;e|x4)@9B__gr-=kOA zJ-dF5Qg~qeR{WS)E5OsG$&55foU(lE_f0gZ?>AjDiTm;Q2BE42?>lsnZ!B75_D6Ga z80<Fhb5Gc=n9%3C{jQ0F>((z02j0srm%oSFZjLTW1CMB(e|Z&;lU)IjZ(ep>?(4qg zte>982siloMATYQb`LJ18>LJ^MrLo%!p{X-tl4-tUtF?g@B;shevJFh01j3%VjMb{ zmM%FdT<HWkWA3G7moswH@Xh?Mj?t;HKmWh>!Z7r^ujuZeP}4U8HJYKHrYT~*j#NvE z-ew7Wlk&|rf0MN#mK&_(+Td(9y)<LAJ^FzQA>q6A8!W<2*8a(s>tFdt@A%;ENl57+ zk6+P~>x%DT=$ote=r2NVK79RIN&g|svo8z%`(=p0wz6{c`W5?aVa|woB4b_4$4rC5 znDTBRy7;+`w_hGoRBv8nqIABTHq|Icp9l^;ep%kt)4^Tiem~BjQOyKt9L8yx*&ErQ z{q(!O#RsKYeTSE$ZO8pM&Uq(i;UMA8FEAW3Y{DNbud#Rs_9zCLBIGEz!pVdWXJI4& z^N4U4QOf`3%kaq(NCqN~boVAHF@VxT*a*x5n314hri>sg3~p&i3XCC*1Y-dzm2gI( z@s*&6{;h%$7?KB;Nrw$QVF2KVR~wR}YPTyku}=%JGE{>z^i{i`zPz&Ze`6AbpE{RG zwECT!@lD^WoxfkFGGX$k|B5B@(6y&~H?CVyaHUjXr5F5<dFJjb9Lw-{p1)sO{9n9F z2>o0=RwBPi<^kA#FFFj6v#S;}ET(4wLsk+K#>Oj<_H(1oLVW*9itg+og)mjfZk|RC zGDUsbY}X^Uvn_#Ljwx4Zo{LGtRfQcNPC=U5W7#dX98Q3NFFddCY3?TkiHCuq916gU zwgox18&;NTMq|oysh=~Y{UX9aGT}y92gW1JWH$b*KNON3hQ5@$@2`yX4!0H8JjJBs zs>d+{;LWUBbrNIn+v<2sCzA6X!C6j=%_hhjwmY{DE5z@zF_{)<uM(7Z2C-L88lRh< z1zcOafaH4Ycj@o`xNiQi!KZoG_(|u$=g#A(we~7fy_inx`;kgj!tv`@fJOsa+8k{8 zu%G(Zy-E_{<A0O>-skmyJl;f)t$waE2}^hye?zF0nL_lK^!Ba6a9)<#HrDVF15WVs z61rF^M^YWmZ%2g+_VF)a+R|t=)vWiMKOClE8}`4@Vc&M%T~AFJIBMw=qbls=dQucP z?4%^vqcWCK&y4V}c~R9|mdzw*n1fM6(Qjv@ECiLLS?u~rYC4r7hT%VQDA(YKUU7-U z9@$8P$lnVO=Sxg}PcoqRpzxpSC>92)1Ow{A#Y|-{rJNxPV&b@t1AGL~{k!Z_P&$-0 zb~72dls2kx4lE-e)cW)8soCYrSmvrrJXaZ-;H7{qB(L?wrQm_b&hik1a6RJf4Fk=m zvpvva(bF%+z+nL%yn0vX=M4*o$yeA5n$Ufj%(v~kQ}>;Sr!T)f&%J*4<8N!jQFn;$ z<JaPE<qj}K!DTaP@h2>MpKgSD6~%^Ijx=e^qf6!TO)9{-wT0yzRFD+NYlGx$M#=B^ zcb`T2!tN(r-2WNz@51Hubublyg<^Yf%$|hasi6n8QN36KHr5akRXE{@XpinGFna+U zCAd={0-@Abm~hF1@!}`(>q!DcOdcsV^k9IH!6%r_xpqGu_`vx>B)c^R(~LHb$JsV# z24bi~UjK^x8TO>rq0*!Gg$Qmxk*KgfEKW{zr-Z#$^H6ybCki*V0vJL&BVltBlo0i^ zA`2_VC6&yi3jMb`_(rZ4jaC=Yq_<H_cH69QsG@`_VCW{uVzih=0OVSs@*EPuX!NJ< z_vjxeku?PZ!;;;oW_HWYG<q9s&}9TB*>C;gW@6Mwic)TJweJud87Ckv3?5w&2?FCM z5b8*Wv$Z+wU$byNUV>j;TLEpT+69GNzdQwu>huF2vB)xJYQYgKQ>C+px;J`VUo!}Y z3hKx$KQ;S_e}3lY#FTldO7%+R>bv+*bkFnpzN)Fmv)x`Z4qnQb^InmnF!CRGd&-3o z0D~G|CBmfE+6O)<5^C_=odbP()w?3Si`%7=RFU=2kVfpjBL6}QG<x-5^$DMF$K;Db ze>sUqSwvox!p5f1^jTKQVVtEQBtRHMI{A#$iYsx6CS^u;oIvdDT_O6)@a=8haCJ;- zrjNc7=09KVF9@=;IgrZ~t2Id08I)>h52kRneb2<6DNcy5@frIG{MI?~&)7(Gq?o4C zCO~DukoLll44VoPHyZRIY4b5KPWQ^ECK#<Q0V@+!?)t+VE%0jAJcZ{L=hHt-CS67A zX}4`5-p+4@ip0a@4PQd9>;VO>Vyp2H?Kh{(<?Rt$5cec~rT9XvzkVTiQ(sS2j5B8M z7FT@NZ1=234uiO}!)71+{hup?OOxIM!zY?gzv!wzaTI;v(<`0{&6W8~ETKz)67d?D z#Zru&%8}02E*Y}(w|yb@R~>y@e=#>iARcMt<I!%NHKuQ)tReWE?Ol3Yu@!422Cqx% z&ZzU18i8&9)C|EAq;0E|Dj?~Ao`V9%#l01di4h*9U9uR(84;n7ZO&OCa7@&gJpy{m z#FgzBQ85?#$tf?~imDurdz{A&hhrJ#dn^W^cq3Zj6ay+t2M#CQ_JeqDl^!LjO%if} z(gbweLRB+x5k`77m95&c?D%4Kjq$EXX}+l^e06I4>Cl$FYxBA6KDFWUv;($jV>!3p zm`4Xe(|N2RhaW^f(z^Uc-&gmb(S=qAg_FjcYwF^)w4Psl>j;g~uvtWDn8ewm!s+SA zWQr~GOLX{IANZWegh(a=J`>a9l&Z3?qL0JSA7tkS`wz_mKE@@p6h7tWeJbga%D-1@ z0VDvb`T9!HB*sCk5Xwj#dk#_vA3ewvjBC^?ZVWZ4{%lB#7cLD1&S5_N_hD2Oz+trN zB$VeOuFfGejh<qT?M|l+-%!j`Io&B64M^f9%*e7t64NMSBVTXXs7@1z_u}mRUFcy- z)8Pvna$SZ?*OaAG!0hq?fCY#Xn-XSX#M~KJn2;TZAU#5)*?6h_#=PyrPmBKc@|hqp z7JoBkpPo0D_F@EYYirND;80Z$$!hrxWxaPZJ<s0%5xcSYi7rHceg9U{0=iLnmm@J- zheqelPL*b}B_qciKZOJ+Aw|6o9eYq+ubov*uVonx*&p~^$@9WJ00)MTTGD)qP!(@p zCeO#i3ausMt|enz=J(QtN%j_oSDl1kwmuhMnHsl(Ox(%Gt1Ow6#-qwnj*CW3JhZk{ z{)*uXuBSReB{+N^4j_wI2*En)vnWgU-*rt>K+Ve@<gA)RoIe$AcMi2U4u?%70+P$& zA9+mcQV{ouzg=vd4azNxO}DkbY7&!}n1ieL`b0#GP$Z$EBuXvll~2Cfa=7psWgz}w zGm~ikQEQGT=Fz7#FHzX<3MKJh7cG0E>ytOFG%-$Qj=rs8RNk*e)#V?%x~H&FHKG5U zO<CNxDx=YqVMj$_wZRq8nZ&GWH2RfJ$>?Y_-fo+3mo0dQYmFZ2AFGM}`(#GEU;%q2 z=z4$ITmL_OfD4(#d;Nq1Lr>|lW{NSCxT{Rk_OH2gY>ZpkuKj4GW9Y4^$Hg<j*-um3 zOk3X~r_>l8k(Ov!t8oEU3uzp2nCR67W(Jglz}y!-@YTW6zBchWjz|PYy}@ReWg=Ax zV+90f1+bfs83&;fKgq(vSMq!+EGwB@Z|$I-6|Vi*M|s>tdnuU`4_j6U%ab?JJBmp6 zhn{44rU;?g*p7y_Sgj$RZ1GoqMZr+BN98E@2p`OhU%GMuAT0z?TvYKak%3dDyr`T) zVA>mkHm5tB%fe6+jCCSnNxAMrUxe)RG~SOQzF)$dHSgUf6t9<E-v4{?w?se1?c`=y z;VLw1VzsLOQlkv<2zS60j&I$IHZV?S{fkDgp#LpD*w39JFZ7<z^FH)7H|wozwDUng z#-NSau<=HoG_z#%rcq`*rmx?f4>FA}1{1Rz{zJ54QpC$P&|Zz{MP@bcGuSK%K}?w6 z8UYDC20_us&GJh)iVCWD@uss_`RYRyiPFTeQ5IGt5N7iA_E8jG4C57T4KCZs@W+M7 z#?^xjdHYH0DLYcm`R`VZjRm4=DRoC3i512P<48=Z7X=;Q)e7!IwZz)PAMUg3-TF^W zbbDXe>GH?Zz~FHoH|g1u_$~CX2Dnlr?X{=`Jp>MEn8-&aFGlT5JF~CRe-zrUZvH&k zT`-z&PmqlwBldVUEjjZ3^ZO;S&S}g+-&ow`g&26Zj|eKDE>lr3m};6h+Vd%1>M}pM zTBS2k<3D}%bGdq!kl}kij1QRaB|wJK3B%Cn-IkfUf^X4w`I04yxlHrw*pSQa@5YJR zbCYdMFfxAA?&)a`NU{nQK_&sm=l-U9(JzQKK`a1J41Af6fw0_$F?VhxQ#Vi?2-c@X zjVLsuT&kd!qz0{Lf8$7~_7<tM&m&To3!M}Gh%R!vdG-EL7*SoNcqQCSX2R#_B3Sv_ z5UT{GNd1$Qt}6ZMhw|^CBP<1*laNqF8$5-0`q(Y%xoXIToT^GSOI=^f_zz9l$X_va z^Cx8|xmSlg?F+RGPOtkv`=hV!10r<CRqz3C8sElSv6LMHDeU7Z{Xef>>Cgq!LPfZF zfCr6+@4Uz(X7dfCEeX=w+v)dEGF9bVcwja0=MVOCE^BsgKc~;{P3nH--9LXve(K_1 zA#2_-8h2o-o6O8Ze`ST^NY>wXKq+y}VCM(W!g|5{<0RyuI_&T=do=(D2G#)>31$XY z-Vb-EOc;#tOvliUj&Mq=s42Jl9otX-*vpPU>*HW{U5GKliKZ@x7|f3}0|9fN8b>m+ z?&1pqEo*-NPNfS-#krTZgD8Kb2*hG**>ZGB&t1yu`Ibropb!5tZoD0i1+nsI+GR`P z^#;=mVh)jm1za+%K{4@6*re)s9wgvX$zO3iB34J`mY7tbo;Su|9826Godi~KoV9d4 zcW@?)z->+<g(%jbO=YR!iZnCTJj?f)*NI>*alEt1Hw$72a=^fu54?wQ-jd?7^XY%} z;n$XGl1`SVmzTy;?xA}vGn1;+fd-r7BPUnU0bcBaI@+MOYs0J*NnqllqF<uGW~Ci| zd1_a+?&s&ij(+Ju*4%x6jV}77aJBk>G=>B5lk16wcr9>e%OM3{YdEQ1Z&=fm2Uygt zo()M|%#M8{9?HFWiVh3BLGvHe*1gafUlc8gvGV@DxgWf`r=gG4PrNVX)Ksg6*IT1B zKJ6w+z=O4vPRcGADa%}3DkG57WJ?EgB6*C*3Gc*Kx&VS&F=fhRUX_Uq>f!)8`VO#& zV?15~2TbzXDzdF;$V@4OJ&mm*tRS9EAPm*W63eZ`(ahnYt&@5z9N@0K2u^AysBvvU zi;0Km(vK8~LyU>*XWuXxiQ5Io!AvFn@BS65D+~=poaX8?Ij&$6D(hd@q~K<lxs?m2 z7t5yGVlLwu@Wimf`6PZMAu2wQi2xDWT*3pnx)m%Yr*T7x5Wy0BxyX4KVw8_Ur~0K! zECsAM5yEy91-#eI&%m|H71B`1vD=KbnK!P}TUyw3m^jKo*Pa}hEZ47jPn9(?r!k}u zA#HF@Iw!Hzy8Bt)AM~fGhaKCifAPmI^ZNM|P9%*=*4GP43YnUue6zL}EJ3eS^YrNE z!{lK@=1TX4aBlwcip%lN+X;=VxXD#ly!~Gz5F?NfNsmIbn8102<$c=;FtJ3-G9bqu zj2ZJpUQrTRY;_2wN(7b+3ii1VL|L__PxXgeAuH0-i&;2YD#m4SqvOkk&70Z$|C?W_ zt|ol1?|1rI%eGxm38)flivlpyPH#vS83Bp~?P5%nXnJM@ViYtJsO?uC#SOetxvT8S z99~W(B9z)8oTew>{G{FeEOL}ECj6Pm=VBsYoSqj2IlDy@t$#QZF*J2GR!J$hAMarA zzP}rX`|pb~foiR4Yh$qY8(QwE$I`%X+)6j?V+~H?Z?^FzzQR&JJyoui*BEc-(SOE! zZ=ow=*l*dg6Sz^d<uAzvtJTzImN+9U*;(fTh>J5KJDE|1tRQ;7*Lj6P0u<TPKFweo zNj@84$NKHSTdrI8auSV%v>KN4w3=LB2ORdFD)J&cCk5Nqqyv*fQ|Ff35d$&zHI)sH zb|l4dELMPDlhvmoW#z5ms)9_v5Vrjo5B^6$w(i&ibdaLcD$A<2L>@2~O__xn0BUdX zn8;|wY1je}Azl_MsRX#FxdfZ=bpr?m6E1$P75{yUOa|ba)suOxv2-#@nuNEy0KM@9 zWvFPn8QEm9C<!&79!y`7Me+X-b(V2Wz3(3%qXrBZJz(lcX-2~Y96eG>8fl~jq(L0r zT_dHXC6$y$TDk<JOF>CRMgH^q;P*cd_u6@I-sd{k9oOgnTo#;r1VolVY|p{W-%bE^ z<a??HgaNrvOFvWe2(j~<2{5|=h(IdPCmbR=LZWF@<MaU7p|4x51ND8CXHU|(>9gxM zk!GYVa91zE;Mw8rU#l#{^?=3gdDeA#N9BhPQIQ`KR1&BZQ!0QMG&y}LRj9QJ%ws(& zfXNyDed*h6!&hyWTOOnD=vqYi=f?e(dc^83pA9!*1hBB!7=wZrd&B`EgR-FHeUD3_ z>=AeI@Uq*!>G5k^M8BAe{l`Z{IvnXvutyz~TXA-1?YLCN-~LucE42T8Ldcu;_?U*O z$4+|6!8Z?+YRs1sP>D;??lfF}242`+Mtia{y#5AMxEbOeCrvWreeF>()_%{>i&5m# zlf5|x0f&_5d#tX+o>E9ppU*#V3sTF@#=s75!UfBCTLW5saUdk*DbPy3d=ob&SXgDt zSox<41SP1j8HYQ{sk;IL39h`;!tKj;4rgof#MD#MW|Wp)@lz^kY{?LA*!!yKj&D(Q z4dUs#oKrj(gbU!v?=6=GB0In#e>YdCZz+M9k%;6>rQ#S=p~6zq_7y&L6fJpFiC};H zw5dTe&z=4$Y@G&P|A^%fp|u+NC@6kdSA#N~rq5GfUHg@c#!4D4Tm0jrFD{KVBR94L z=zh_e8~N~V*Rl6>lqq8|#%Ex@<m<2fOUI0flE>j5(q@@SB2&#(wdPAq-Q6#0?OiXw z)!DvXJJgK0T3M;9We^VpYZ}&yh)Au8EI+FYSh_3KQEToN_;$>&w#{|@?T>G!_gyc? z7P@}FdXl&p<0UHmi+`+re$9y?d%qfge%v2hV4oz6VH%AJ{~qqg#L~q2pob^OgA|lZ z<edmbhxBTG7obZKp2SQMgDQBUOTN1+xe<XN%wiiV5e#c)+%SE{QU$aY1&<A?c_<Z# zs=u`1_h<c_aQMUNSqJ8KFXG(HsSfnr$Tz9^+1pSm=%)QZ3`3K)0}mnRm~OjiV~>bE z>CXjQ?XeDk0VBe_oJe2lzxYWs*FZu^a;5;_<c+XczL~M3mAa>nHMR6ll~lN1<~;Yo zupe`J4N((A<&;r5k@4#2sL34hw>@-x*5>372r=rV-8F;t3n|*8J03nw4|qy^ZCR9v zum~xrwvYq(rx0MnQ!$DIOoYOe8?;+wG2TS8tqOmc>OPBhP41w)0;{FEZD&v?j1O~Y z)8gIFcsPVm$l30nWC<Z6WgsV1h+!54K$)`XzYhTM!C7Zr4$bW<QGo@>?oa-v@j9Da zL(dqM_`g=GwaC@9js00OR^fSrc7r!;)3_l!l)q#4f=JvLOG0Oop(aDZnCo|%S@R8w z!cwFQ)@-2Yp<<oVQn?=1e)f4sO!Nmm^JLO=&VZ%#HGJ~WO`Ayiz<=?>i`GZBTjcZr zNy4UE^vMYX!}Y=bG`s>rLZ<!!r|NAAbx*>tqLBfSFwWGb?XO)(vWh0|Z~8v-89Q<h zES<-QS+%092D}&tK)*Rims>|`+Q$<zdJt`ko3|}GS<{<K<dt%#uIe@2KJGV~%|BSB zBp8CZdAd_aHS$M>kAGYdC4M+8(rb)A+^?No!p4jhL-uyBZ$)CQDsANj-5ys5j(r*P zuC&uM?RdT`I`un7_p41N;@jWf1_j5#zGi!0zg$+g2scGMIa7ItbCTqX+#PS^2$WoR z`-^+LNUtA)hUJr=h5xjOslHh#@%@_`dfc8GU$=5|``73jPsW>(c?^I`i0z$K21Sn) zY44C8p)YrRJlA-bEE;Dv{ErU|^%a@Dy@CZirv$u0&P=p%SbG#MM8mw`)cquin50do z&>3_L6<0a{-Jd5lFfD2i+@z}a>d)buF2KhRlL82Yy#Q>)(vo)4{S-&L7M;bhiy;Pj z<CLPGM^k|*e+KoDP!{vq-$k{v0ph^*Gtf<7RG$Df4?6zf9R+$Xjl*h~w&EAZg1r(k zyQfN;!#AqrHWj!*KacjEaCB^8P|qf7sB%Pl47Jc)invm|8VQFcM{TmweVq57TH3GF zbFp8!bMN#jy*1NN<xaMm%vo{w6Le4mZ>^nTdBnV0_}~2PzBS?cVsn3PkBgc85ZkU? zt&rvl?^CxC37h7>i~?@Vk|j=P6EYEr4NB^qlQc{kM1y#_oq0^~|Kr0e&xZ^L?iqUG zt7HIrUmtE^$d;`8B$EwP5DRz!He!NM;G_%a001Nsh8A%@Ya~Pi0Xa$pP#%j(-DN5~ z4FCrhuy3orA59le9;cD)EfJ4O)Xltjz-t8X+^6QYvG6oB>miICw`gKcRIhcbrFjx( zR$D=sEyPQsQ(>jl)7K2!CV#<5H7fYfNBpbsK`qAt`D=Dw!n{gPW7h7chuk)1i+cf; z-MA!v(pCHHx5=+P*p+EY?bp>4>ree}d-q2P*`_F>`acRVtKO%Q^qBNHxuf+L_T?w; zj=zqlZJ(!AYlCBI32y${t2+#lQ6|C{XE9^)g$(hB0~^_>(Q<lt&XJ|CxO0*fcu$(5 zC%=T4Nm$61qnN+sKYfCj#3y-oLQ^b&0O8Jv;u+d-Dtv}uGJ+LKM0-c1hQzSg0-m`) z+gM?FiusHYu$1(oM|lnQaniFg5Iianrg0aq&xuhCh8O*c6e7pU_VWlAWyv2Y03&~; z5*Up?e<Y*)LA^??@$|w;SYvL4_7Q}&dH8x09Us$oDjF1-RxK<@GgA2EA!l5bM!QoS zyEg~>!8;s^>3~|3)C-;82gPrSOB`{7R$)c-)4yn>Z*M()$tVcQ$_Cv}f`ag|e;=0} z#td@j7f(BY&G4$BZsgciR;VWhF%hA*jQ+To$oMv+0_8}}E?u0V=`SZ67WlXn#Zh~I zAt8aycLI4)+n(ugb<St-e3sG<ee$e|^NrCW3SLn!rF2ei(tqEV!{Y%YA4&cOpkOrG zL7}pJs<C2T?IkwA{7Th22lbr89aHsGXQVkT(uo?%e*YQFe{6BZ_K)?1O#FmbkT5D4 z3$VvKjRE$CDEnS~YH)fS{I=;&jp2Ok1HRK7s6Ho@#w?Nz9}6?Esc)-Mm-ub+{$btU zj#s{!ZO>kQyuqUKY~k*fd{BDCcPLl_0-)YRa%A4S-T!iX+kJEMJm==w58TH4kF%ev z?=OETesFlUrO@DG1R?Xgqd<(ARkuib_Rn7C_WawA%bSnn`?qzt1@@11-|q(~D!MVw zH{$|??)i#wme*{*AJ~1rkxi@RnbBY(MnvD499Fhw#P(VDZeqNpj3rr~2)`}OFVtCR zXQ)iedD-p=vm-^*{Nux}9K>2|k==u5XhehBffH%;7<w|L*3a!2QV$$L2=D2YN36Zz z9ffI_N33)>h8VkY)N;BusUJRFVw2XC3lFDbGB0yv9i#Rvai8VY&3exwc|K<s@g^p7 z{wAr3{@}WQCi_mHZ~iYAt`jZw-%<8rqSJFi1QvDYyY4%<gYw<SG8P$r&lmY%SS`{8 z*Q1$*{l&R?=_d<!K6IZQRxO+FNpxS{UJ5=MwX7T&#`_)fXiDLu;a{_pOGE#2*{fXe zgYL2J^>d@`(V$nqxCW~37F4z+JTfonHkhO2PZAeT0Jvqo1b3hJU;nA}&0g=0ce3Vq zYIyqXTla0&=g&M0EZZ&j_R=k5@HSp8M|x2`ONO$@+Wepy66t-+<N1%z9kFWpc*2!r zKnn0K+$N$>oT7tLIwI3<nTnd?0sbB@MCTxgEZ$5-#Zg=eKVFMnTgFNu8qFdZP$-Bu zOGHmbN9|4s_}rfXX1y;**uO?yxDr7s71{`omOyLuKHscQ^b1np6k*-vVi>MObt<VT z+vz&|X}2vto$<;yrsJoJDRy{kZL5+Qy_qUVuR3!Suc_Y@IfCS=ZMV2Jy#1n7rTXdi z@7rIgN-DFcUui;OUIwAc3JUE>FF9&*W)E8Arn#zEGKVrveiF_C4}>`CQ@7~%%Udz} zUhnrH9zM`;1|JEhOff(`>x|%tohWJL`p<!`;mEB_XT8SA><;&p$CZIzeltb)dp;Q0 zx7Ty#<m-MYiVE+isa*E@PoHTl-i(YdT_A&Hg~B}XQ|D=3#PLSTQ4FsVwLy!^J@2)y z-8pcxaC@~&kGN5Q?5d8cXxg-%pY$^#@<B^MuFrcWOC^lA<u0=sI+)#JrtiFZ1O3o_ ziz{p40)K8_h)a2=rTMmU^0gua!bL$$L2J%zjePh%`1V%s`g3xq^^b>@lNS&Q1hy4J zU4$W%qw%|Xrwk(D4>r{gp%aDxMbfneq<2I8omJwN6y<$(YGtUsyV}@&W|k+{G-YX8 z-R|QLx(~_D-f=lIAps@mOHjjllJ0`OJowT{Dx*+vU=>ZjDQLPFvZ~k*BfLraTvb}u z<a^Zo=`o*vlzU<P!|y-1Y@Yo-zTmf7ecD8+2hfe;K;}!_bi6<6yBYi!KPgg*$a2n^ z!LMue#1MHr`lvA7+2`NHaaF7>+y?gMAG7jvRjYpaEN}CdG;4oHUp^(9bDS}ju6WT4 zm!+pT>ys-QIF<^MxtUPldAWRh`>A?3WOAdwylAF^jBzLgLv<xmdIzBqvGQUGb98Z- zY2;{s-;ZQCIif0>WoaOFM2L<Q8dGE?`b7RuXM3Sq#4O$X!aXUUel{iXTupM8kZAQ_ zey!M@XuV1^I0O!t-%x%%1p-ch2}upIBP@>@Gec@vo|R&t>dUon{N($kr}~#Z2{9?B z06V6}Wk*-9a;anL#UsZYZ4!F#(Nxhu&CwcGag|59gEUt0Rp3b4N>dYOn^$4W6UKSG zzPH_LzBuE!ovVO><%8Or;D3C&B_5a*ZQ<f4DW2X(2QdhTI0PX#pvenX?nlynZw!C= zo;`UttcR1p!=-FgD6smn(jGW{`I=DjA$`&E+_A9S5{!xu@VKG}uW+$v>gw*xA&o7_ zY!qz{f&^r={Lo&K=mj;qCrKFEzKE!QP<2EtsrLxryB7rcGn6X2y3>7XxZ5LX1|(5% z31=(25RJ5)Dtfv){A))K-&PV}m|W#S<$NoBoCSJMImix;_*lBOduB;6ac@NwS>xzz z?w$%|F(5G@B}*hNtCmhFp;H3H%r{G$25hP_<aXWnc&0#RVw!ofemnmem&b4aGOb+K z>59M7zI3nVIq$l7c`*I@l9p!V+>#G&xCFiH9987$OFMk??HQfm*0X<n&g8<8@x^<F zu&9QpZ+``^9|T{y#Jq`cj?pn{<yLa4w*n9_4N|jqdtUq&=5_$PKf+Xq_HNUl+dp79 zDM=~8JzO@SZ+evnQC;z{d!os6e*A!<0TfUug#*9z4PJOCr!s-k>mJNDAuZUF*!W{e zNXVX>3DyN%rdnqi!d;aJ<Vq0B`8g%eE@-1dM23jhz7v492Ir*hmSz-R=RI);QW9^m zuz(Z{BL4`c08;-dB%iD6o>>e3@}|SPLkx@^a^a~iGQdL{)SN3?gAY%N)3ZdubiA-( z*XX*(@}I<U?LnDTdspq@T%jC#z%!+pZ@0JAx9gv;=2FcD^6eO+si^NKGQ>;|Xys<0 z5o1iu5EgZYM>Xw_I8*%o@wruMa>4-jiu5`UH6N98aZ{<vD0EM3e*R3FyLI41<vW6~ zZuFkPBVuk>vkWEOr3xDt4VIV*8k3{0C><keBBx0ZFBLWV-lLF4MyOF$EGA_%5Jr8$ zFg#?(c0VZ=<f=pkwzd@+Da0lw67f?I;+HDoK`}jcHo^&E5qlg|cy&bds#GT%LR0QL z1C^@DBh3L$jZ<yPoj~P&OTbhQOk0~a8pPcT5O$YUJoJupu_^T&(5^=yc!>Ud0F6-q zU~@rB(k&H)?qkR%yD1GT8^x`xck%Bi8-j6WQhfHl&7`YiF6W#r_>ar#{Zy(m3|w*9 z(NFbVY-H^0nx@x^&EJ=Q18#5UjU%G2;EB59DcZWMQF7nCc-k};|MB^)sEG4#+0zld zZJF~Hqi(syO+9c|KWU=9r;?H;TwBdz>kO+U%lIjt`5>OLh5}kegpI9c`dPyy=?!fK zVr?S#<SLuJ@8Tw8wlE2(rsT|GQ+<P_9F4pN%=`^DgEL)H)KWAs-i#)IoqQE?pr{Jg z=NATsFj6JnjdT@@Y4MUYGXf0BeZrML@>qNvB!J`IY0YFLl2Zk67rTPNkZj0Oje(v~ z$9$l<*|gf__#IxHA<UY5O`7i4bFHW{gB5dr15xCl+h4k1vD$r{K*7;hg%escXJYlj zLFV+Ml&_2QY)y>q7wZPc=j;wr5gjJ&IifK6mWs>YSC{2VyZDd`SmA>xDPD33Ypvnx z%){Tjb7Y6RifHgZJ~xW9!P*2T1^)5ZbE(@pLOwt1=GH@9Z+|u#622Nxdi*c(@P$?N z^A-x=V+R`ViP~f5sN)EfsiGn&!iU0(3yX@Wr^bPJ<2vN%@P%bt5_-gIxsR2Pjq&Us zAD#lMPCns*P-K8zzNfA5nBD+IVyTu?A^~aP<_n7A*u@8Y?o$P#q?ziBw&6ppGY7Jw zS6!KVd4#8o7^}>w&aJ*2o3qZDo2T=Q1IglfwbJobwFWiRi^Dzb@0$oyFKp8js<M`5 zqvdO557l1f(0o|1TEM+r6WsV|yWq$gvcFF4{L;8z_8m?Wf%7MX{-t}46)GJ4#WFh7 zxNUQGobdW^y;{nWKkw+>$Hx`w3O|@_UiyE;{rB^r_xY=r|M*-hz<4a{P71o-bW;S{ zXM4KftY1QjZ02{SdBbDjB)VjtFg9?B0&xVS$Ebyq0B?vU(_TA#LPkT~%c=n&y2It1 zY=k$i3hyIFCa=J;p8TQsh3;n4D;1EiFJN^5CSG6p+z9#r!v)aQkBcoM0&p}opdM8V zs}VCXhk~6|4rb%)?GXc6=TGg(#geqzT{NYpHhwqQHj5%)&|(D?5x&5JhfZx}m&~sT z{;)B==Plpo-&kNDooB%klsKN2Bwtw8aheCe$Ql3NwLgBMzyJ42kcOV`?Za7^o`{Hu z_&Kq|+qQ2<{T~jV`1#`URxuP0OA$*kLf97$iW2*gzV|Y<cmMuN&T_9$>-K@C6GJde z&-s7(M>!AaKy(8A3DqSDX)HqQ5QY)_X7orCE3KFG*Sy;!87v9PARzg{x&y!`oQwz4 zs*BOthgUT0JkR+JKNS8sg7LG*rv);$u0rHr6~OQrqdD10UcLg#aVLD3U-!eZGeP3~ zxl0vscLSW3I&;92Yj<_#J^x%~ENSo>tP5!~9|~mYl!wtrRhqz25WhRtjz0tfx)Mxc z>V2Ddo=y+ez7njAZ#1>8S@2(t*9zYCY%?%46I=hg8zrFZQOf7YLA*2C-iCYUYbt6m z>3~Egv&GvRntgk$Se^hozA0NGx<&n|{<2+0rQ44T$GpA9T<nljj-Hc+`dHSpTW)Sa z<DyqBDuOR|-;@eBQB#F{$fR>(;SWN*BT4(m=ZAa^(vN8K^dQUCkMoPH{FEWEgkP7d zg%;+u(8$hRBoLs@_+r=vBV@$hT>@xA-;1vU-v!p4ld@AHQkY-_M8hHd9AOW#1^_Cq zArVPLCAhsYFt!-p_R4baJNejs>mfdd(1h{nGzdV02N=b%07Gzb2P)c(ZzAoQy+ClI zCu@m?pDumItoT=h@2)sz8q?+ayQW&=yRx*_MgX$j>uL2fe_<$zOD%XDR{m*>lef-C zV~Sac5&@z+3M}7ZaW`S%r5(x+!%IJ$HI+L^vF2g~P-3`-ww?iWD8TPu)V#U<rOG+6 zkbB#v_+##{z57^kpxy7BzQn01$i%#>)+1oyJzZ`4=H<D1*W%xfTa$!9;SY8ltg`|C z^)I+pf+1Z9c3vGIby?45t?$vUKSkdoS!@{Qp46?&^8Vk^zsHZ2#ImU|*xF)XPLVqZ zz(SIJ=g(8i#^Xp0qEK05mmxN7Oy|$=AlIoWb{<h8S1^-_#K_lSR&iTeaP}y*jmJoM zUt!_0KRKn1fvqJ~0x2P>T?qEnCAca-ozddx#O!@W7wcy5K&rgmVxb!b;vk4W6(5fB zcbw@&u@8YXm2jF^y{hfO@AiDD;9!o1BSW{j;v85gOr;!>d>BP0$iqzE5G^`$(XU>V zr$Cj}PT+NCaEvK<Z$b5?A)BzaO3MNNrh6~zjF9Hyj&i~CS8s43_e<QzWPnStdUwYe znL@bCT>a_?TU${dm07{?=U)<k|HaRloC^|HzleiHdfW0esi%6&M`sWu$}FKF5X;6U zeoO?42EPchk3!@6Kt;kriHLKf7(tkg@3=lvJ;|MuZsVK8?md=X9RPsFK;_&tha`+M z1l14dQMi+$7d-m>PM-)7%F?pH@j}*ecmT$D9~})wl;ucH^a{g;NJBAz=zcuViJKN} zXBTElfx(3}kuT>RE7Yo}6pL+T%7@@Vr?2xuv64tPY9g#KRe5!GD;5BNSiAP^r!hJ0 z?{N~4v)}a;l>=C6TQTb)PB$#k?o3uZMXo*lgzxOXGh*WC)0>a5@?fita)G-<WJ(AR zyuy4cMfy@SEkOQhQKTYZryb-)N>NfMi713rLCns<5KcVhj4CK8?7#eD#f~%soGePx zrcfaDpgJVD1`pI!;SfALa<C<BAsTFPG}*b^Jy~`$qc(?Q?u|S8jdchn9Z^o?pv{G~ z=kCIPkx@pevY^;t5_k4hPN%q-qDv5#x_5w7aPMg0b5_`CD~5v{)L=)N$m;|xaeH1? z>!f-6v$b||<#YZd4@Ckw-YM)mCwWikUE&<&rdT$q2-4TAXhsMuYjEWb4Wo>k)fBhy zf=3K^-)|&W@yrIoG=!KT*hp<6N{%n&BE|>y&I(bF9|6Q1dW#fQv_6PvC!-XHLWB<D zf7?U{Ek%ik$HNuaF$YINaT7-Mav}Xi@l-Co1cnKxVYpgfbKF}ZYEe48VI~+KDU{Tq z-Ez~)1O3aCO_u8)p9EDtq!)0n_-{WRH76^V8XiZSZ1?~*yKuWvooDGY2G(!I9y4vZ zq5)j~Poy<}&1r9Nl`j(-j;XoDzc}<TTCKy2O#jBHSIeE;W8KGVmBaY~v=c1=ER_iv z1Gg)TsY}!);i%qp(cfMcm&Ofl(?slvpjYQ=EZ~GGJ*GUlS(?KQ^HjP;LD8iy4qsoe zbBjIDejj!6+Aa6?;D<10-c1hy9Y;*c@K{l{KLxFVX7svWXiAbU30_mt6iiPRfN8fC zG=k7T#HykGDLa}`6>Sgw3O?z^a=*CuMw8B3vWNrzg#DxTyhmw-=$dD(tJXkdt(Af= z<gbaJR_bvj`%H`cK$|i1$hp!`m8Cy9m*0al2xJ@oOITm8;y*rt3N^@-tFfhv?_LrT zf=`OYUkmZ3eK8NmO5&q4s7Vs&aycq>(Du19X#jM<wV9$<W_<DvtMT=x3)djquoPH3 z@qkaui%h;?dm~TBxiYcWEFymD_q*|Wx0BZt*qeSnaI6cWc<wxZb}fncGW#|^aJpN~ zjr^+x)ne)gUz#`)+cRU=LOB2-1~2COPjy%iavMWoYDP}16!$@RrH2~7QL=+cxSkXd z+5-vbm;0J&s!2~otawGPm^L$8surb*?@5I^8K~#HSHQ#*y$h~T<)JcR<t3ksmWTU= z!V`yMEH$Vh+f0+heb;|7vX#tzru7C)a(2)I3YtW5Oxoh%=Y*z$0p$Y4a>=Ns-uh68 z)iyrCz#B$MQ1O5IpO?meklu>@!g+(eBq;(<#2=w?-76oJzirT~1?Ti9Z^qD<p^Bzi zWXLWo42$yAi~_#s3dULKVEYCzucF<4LD>SN%WXuSI6>Yo>TV@#*XjrJch0Ps@tf-k z>~1B=hZc<#%8YSn+;Kc26jvLu&XIK)ba?2D)9PE69hFT+*H>O~DV!Bf{-QH{sXy<; zSwe2YC0US^H)_q7{+J9{Fkz=<NYF-iPYkH?gZn%^v&BX>VV-#93&M?WQ*I?r+=3@t zIj4lyJn>if1IM`q6H*PO(kbWXm^1@4g3*&Lsk*&uhV^b#d1YZaCeZ;Du=zFw6WUCG z$YVt`<UM_QQ#rB|n!U-c!c!po>ySa16t2mcMH+)H7Nj--#{7#P9w~O=M7Xu?+5-C1 zWOE@8s5rF2oC%bVXk#UzSAQ%!Wgt)2dxchg!sU@nV30wN0)$r6vGN7q<w}SXhy=tk z;e~%stV3$VP%DnkDoWBRwZE;|(bmVdzl~R?6X~tXiIX7p+$o;O{JP(#YhWoGr&jqk zMR-7I&6!3}QOSs$hKg`nxuu?##2aIEU5w(^KD4&SlD2`uQ%G2f3-xNk)mhYmWgwlZ z;dI5fkXMeV?-h4fdF=A1YRnw4y2>D`#-%}`WLd67lmd(@<aGy;!wBJ|-N@UDC7VSs zgHfeaf9YXv08rs;6sCvTAv|aa!A2WuS2rc9jnPHL=P__1mgpZB4}cNk^5x={Fur%m z9T`6J(AN)F#)SUyF_P#+#%v7Da7l-}Vs?#w{rqaZBr3+C#iFe3Zu$w;6Lodmw#X=d z?|B7#f7Ow(D1GNs*@PIUXx1T~<VG<TplAl${RF7?3K|MYPIB*M2}yw@$jywfl!QN} zVxUcJX0zm?Ww-3%EGOF2K(3U6dG3UW(Ntj!c<ByB<erOji82eIwJT2B=%ji1MyKJ@ z7X@etd^?T|ZM~9kAP3|K#5P&AjRbDeg|K>%zS-V&J&ECG=D*|(T`(!Jh}Y{$X(xIv z5U1cC{iVso*ub-3Xf2z4^}&O?Wl1JwuZ!DH2_n>Kn0~}Z#gF!+fkv5*i!%gbIJ}i? zswtJM%Qz#-pUd>iDBO}2KwAF>*S*Az`KJ!|?|5H3Qlk@a0=?3KcqAVZ0~j^N#wb6$ zxrDN~wl?ePpW)jUrFs&M^lUk9INCIl=F;HxS)GOrXW$FTyR(7Xr;-?Hvn+WUNMUSz zhlQ0w@+8S-+?kKU8>=I>7{P4@T5A1&3`D35Xd1JDhMD4C6M%xOpKt<8KpRFY(yB5i zj##l6sEzexhkoCrRsUn7lpR>uZEXpP%&Zs@08cp&mXamzRP%D%e;*s3y=N5ud!g>G zN;}GtnH60pZq7uGjou|ie@y0z0jes4>%Jx4FQ64xmgiv&sZyin`8bwTu{B)8{8UK1 zjMmdj1R5cBv#z`=h<kS2b)<Wmbo*h<CTsK6oIs@`?^cEt#j%=ue(&h<NGQV*EZtc7 z<-(9yLxN`HfBv*@#ignJQG2N}ouA$}Z#8GW<Qm(U^3#<K5*3K%(c)&~Q<QYXGkeUa zIA~?>kx93NlhAECeDgpVF)2eZd3`p3!HwWArf|TxAcw|NW>7e1YrSfP|1jglkYXAq zPqf4;K_&l(AY}sMrq%DNkG1_tgBmOHbl7S4GH(24g_sIUfBH<Dmk*jAfZL91gd`qQ zX1SE-a4ukveU(L%Xiu?3cZjG#Y8Rc&@f)3i57A0t^k^E3-o?Qt@{9Z9>cvME<g^@* znD|WI@^I6W9@_nQRuY(R_w*%N`<TxmQHXn8I*|UO=qzV8{QkBi%dyt$4D%psvb^~h z;^)#+mcrlmb%!u#6saGps<H`dWJ(fmt5^0dZf^(IKi%dC|BIjRO5sRbT>r1m_kVL! ztYskceX~-<!a6wPv?%<O!}-)mU>Cae;hVe5i?<Rl{MreC$vn-GT9znL%h-Tm5+^Ik z#Ib(-CbCCt)M#p}=nnQmlJGw65KQtEs$)N;4Usd}wgbm+)gxQbUe%bOZ_0h)0(D{E z%Vo1y8cLrXD`@<Ptd+IHzF886&Iy<CRp#mHj#!Zu$Ot&|J&tPg8rHHuupe6t<W(@t zH{oVO&9}}~>iQ)4lUd|NUf1<BG_`D(9FZHs8&qW+Kgqx2=S$Tn$f=d_<rf|~hBS_} ze>OA!*`DwO|7iZr@1Wfx8F|;QpSAbQC%R{#7D1cap~{W+zkV7>{65#}oRms=@^hEP zBJj`KKW}FunTiz`{_#0R<q_F7pFq3tUWZaI&EIRTzp)B)p268d36xn<&4+PY$+`0h z+=qU!${x#ysxidl47D`+wUd|UuvleIz+;3q3Br$x8e)3*_G`KR6j<l4vK|nCHRpGf z3IY5Yk17>}KzON%pw#_M;aZ$Tp9o@3qf(a-6ZE6B<;VEuocZl!pAFhk=ngj7y%2kS zy^yS&PpHd8P*#<aQqDC6z?IfA$<(y5M(mWitw_#V&xqW+<gxVpUgx*)1}kb@mx`H! zzpS+H0#%!H)f90PEn~CmcA-X#II;_|*xi9nm@3C`%l!<ypKJG3xJFqV>JMnk)c5X} z>oHwK58|hV`WtD@Ms2l>DEleLmRbp&y&SR$+U6eb^C=A!`p-Y%M#V+k2zYd_PGO{v zYl2-dFOOkBb&zK-_JW7CXuJP2c=%7Nq`cn<1rqQRplp6IhJZ#-EQsU?jQ=6xBEgU3 zybHnl3AY;u*hHf^nw<QPIPg8m%)h=TPf)*vMvR_I5%)#Jx5B7WzAA3KbwyVw^Dxyd zWSNFm^PlAq?<kH}3vRcyN~*N);lEuxeY8L;FLaNH^ZSQ9gXOh1u8%1lf-PpgLFuhJ z91JU$7AfxPN$#7ISz_S?<9H8dU~&_}6n6EJLmWh*=xO%&xw7YtZ!_}znPcYPxctdt z`T+N|x3{yQt({p5a^5=H<a^t>Kctco;X>KMOR@f{;8oS-o>N)m7-j1jL!NmS$+J9A z92tJ7#fv3k_=ZuF$-nqH!_5y}=(m?)z-NqU1HK;<H9rXpJs=qn0#V&1Tc>3eOx&wr zRf;MG?jxM;fxsJ48@!QxN;+w(mSjJ{fcLS0Zk@~XEII1e%ULZy5=~aQIk#MnGn(JE z?fF_Q>pSVd^W$Y0@&~!d0A|2BgsLboC7J*of!#R?@Hd^-`8k=YMiadyndrw5pK%u! zzSTIz06=g)h_*c|-0-t_VBqWAWzXh67%MT8jlmT46e*jHABPWDbcK-)hgX6R6sWZf z>r9`NYEAdQE81#3sWi1VJ{@Va*>Z8P)140v-m%mbdUeF{eAj8?07>S*Fd1i1G(8=0 z=umr~lwd2H$qvT($1sXz+H{)7RZ7J4T?6F+KSf~F)Rxq1x`hAyGp|(5kS@Tnd#UJR zJV#^Bx-Cb4(%!;gtjq940ODC|(eSB@=qfGfKD(N<i+DDhP*-#Jy{MvKj@bXL?El}$ z2iIj9QT3YlxQ4C$RuZ1fMF}zy6s0>^D%Ln0b<e1x|L{Rr%B(>CwO>nxOqbOICYpIZ z)7JXNHIs5TIWou8VVBa6_eECUv-Hn7@-C-KX4E#hn-ua%auw%rs&=HZndnc*A?hYf z+WD4@B2swV2UpcbUCRB9C7}MSkCiuuCS7Vi@xQ#$oY722oGr=s50qA@eONV!d}+)s zq9m5Xs)WwT(sXIDO+6}jx+ff|d0$w6CcUWklgk7ck{Li(uCCfeK2t3L;*h@8h)*qc z`fvU-CHWN@65Bu1$v1C3UO>kx8ZPf&zyuf=m?3B*mun9Tfx1wW(4l8#sr&oRMD9eC zuqzkV_gEV#E866iK^TMBB6>=ztyC-M*_kx82cw=*je|f-h6?uOIt@KNn^8{)yB%La z-E9c7<eZkWHd5SmJ}pWwrR0Spno4?(bS*81m;i7Cq-vNjH)1x7f{K?CF|+-?UGj`s zPlJ63R~~7dh>Z?Ykpn3bM}Rr>mzBrS==X;y<oIED@p*-vGs6mJB^;%(at%Jc%-TCW zgX+e@ad`rTJ;{Ve%IQRm`w_|1{BtH`dDKRdtxRN30G2Dd=TFW|#7D=+I5&pxjdd=5 z!>RjpS-hKM&xW%2^L>gk01l2d8y!v*IscuHyR{NUniZa$b@IP<BFv4gf1%8wK_Omu zM`h^A`G6f=rOkACYdME){lZpD3E>)bgAHrfg$5}=gS4eM2M_9m*H_Fj`iLdW;i#Hf zBdsfC5cWvLXJ`%GzHdm;?dVXV<4|_2-#PKk&bft|G)kgQkr85}7M9fco+3`<`ntm{ z=r`+bz|5aYcEqRT`nZ{xF19Gf09x$MUcel@{)HccAfK0*F^S1eyZV}^ni484n@D+F zX;KA+7}Zj)`O8lUsV0X8?-b(;sX#cSu4T)?OZE3nVmiu<<7Dj>$cchW>^cwxD2MoD ziRq9k9X(El-UMn5Qq33l$8&A`cA{M*SKjK`UkGud8ZrFaH7JlalY&^q91X-SHP1=) zU;asW#D~P4zbN>lKFLn9{NkwqJ`tm3^wdfZ|DpF-Gd)pKt=)R_3<S7~^5Wef`e0RB z7tV?S;QKIrGQG4e`uj&LkhA;8TKDe>>#q62Mds?EjD9euc+DoyYnNx8^kpsO76(<L zd0#J)8Hm^m>gH~gFmb7A%rAmXKXq~uA6EEV7OL|&Q?VL7e!h&LHn;FgvLq(hx}Q|U z(}wq^^>YnsI<pV->oO+Lfz;H=nsS~7C<9^iZtUi-exd^)^?61Q^}9mU3>CrUjBFBy zmc5rN{(|vqnK4<R4Ip+Gxor2ebi&NgCBN&jduu~Re55vmK(0Z!EndUfSc3KxLTblL zuyfReOjaU-H*}YZ<mOeEcWb5rgHmt}k~aPy9~;ykWJ>uu4Q%{<O-n%z4~OXD=+0=8 zWaj=Ycv=02z_FkAZ{HnXYwJP~S>3mZQNs}gce&u>iX6UFC!Ms?q)FP;=ghpzyOXbA z_AXy;PuCxQ9WTNeMc>wqonPw)>E}x3avA10cZheFnLj)&&qF0bkP*}lT=G<ccUz6f z?#ZR)E6m_r8b?GQ-3|#Q-EJLBiT3J>ykMRJ)u+>u0+oNcBGR1dLdtA<@JVOwEgv1_ zLgl5dq$sIdr#{7l@PwfdgnIjE42o8J{O6=(Y$R##4@9_ol<!{=Rgrt*fJVzb^v9wd zFQINtkU^AHHbeBldq9{~!?nVnq6)<`^IAbNJYslxWzxWiJnG65lP>~N#CDwh5Jl_! z$H!CgD>6cHJq>1JRmK!D?mgQ{2eHB^<`bROKp9{ZkXh#6y1g41tCJvn68`B})+ePk z>8%3tz!cds<H;sCMlIA_bHxh8@zmv4Q5RJxclWK|-`kGcyx?n`Kcjri^_=D<dDrJ% zZY7GBxY_;9pjFJYwlIcMZN@5vC~+tTQ@HS&1dM_Rbv!TFx&86*W)kv4;%s2<6JIa2 zOU|m<NO7-~VU9Wzr%LS%M*EI&WjmoBLb-pHm{1^zEX!4fSV7lFpX7Oa+#fh(JWtc1 zOILowXsQR~KH4MC)4>PtM@Pn_4-uqv4?V(wl5}$B4W~0ytv7X#4B8A_tk)kK&XCF9 z7dQ#2nd$kx6Vz2U?ZfCCUnm$;ygA{f{V#qz#6<<-%hxFxt8A$N>4RGmRbv(oZu}}? z;a?02-l^v{{#oqEG>x=gUGQqv^O{w|-*+4Re7kt502|qtP7&wQ!=2k)m{%^<QiMCq zMOXH2w6ZcXC6NscjOID)xNz{^(R-mOy)gOf?eiO>{c28W14L8m*09c8PXqj<AzpyA zL4q6-SBRF!hEpLJ7$>|;RKt4|LclBi^wu$yf!Fsse**9j?stEyF|$P?2%zG4^*<(R zRJM!UmHtFp>8RO%jlQyoRaI$h{3>trEo_ntQ1<kh-ilXw{<|lznB(Pmm3#5`emNzY zQHt3c3It{k`0!g747^Vr=$i5@JEzKgMH4=kuEv^uP>7L4Kpg6eKs(OfSNqKRqSkEY z|M*l(+En=w9O@lp97BtbZ?0v-?|a86Mn!8mGBMJUn$qSGhgESVBy#|Fr<tUr6K@6o z94eX(w?;(3Ql4_@76mA>Y7DwEh*1oY(3YS3><x!n%e9)3BfRk7APs5qaT?)q7#=8! zjR^3Z2vtJdr$7-hE2=?5C-1JE2LWa|Kq(?3E&Kk+6GdQv%VD^kWqRuYq9=Fb2uDz! zZ%N3YJwLhqcsKz-_3xIdA?eV@wu%^N&tZ5ZeMBZ{Y&I`rMS2Pu6qU^7-W-Bg4s&-< zBJyP53tg_5GL8j98xYg@tt5EDsDAG_sxn3ool-`Y18w-I)<CGPtpHS76}NXFB{>le zCXgNq$5SK;vJU4&qMibKalONk+5~`qd{)ipc-&b}7Vl6n($I>EDgn8|IGBaB!-!ZQ zbO6P8IG!??o$yX>Ob7s9K|`8WUf7ncxMxWCu1&aNak-XwMOq;SZ!qq2_3FC<@m<eL z3d7salfUhxum1+Tkol%{VpslUdiY7uQrbu58gm;PLyGdWQOyD*1{-)MZ1_aT%<0~Q zau1x+=|r?iKGd%3PZv+q*N2Af&M)8pwmH5&tWDC~`)*&AkX>QdAz7%6=<zVyNOqBA zi^;&#t!L>&Kn#VT%G4lJUm(D8h@6y@A{Mg=bhE2}T@(^x93lsiq<wC6Z6k|di-@a` zAuBPSL208{7ev&)U6{2N7twuT@KQ&r@hv}~pOGJ!e}&2oXCbidWp3+bCUe{Fru@gp zSJxI91UyND6{U$<DPe7tMWM;{6{pta`5WW)Y&Kr*(Y;x;cItRZc%-o)Wr9l`;1xC% z1=-_DaZ@F%-~nf|Zm|(b!KYIJEK%xsQ;5lQ2swp)x9%xR9G_(mJRya>2jM^f0C{9U z<z1AuA=>Y)Q3F7^;cyf>q=97g<!{Kq&Tb!D)9uU4#xHgjr<LaY0uE(qkA2W@$t8#; zxm3M&@2@?)xlg!;&N)cWVa~3r|0(T2L=>t;lZQ$n`_jdp)|HJfQqaGApnB*uVspi; zYKI!y(z8lIBE#-zE@UiI-4kAia1g+&cQu;j2NhEJ{q>dE-=ji$^$dQuJ-RZF&<aa@ zeKc?U8mZ%Z>FIdu^ZD1aiiIo@%E*6wP-0}E$%PAqciDA87u;K-2h!YL137-8c(J6( z0tN>IOvk`e1&9+UHTDa|6rEgJA@}3815a*h^RYJ1)*6|&D(v(xupKB(Z!V%}An>fh zi<yWEpUNZ0wDo+5xs!g(ba4;l@?71cy`s9O_Dff|jQhAD)>-<NUo+$O^X-JGx%jI! zV`WpEw%b)zH-j!p)LnY~s<xV18@;Mnj#F=wRn0dwX~mRtF|KpfWxf+`A3{-T$g>-{ zGjeJH%&4-YX-UJq($l{)|H|MCiAF#4%;LC-%pZO(mHWs2)lX|<H@o!L!rh8#Qa$}C z0BzTh0m41><warQ*0D;uj)CG1`VwDlV-Pw=1-Dcoh*bT)RsT5kWT4n>8&Q<-k58~T zYhS|F0wL@}mPq0UgY&|sR7SC}mFA2B_I%1F^WODPM_wfVT5nWQW*dIXP<ic0mwAz- zV0iEZZTY<V)%kh260Y>0?`!EQ%|Q`O%IP!tm)Rp4Ngg>GsgdIGDDAn7aRvtX^7C#G zOJfDg>p?91vEa6eI6bj2IxYOJvco*T!_jxdjDj91HNj)$T=*oTr(<&J#NQj2LrYHO zjBNGuuVY6U@E+c=EZdXWC=#;h?A`B)VwEO+{i(ud(U>=E6lw8caCY2kMm&kKd3TJ? zvj>lrl#Zn=I=yIAt;||Cqo(KHqPJ}bk%J@`7siU`TUP6mV$S&uJ}SAAH}zQLb(@?n zccy42=|=sYxnS3Ov)2vkHC^gLxZxJZr&WSE=Y{1uA?((B*LFU({78ZI<vTCc3_Bun zwIb}nbyy>BxK((h4AbCM$Ju`kud1qsjya5NPCCFqa<K*2!f?j-(I-U8>s99rbmMD! zV$c>w+Z609m=O<KQD?tB!e!Uk=j)^g<ri`cPYC|4mv7~qJdL8&N46a+7o=z>h=mj; zkwcu`j75+AEZOLpow#y@@U`_ijRZw=CvF5$M=Qx|Iro?)Cj%kxzL!}RSXJ_2_9MnZ zTd1v7b)V3QImI_rb&}&{3yQ#};d;xwCUpGL+rmcUYW(DCZj1b*K^@Z-0k2-#yyI?I zf0r0*e8DF5V*l~zQHi(i8dpuEzPxJdR$xj-d>NgQ*HO3?B)W<#{V^IJic0*)=RkHx zzzDFX4%?;1moh4ABA;sZ2t8CyxqO&FqXq}6WF9^IzWwL#>-wtm68Au1rw`RKlN)BL zLv^m&_;H$^+;^j_(veumq?Z&`_UPQ(RLRfgS^E-<CVa~9`L*()tA`i-zm2ncV!eSq z&rB7AtTLygA5!+4`LEyCx%6i!%K}C+nQ+MtWQW<_A%w?c7@|2u6feJCwulmjDj<}! z*_Q}H?Gi!IJ%=GiHVW5>anYv_%)v5h+IdcpBzx=@TLKF>DghHljL;GO2p(_51LnQP zF5NY{>xkVhmWowJnr&L9(Z#Xm9xTw}Nz=oC=>|d|gsV&j169t$I5|Pom|fYvXv8~@ z%}afYlnuzH@f)Q@0e>^$kC~^{|M=Wm>LYa&7d&7nEi`-K=4+GwhRE_UmMcF3yyQ`* zTSPjS`jF>jN-t{Wj>u<rAY#xX7ww#)2I3cjs|c9=K_T>!yM@~h+x@(E#h@>Fr#`({ zd?E4yUy|QncFDFZh_m<u1WyreUpK)NsNpOZuI&TQIx+?q=8Nq*t(TwVT4d>3b}l>^ zU(YtEsET2{z2qM7C*;XF*RZY8DMn!9`FfSv2!<~*okY?44MK-l;)&SN@GzB_WxRww zgWCypIOmH8h%cT(6eDaY&Vqr-6PE4u{aVH+#c}JLu$>2GyAWVTsR*SmzT5{C7s8AN zmqJKL?CR+$DA<_@2B_4;+=xZ-X+eH4#R*a%Mkp3IKrVtW42ngy*r>Yy<CAQP>;F}( z)#N@LDdIeC)I;C+DSNuSG&_RFLV-tgMG`=#{G7nZ(T{+c%w<DQ^MDx3lblyk{zGuL zJkurI3vSIyNG4tfQ_?foi6VYtV1-vsi?0X*);wjOBExc_k;11XpFFKOR^?mFvSMq7 zvq!xPLwc=`dM4IFik8}h1a$k`!-}KW%PJz@GL41jlYF@wx$b)%X;u{VDVIXS_gVvM zU1RgD@sYY-OPYd*{1IpIx0fi?gVPVe;;V((UYg2SXZw&!OekkBLWA&4M7*A9{jo8} zJB}q=cRtdQ^-Rw02NYt?mrQR9(F)hUb-0`gc{KRp)oFJKN~rAUBjyXJDy+0k=iF=d z{d}rsH<cxN3$JK-t@^-IPgno(5tAwvk3};t*U&k~=^lQHyHeq>F=>H6m$27At9VAk zHV-01<THfUh+5=;cH9{?h(xrX;Qzs&A$U#EYt9u;`;0%EFuXl}u!wJupWvoNBMqrH zDWu^m8v26Lmc=DbmrEgSI~r({GQQULp28KxF85)Xo>P+gjub+e?oM_(atr01MVAkw z&b|0DcYmiWA3L4=D#_5%|8hU7ba!F5VXFD&gadA)Jg7#&KHnsWW&AVo_2sUKt|CnY zaTZ%d10Xmy(Qm{ZqWD$2@Q){3vtAcy`|E4vr`Vrze+p!i!W*)0-_Zqleys7PEVbdB z`zf0u<W#iKbS35k`+=w5wi>{z&0ESnv65eT{zJ%>eQ&z3M@j*MR%#8&|Igpg3JDgD z{jRV)d@#!n=6c%`Onnxh2qTD|5D>^y9qCzZW#9R0!;;}y3FWe$7Es|C2%T2K>%GJ6 z7r?+{*5=tH0>Z;fZdStIZ6TvjfM}9mqNc4Cfu><O+Q~dpeup}?>FP=u2xKpO>QTwQ zEpWuGD*Ith#<HwpMT0|~jlz6g%g&9I;?2oayw^30R)ZIhl(%;T3rU`k3Bmj_-@%TT zC%c5olhD~Ufq7oho19GoeiMC13+z+M0JMvvVYO_w2vfm_yNKQc0U~fHOg(c$xheBk zTZD1c82H<#qq66TJtYqkIk~FNmOS3xi9}bBZ4bMb(QPQ>nnrSkG*9uDBqzhTfqm7I z8hYcKf*@)Y^hn>33>jhlk*v|b_=!V)6^}iz{7idNfD;+OOf!u{PveAEpLA-q)W0dE z;(Zspy_tt)sCY!F_`571`S>9`x51{7XG^QSs(`2E0eR&^)oFv5_)Z5^_SuX{X@`IB z4pl!tKolIof_&>4_C|ALT6`y30wSL9i)V161M{SN{?L*%Lm(j1o+J+0bjQ%`;aGDH z-EaJ1$F<6zNOz3|q=IMoWeIB~@0&fRQ63I-S`qD{0Z!n0CBINBD^gHWCe{!&5FNw$ zo2@e1=qGm}@y0!175=M{v_ua|+?kXdEC8&*_>Jx#@1}(Jq18ceJXdvC)I_IuX|Ft@ zmwqyXKKOF7hwnv|&hC})GbANu<@Fg0s~MQ6)d#Vrsuok9Q#*c>3@xWLuZH~N(<r`r zH-JlFd3S(T`?VFg3T(x>@cHmPr?>OF@R_gJsl)TbPtwT=nSO&Lh8n9CI~RHX+gY_S zN?dtA^djy;sw*O+{_lOIksL9nD}IN8i9q=mOa_mgn2KIF=p9EO^QH7AwY|x_L;J?- zbY$1ONn(z@<4kB>PN>_={{4VW8?BLxV-U15zItB4_r^-=$6%*gUq|I)#v`nqrITw8 zjY*mG4Y{ePSCtm!mNgMoc+YD|t>OS5w!!8Ck{B#(MA#%&g%z!OBEV%O5IJ|F!1~ta z=aJ9cq?TR+c&5TAV`k7wvFgbJO<<zln>7(@r1xBdcarn8Irb4pVEHcXS^a0KjOK*8 zw!~Wf@zsf}22<_DN45Xy!#8E5ks3vh`~ZLM*f&dMu39LybQPIrs+s<NAs?YN<<ugi z@^e&_lnxBDwIY#LCo*cr$-0&^%`pI}U<f^sOiK=+efAs~G7g~yBW)`7iVYm`fGA~x zK8Ioa0WJ=n1YpPrYFyguL$AgQ>E%xo8fJYF8gvdz6<Rv%pgbTgLWfXT?otQA^8CTQ zSVq64dI$)<&ZPmh6cFZPUx*ur5Lj74s%$-#r;C~ZV0r+ekYEI#B#wYR7K23ufkGlV zE%`{lR#-ppIME=uMl>FlR)Az)V?mNoO~dj=DHJ@pgajm@UFc`U;xQOfgma$y1SQrL z=xcwp;GP87bcdLbVNrMvf{n^VmukzFw5|Pd+gz)O<y^AB|NK{P?Ol*CyqqNf0(Bu8 zxW;)lN4?e2u)nF27kUGqjm=6d{_=!=fr=L{rnR<ca^t^CP4-2pvs4bo)Mq?4&`EW@ zWmq=io@*ql_l+P!$~wW5{<k!boyYpqN8rbO-o;uX58izn%xNha%_x+(X-+iv=jQ$# z;QZzc-j%dH{A0z<Q}U}lUUH8JC9^_Gf5xtuaG0Q`aY3H~Yb90VqSY6D(P>6j5Jvf_ zH{o#lK?s8B$Qr@=GvbX#-NTwY>2e+tR%I<w<a7Ru*86m)2zvOLcZ&R$Iwp;VddZKQ zc==~m9K{3HSi;ZZjSp2scvr}>H)`_rPYAf&x4t2S(8Ia!*82R^R@AP(<$QW4D*_DK zv*&sus&bh)x4JKK_3!)g6aQVu*Rt#f0C{yToWiHSl$ko`TRfQiRK8p5eR?d-mHz2U z`EAW~4h%%U(3$&jp+xp2QHss%ZSBjBMg76bgX4^g;eeALO}~_XHZu=KFo`&MI@Gru ztGfkQ&%da&)F4Xl70uurSl!}hc)-90>XuEa8e)}qh|1Gp3D>>8^)HH<kgGp#=hko1 z;hIAptLHBvrAIT)J))PB8uyyFQGHa#=_~`D`YxujNfz3wJQbV_eeOTTmfqE7m4OQ| z^1>#Kkp@z+QYW8azgAv;nYii&F95SSocY=(eCmpy7}KDfBW)_Sz?SG2Hshw`#`dI7 z3N8(LY%lIBG)#%yM;dC!w>McVcrUItK`$6;=2fZHMPAz27|a{|cRucosKPHOc$Qqo z{PU!}OZPQG{jO2v$CoE=^9t7s$P$kZ{dJeLr=E?#{+0h#bQJ99pmAgI>;Pp^m)`P- z4bQ@UR0*nv6E*hg7X!8269uB4G-p1Zf=X)-_L}>L!dwYJfSmHC>%&~ld8t2_<k~`U zh=S6I9jCmn8uxa_PeoOh*WD%?=u~_TC|8RGK0Rj5M|$!_2fzYf@oOMb9X$Oj_2_0i zjI^rlr+ip>$Pkp-?AU{%UtF}M(G`z{5T`xD1ZhH|u99fXZ<T<`E1Lgf>n(tye51bq z-DSZA7F>{6Qo6f@1!YNzr8}h?qy$8irMtVNyStU{?h@$`L;)2kdExi`=b3l@^D;vW zHQXQ0eXjeu&i8yzeCW<>UoV#|7h4grzEx$i$s2dpGK-Ni5=qu9=86#~B6glRGPXCw z^1&t}vKq6cCGmgfhj#RX|CCyI_nc{o(EA0!^_~qs#Ls~^m<>R*I`c&{roiL1@Ygsp z^N56JAraOZcma$LM6|R_o|}{&IIc?IP(yy2OuIn0x%`pOzuO7Sk|5e+KL06*w_?WQ z!y=!U|74i+bL4%)Gh^MkLa6*d)Bf5zI{4pPN43Y{%o8kcwb`d#l+}&%#D=|<w1+Cn z<}l64(ZkP%Pd|z*MQjzaofb{2@j|g=<zs*I<sN&c>smOfN2J#t#j()Z&`THuZh_dS zd#ar{!)AOiav4N5OMX+dL|#XQ#+k3<7iAugaJ~o^S2Cpp)R5%xV2keB?YJ~k^Jj*W z*sjt$nNY$tqmBjIH2ne{dBJXtBQyk`AfyTQaM=}zg4{J^ao|b*kGkm947nelw6HQ) z4wfXUEFSn=N)hov1bSzmXJI&^D$3&3Ff;n}80f>?U6p&aSin$Hh~kck&76Ha%5Hqd z$;ig%JkXkr#D^sHP0>8ChhYq=JIm7oa8J`uDlhVyM)%(f%J6k(v80LR-{HE>g@9(I z=>er6%Q=gjqtHk#oX8XxeSjQi`jg;v1J_9sX>XGO&;+2y&!imeOyn_dN|Gue7a1Fk z!yganDA8^t=S$B|CBuXX|B~AD`nUiL2gRS_A4XD2j+XK*k;vttRbvUva>bl+dg2^z zpN@t`ep~A{|M?3DRLEJ#fZuO3G};ub41=+bY8Or4))%(TQngy(QTgVMx(lp?(80DZ z*i#G2IU}Uz$=P4JnK0$$a}0<6p2)QMb25%pyB~#jzyC735xD`J)W|JFM~?#C_Y%7) zx8;A1E&S4&ba27C_mZiSRcCO$3zAlq7EQ6`g?(Hj-AYBs^xB8ZoU1U5xvVEQ{AuC3 z=vBuKk;ty#yESus?!}p$IkBZ!8&DUxAN`V!QznC7la9&etQQdQ&3Pl|j3ERnhx=vO zR3S-8<9WJZ&|aZ3SMKliU{J2?j<kBCyI)u^J|rNci|~nDg^jRg=}wzP6nR20hp?H2 zTZrD-uMn2P@#oH6%Oh4~29sP|4hI*7H{Lr>5S;>>EM&yrZ3F456neaZsqkU*#}RBh zjA5=J>*6tTMB370ULy))4Fj#VBb8Nx4c%g&T0(U7#vFRWFj}P6S{FOsbx-Uo%lwNa zjuXStxzTvw6O7PAu=MKRukQvzofI5*PqhP{yhFEz7-;a)J7KP7c$TbI(y1p6Y#~D; z^nwLWbGy>w?=8eHt{TnEt_HS@skYZCg}r7+JjA^B;gzdJmtLp4H0q4^27?ro-s+0h zLM9oLhMzwaYt`>zu>l|eFa(pCW~l34Jx$IRxD^7##twRl8HD*UAO+tvc4Z)PTr5=` zn-8oJq`{M+q4)OHpI5!uluG3Ee8{OTiCNeS6mGZ0sn8=la)B#L=P)!N7zm*70iZR7 z;bMe;b%JwK(*gi^S{^waa$fiIG=#}9aQ*hF(grrZ>~xtJMD{WKpm=5EvS8_Q>%b93 zH(k1J8_Pb>O^=#R8Cl9y+P1AJ-cns+RKH4?cTmiK;8XnyjL_=L*Z}-#=Pvk=Pd1>n z1}n%NyrLDTp@V_&lViJ(kwv?I>Tj6ba-#J)yO2Pa_9Q)vx`O$bvED$w`y+odDu?r^ z5f8Z&fwE@iXPt(FKNbf3B{cM2MQRrm#EoqIv0C>2g<kVarnYC_HTXJ&C;IUvoQMvH z2|$8<{7JU)G&r7V;H+FuP9o<1IB6HGyZ^OyXTPCsj_ZwRIzYn#rYJR@Av8Kx7|F>u z?~PBV4y52a)IT}v5{=mkWgE2pSRvDhf%s;IPX1}b#sD*pank`52KtO8AtXz)5ZQtW zFHDInd)}?URcC@Bd$vqC%!(#<Q?-?)Y5uIytp38_#5P=NOdS9XO^@Fhu9$l=)@k6_ zX)f@;`IWLlnQWmN%Q?WHdVaGvF+_?xm#Cq1UO~H9b$q`tA*XaZChVR|aoVkY=2=7@ zb=sxhe$kkjRoZ*(JlmoJ1q)-Rgw}R)avCY@?IYA?!%&-D*qct9-=)64IYrXlp@hN- z_8%Pl=86W@xH2*`Ek({HO}<(7q`lwmF*##5N}KhSTjny0QO~+~x2lg-F8f_bpQW9y zkS>aCOieI5sb9}=!Anw2fUHC~5R6$6z$nQ7bDmj>F$#j-0;GN1v5LZjqX3OMHdA6e zp{B69$Q->7Q0Qxdz-|c`4TmNKL;^rgFx<e>?G&&^H?UWX?XpJmTeFSAMsVN07xRs~ zFjYKs&Iq83_4&~ERBrIg7pcV9etQj7j8<{cAdZLl`5@AV2!w2E>^wWeQ6#CD9RHf? zlYN+C#r_J{H@el}`GBOw+I<QCt~tp#&w({8P5CdAzaZUD1?GqXeZ>S~t)@x;mYQ%7 zJyYbOC0QY@z&q7I@jIc<r@y&xUq_bhLzf+2mMV19CHY2GGRqx0R2J$d+oqwBV@5tz z1R*JU38`DOWd(kixN*YfhgLVvSYO?U#oW6wWde<ev#EI`wqG*Pf>~aHXlnS_oT08% z#2c^CCNyY}UqB=eFYT5qGtmGjk-+sbPMAv)|2;S&KQqlIm0FhIg^xMd>?!OMWo<a# z927}Ipc@u}R@rQd2y9a$-ds>FIW=@sEES2vW7h3|GE}$hIU_?wF7F<Hf2DU*@<h{Z z(}Ly#4}8iHP`Xf{K?xAIe~%k}Y?jZUWY}inq(r0uCaQV?dAkLci+;<aC6ukMm>2&s z>$6Vto%^Z`lSf|SGGpmjvXR>W*6tKpa2d=5@K~f_A`HD{`th6=EhV4#>uq8&ZFnH9 zpP6YQlU~!y>5pWw8<FNb-e$VtCUR`9G-zuteY5TjBtOPu9_KEADh;d;M`<6r-QUxT zoJwGYJfc@lbvE?_r-rijPiH_*Kg+?}PaPJo2;t-@%mylScv8>M$K1fh5I;?vF2Py- zByBCxoocGBe++a8IM4kS7}L($q{svXk`)*T@X~#~$^Dx+?6P~~4;jcbr~@!HfYP-< zVwp*r<&^e?-4xALtzma?E*6iHO2+SL&n>n5=l?UOCw=dal(hs%drDPL0@jZUp=0?m zBz{qGQC;L|RkwKe<8!&9TD)i4Hv4hat=s_rulM^O_TZ!Ke1AJ#j;r@O-Xy<Tg^4p6 zw*TDKaZS3mP5v^d3xVK6xd=k=;8Q%#QxLl344)g%s{77OJ*TSA|0qMwlJyIEFPE(- zdmop|-(42;rm=ozU9VDPHOmOu;#v!*C@Lt5)CED1Pxun1YG`c34oERj?#g2q#7c87 zP<-&-#{mQ{XW7E)jTr;<#sk<?^XdA{aU`9)<rdZ>Cm<_5V_jl{(@+LW&e4r18WX9P za{NRX1g<=%NLn&~LO87FS9So2-_Q)@@?jhSHW2>v@?$Xk^wbXk4^4~3eCFJJYD#y^ zjQyW~i@(7b!Up4>Pfe)UAQT0kn}|k<q(b?sgMy&ChJ#~ZY|q(gJTeCSR=hHX?!YG& zJzcnBB$%d1jB^}no+*A@e1(Q3)2ax1-_1dK_T%OLcz%Xw*2)#2JEYQjQZfYjszk|i zg_d|EPRno@toBz~$L;Q|N2T3w-ZW2_-mLp@4izWB;rM)aae3P&mo&oq{#9_GnM|n~ z=BK8dFN21xL`}c!FTe$M!NVB$BhlGHE-wEyud!O5UdZBXUoUHW%`W#;;-Op$LM(OF zwX2mr96z@!QmeSU-4oX7Nl$5O_Wk#YJ4t7M_qtSxW0E2v8+oIU55F%Q4G>qZ6}AAn z+h)I6u&92EvFB2l<^xNZ#A6LCpP6qD&6NE2{KNRHk1I=M#S3`GTTWHHV~r?|nYZxR zac;_W7>fIer}`?OViQ{xCilDh$}QUePlosFCxWJNRFn#uvY@~&+D#TF(2}xWoh?Zp zFw?Z;D?3M)2I}9E+h9E23m43C{vLBB_N~F^N{qqpt8*V=&at@~wkC@@*>?O-8}L1z z$${TT-y<IG`}=;Z*ZXh2ly>OMynpRnEmW^)_wnn)r|)$QM+4D=^a6<b=L4+Y?<cJv z>vtyzxFvA@{M=|<C<nq8F=h7{BL?qG`sZwRRm3)0dDrx8yv60>-;776sEzt%>N)sl z<(DSZy9z2C@Yrf`4*9Y<l<Y5}5IZwW>c9QW67a+Pf>L>K8}YjBj6tPYkUvO5^N0MS zCrn8mX0bZ$csZ^GuBB5kY4~VmgIPrXwhkMugRk*{tYswd3!+H{Hz^tZxUYL!_xLcR zHs9^{Y#V!~5{si+>Qp+JawL5wQrQI?VoBmd8#dI^e*WI~_EzP-&Gj7NEA#`o<k*7h zEmSd&)vSqE!4DNOzEBB)ul`E9Rq|QAV5p?H_xU`RCs6p)TfSl%9@erO8+tL?G|kAA z7Kf?|rt3P@#INvM&@OsqR+mEa{yjWpP1w?^Ib!_%YKDr6aTf1JJobQFBtKu14%Kyi z_&e6x*s<18GD|0Kh=yydRc?E&&&B(Kf|y3uY^9e8c;Q2^gt`X4yqfq&VME|>pX+Aa zkAH^tzqg)g46P^WTS*Di(`re?7Dx!lKJbZPO%(d1Z6+huob^5F>Fv$sOPI899V`PM zr%2V#3PuAA%<N%X8R7vFFxLFrDAGOe)L~f{I+<Cyf!6$7E9%8=`_<}V`RBa0Om?eC zWz>=IEw9OB@2A<~B)_&ZL10c#5~=M0Cmp2EMal9t_eK}orhyWou0Si$!It)0tqeSB zHnr2km_taHT5*^>RFQwKZKQB&PKWsCJJa3tVX>hyrg&<fsh@0}cf>`yR5TIf(?Dyz zoPY^2<-_vpa!)hwa!=@^6*C?!X(}I$BbQ@Mh%jmun`qE)VK(C0+O}I;_zp90+BqRk z&FwY~!?J7spzhZE{@qcgM!BYJ8odn5B0H*E<b07ntl3CO-u8Xlg<b-8gL^+bZ}2~T z-Nj1*gb$#1dB|faZ@Y+~Tk3rB>9}I}x@PLmulO1hCq<FIV=`Kbv{sLVnh&7^7I0m< zQa;d6vO!Z$rg%aTW%U+xV4;|=h*0uAlV$O$qqzLAnf6ds-AGXP9G3c(O}^@^bW0sO zoVbQrttb3L_I}uvv79Pa7mi<~T}lv5b}(TW0w#GzCEZWSl{X<BTh}Ln{{(8NZfi41 zon2gZ9UBg<pL4|j`HNY$WSBtZ*jL?dHh!WqZ@#a7zE+@rnJZfQDp!b|yn8#B*N%Xp z-}FX(QT~>C>*`JIZhGC5*mm*(=tvLU9I=fIq8;zb@yjrkw~=_2<eINaR@MHb6758W z{(Up@*zt*oXbM|{5Pkt;YuL6}<<zgL<cIkAj+{dT1-^Sa#qaj_tc7;u;Ex)6LiOg@ zP~YWna@3E9ifJRU(HLsdU6DV&7s<~6%X?&nbX0mW)w-HeKe)AKx#W`p+Hqv8?a20g zD&o%Rt}JWz%nqf|L0VciuwiQ$q7k!gm?E=XYx6vK?KN3XlT@Uw1R=Qpd37H8U?fsR zUVq0L+wQzlU46j@HbLaPA)<%R-^HCCduBm|W+uopOnKUcdh$zHW74hW5pqYD7qU9X zn&lelY@^IA%)HL=G~X+Q)Pj?|EW*m-TcLYeTcKEM*iOW&7RQ0Jb@O!$%E>HQ|DAzb zGc+yG#7r*N!>-+k6+cZxk9d0cam^8(Y+M2?APXCA#H)iZ%{!*vFoT|Z12BOzS;ao^ zxs;M-a|eu$SJo5CO^eAp$er<T<B~@<#IegIKK|bmpZ_$x;2?UkgEBvs(zK%i3{tp{ zm>DnVm?whgJRsl+rfET`DP2!fKEDW)^V3igMtb)!<m>0g)>tf*nB?`mMr${3l?%@* zp8v5459X(8eX-N)7HiiG$B-DNl<tzND>FUMSI7<q&nN_peu`2gGYSQg?ZSf6h;_HY zSjehJSotovun$l5FcIBsUWs@rHsmDlNYuystO%MoX<c{hb{El`E=4uv+>4(}E$y7{ zZKO``MlWLCeZ3YGta|;!JcfxeA=SuhYsu8iHa9lu(9vCeN7$V*bW0CEz;vfovWKNX zIHz>XkQz_-icz^Uy6VvzTk;1!rxLi|-lF&7o<|=A|6~4^p3a@d7~7N*ob4ADG%shh z3UmYj^J%H04oP`8{2*iGWCWy29ui`8Yr`97NSYcRHta_@q=#=v?3g3b2}ZMK&T6(; zKrYdQsBx_nEut#Q;MMZjZqoE>ld6GKv$8@rmtZo`hg_C>Bf!8q34gFja$xV5UP9Dl z6w8vk2c?j#v|0qhf{1L|D$GVIr=+`wR$@RCEF+OVBmja$aNG;vNJ2M1S0(0NScXLw zj(Y>GaW6wlR-L&^ZJ~NO>Si^JI}-FlMq`I<?Ko*C*?u{?NuU4u?tZ;rEB=Y!+|3Jp zzK9m@@QnGV_o>O#dtToFadLW7H`;Q}wie}iqNMmW>tvz6SB&*jQ@785{ik;_26XQ9 zhEqW1E&scFOPA}-9U+1tOY@`;DyMcCOA0(fn6oxb@;tb>8v1?XL22uOWce`X)V=WT z$XG?VBS<9ppoEMloHaa070v@F0Sh8?U?8Xj%}1$RbZqLzNGJga1c)j084p&TmsZ8L zdPGQ1SB8thkiaF#MBnm}M<Q1BAkx8t^>xFa_jr#%v_o<LHW0jkR9ukJFIFSbjE9D2 z8%B_e>&#lvgMorTU_=&@!6+OnARWwYQiFzwZ--x5O+?0=0IYN9lNa`b&G6*jHKx4j zgp!E{3j)9ZQ6OuBqcG7ioX}Mz6y?|)HqrMA(LA6Fz3krk!mR)I8t|b6+b@YrL+HJ# z$brHBFvkzwrEwK4%>3W_eq+&r5CeW(0`NhE1gKV{CPxY`-7<DPRR8b|ja5}tt$y}x zu=1yt<^f_jR7>qZz(5eZgdAh-cP{)=>%_Vx*f7|pVtP`;(siDhF6qbG+I(dBNxiNM zrB%zko&zub%N_Ug6r268iX=5(MWgjTy!w4T_QSIJy2YL%Yk~5_d-um~r(OKTm-kx> z_PPTN$2_&ORBuWPut|M4yfS}Tseu6Ws8Fx4JV^-eJ2C*1;;U37(v~hRSjq|~JRPoX zz1$t;5A4Dq3Wavz@L-z^NfKQv?l52~M?Gcp!+FfqK%+aahx6U!Fns5^i|O_u=!?4J zj4`Atil9ip4B`c3QX-a4<%t{-%aQ_YnkjcnteOI0B^n}tSt_1K5BHakcBT-TIdcl| zNPwIUOUVQxtP%jU*oGrK$HawB_^^GL>%3N-kQIH3^57?=umD-J32KcZRF^AQ&Xf+F z%xA+Xn<bzhOCtzS#@cD@2V>J{?rkDtx0#gA;ULN`2x<r>ScI~fHPgfaQ%VHz?KzIm z^c61Ke_rb4?!CKCzI&JWLNi(X?(^SooGj~v)q|NMan}=b1FoZY6Tbi6?CMmGl!{uX z+FxFclDRh>mwA3JbEw^;Y_#K#ob%@w_qJ<hZ1%m=Uq7~|s4a1IG9s#W`s#YOf8n)! zvUe7RlK7o-i85}y9svR9x;?zFst@TLSZJ&$e?&q&u!?F8x>b8M@^-#KMty!*W)(gP zQQha$mMRbRL@eZ+e0|^}AOgjzV!p3mn5`F@>opcKntR=<R$vS3V7Lbsn*}ec3P;$= z;?x`qhpEQnbiKu=gTBJcYn3cbD#dq=6E4vzGaF@Vqm%(oC0Jys_V{ygJ%;tT#w*YU zb#+S-?&&QQI9aRJZ_TGS+^2xMJKu-b+pVR)@s96S+Wr~cKSlTdP+l*8N41>ln8vb8 z;4TJhsnLi|J1Spkn7seyi%<7&<l@fnt`-`zR*j9W%~T?+2^Ns97h1z$bo7E75nX%Z zGA@p+xev{rVR64qR7BTT3D=sOo|P`o8Y_!H);E7mm*~u1dau1-|2*9My+5nmv38I{ z`s+_QRySxJ-)JPXN^6`w+CP-zT)j_SCPZU~N!~u??41D7?*~3+&%lV>z|qM9jyl5l z__7#*<hsLrx^(p{8*f&^O39_!FwYXRTI%4^cvOj69>400yUAIc>#OiaJ3KVlKS$MA z?hiEuUhM3rk8XEYPD58QRCgCgB1F#9&T1hLD9-&nmeBBiP~0qvaCk4}HuJ~)r@eY> z>Z=4R^KgDzWtZD9D^fp-iD3Y+5IcdQKx!p;pVC7TOW(y`3Y{*lg>-I|nX_VyxE$~r z1qLIH@u_@CY3(9S7r*m&?Ula4s2PetEI5(IHqz`Rxq=?0W1<~%DEVkZ2Ly@*FgcI0 zM*J{SyGzU7fr5CYA=q9iCD0K(JaDcR5?Pfx5{#jNRpWOEieddl$wrtvbZ^z3f|Jx* z)ql$z%l{{~JMV!{+VdksXx_T!jk<^X&-MwTuoZucl?c(c;35)zclM$;91t`VK+~XW z#*xgF9&-QxpZG9u?t3?GC9FjtxwYvG0oJU6XnlzCSQtUI9J(a_SA6sWhfkMik9-?& z=macHpGad$B9Y%vfchu)*xpY<CUGJgwC)`!DB;2arPRpou0e^HhE3l%unG<q)IQ4Z zi-+^=HGL&>KeIac9;zT<_!RHFkwsR`=9!?+XsF|<y{ITviW)(ac9*_Jrzo{y8F%2c z1n?v4B~8vSgW*dqV>7m9^4LX!e)^oA1x?*4Cp?VXB5aEJiFm!e(XT&Ker9uJk9Kx% zZ;Gc>-);W5k~V|zlV6l@KUvA?30BvX)c%*n=y5nU`ZFlzKYzA*-U_~0lF_M7D9?x_ zVJo@rAL<|GP`*gYlVJEb6d%nOtU=83LMEVjO8kNz#?4!d@qw3bjlP)&R!Wu5+pV2h zlNJ>Y*Ih;jq;}s2tN_tyEJ~MUSRNAClP_Bux=mZ!Ck;L96OyfmhzSQG9A1&xGLe`j z*?CpQ$$<TNXkr5;X~3Z-!aw1uFcy<U-he4W^zs1Mu#hXMEJ6nhgB+J69w1M42I$!} z$HXL$#{wMYv6*58gZWFaLP+|nnUy__{pV>AIF{pNbjkdkiLHNvq`1#J2|ojdT-%3! z=_R)E$1iydoJX!TCYIf158ex9T`ILF`9^;ypA{92dTn276;?X&kL8xUQ!nxMZfu^v zObs^YZ)^8%c!-}ntqOz%hQSm_kR&}yzmYtoaL!J>Dz{-*L$@MsB%VVccm1<##$l29 z^M5}mT?VN<^^6M3-?25cY{#%`XqaCxs+He)kQE(|eb;jmdS$!*FNfLN$$&NGMIGGE z$WCKe!2$rL<wc`lGz9!}wLEYY9&a~3RUQGuX2Be!(3u@L+4^ZhFjcU5cUl^QFxxC& zi7Mi+DxX(Juc-hqRdvm7t2=gbkDWQgCY|JMYA7@ookJVmo5SoEnk73&MmC*kkCx9~ z+mjO`BQ!JlsT&LB!oUQsZbHV!b2M;*p{hhY!tP$$*8V)P9K8A@_hzsCB}*Thl%<Jz zc>IV^L_h{2GPEHB6*mC^gDeLP1YS{<6B>+z5e6deEPde9ZURMEVBY($cI5@4)d~hS zVz<m_MIv~*db>=<oUO2A^Xw^&r*7W~yCm`}ix3o+63waEBdc#-+<a<KQcem(DwWrQ zq&g=%CSO>uP90N4Yq^$2-j)b;G%eo!JN<XN)~K@XyDd-TYei2OjvaR0%sI1CZ3QuK zz+2T2w!~St3JWw>Z(~c=0pvjCz|m~R3?NlLlQ)NkpIh%{bZ^8FzwW-6R){dA^ye?l zd>za#TQVIhWgu!%;1YZMfr#kRaR2;YHp>TKe%Z}nj`BPd*@rQIRS^gwv<*WO6gtmp zhw?RmnNIs9a|Wm*U3e`MG~#j?_;}n*0tWX+vO<HAE9F46g(f<b4eJz+_mTuSY03iV z&W&dd%cXkYqa-Meh*Me30bm}{6qt%31F^`o=%3Q{Nt2XZQa(yPeDsZ@?$V+T=1<CV zm>|iAtH?YgQc@sgN3d~hE1hE~kf+66ocT<mAy)F_Fr#GCJ)7=GF74SHn%h_Rr#3`{ z^38AV{<+wxk66jPVRBC8rqy$I?tCKYzODIiH1_>!Ze^cPAEHuFWX^ZmF-|{}GD|1C z()pNQiX%(fpRv+8Hqo>&EjL=Cdm(c^7%eLq2a2`Ac$7Q;ml%Wz-PjTxS{Ohx!6E4k z#yHsBTR8!^NqBa_0)P!&m;vUL+#{Y15dzwlgdPaog^Y0p91VH*1LH;ps$YQ3z0Q`T zw2op_v;hk~vVRR=fZ%auh<->Jf^A{YZ~gN1m{)~7@QDx=6AH}RBNkX>fdBp-XnHvp zF}Futwy+0(&+RtAk1y)cK23y_HFAG`pPO&45FfH@FTV})suP;a((|gCFej5m^=`Bf zFG&J*tvLMQ4K&z_Fw%I2k>dNy|LwNEeJPqNIdouh8stCX-*X+`JoRZ$b--rh>^j`4 zRJlBm_65gs`exh+)p99J!oAcaCr7OcUG;n9xXg_bQve31CRcNVKH>?UwPI2a^;W~g zoFBW<!r6H1;uh|ghjjbJGX%h-DVP`D3i9v4*zjTagAG(%6nAMU!nTf82N%89E`djr zYOBWf(%7zd?z#?iq;dXc;$`us^8Ee-&ZPV`(@;79KY@}6G(@2$@k~qU{;$4b`D6E{ z^ERoJ{ee%dh!`TYU^$};Ax|nFWF_e%pC|f=Ws`75giZr8gx{mi92vM}%Pw>4d&_<I zn?Hm8#h9Tx?h!v5&(qP#|J}c8{m+XZl2}l8$I4->%C87Jx-uAm19W=PO#hBZdFQKs zFbR+47BXic3vUL55>s_4&E5aWtyaWeO%e{06fS*EVLbk3hK9<Je3wLiRwHi>{sR~6 z_rop=8g7H-SA(Gj5t|THZ!G%VuVm7SIg!}vB#mVZ+cJnsO)O;790+%c5*p1M&AKTw zS#NIqp)p$WUGs}I9`p-%Fc<#G&$&%LQ;@s#W#srQYAPq9=$^yB5cfIn>bv9lyaAa# z=EoLO@r?6$G!;>KhNK09Ha+QdF%Ntug&Pp)qILbX`=7YGu1pTcF*Xa&JPecWFy@u{ z|6l)q6mkGCfQGc8nNgKZkZG=+XLPK8Y+TZ~(#jiTA0p$jfSE~AC$ncxY3hTxYw-eZ z#MiceePnCmSr|7TOIrGw4;WQPzoi_Xe1Fn#(0<kedS6k`B65e$5t5rzGxBWp<eunt zzLMrzRLiXy3beg_t2I*Fj5Vt)x@4P9!tzz@EiF4%umBMl<oeOX--!q-_G<_9GBaKl zVn8dWny~M%i7zk^xs^jOwIvxq1Q-SL21VS=pjgyz8?If$EWU@0WZmsiD@C#jIv|bT zW*_qXx}uIWjf_0dJXhxF4bO>w#_4;1=35EneNG<mz~`H|CZdXZB4(=~P!iHa(0?ww zg$%{S+;*oX#3KHG^7;Q_<^O&c2q2yUrZZeBnRiI!Y<zb+;YpBxlO6nqGdFfRW0V|A zI`I9v*PKS{ZJ)!un_BgA?Y6R7oRxC5Lhc*4n=Ovq!PeVC4}&prh^;|d#>MXveTAV9 zJSV%no?C-2>Tkg--&b@kkK=yd+H;hQFR_#jazJAG)A?-xJ+FyrK{)2*wlBc3IYfVC z5^&ISEkY%e$=k7LC=D3i9m<odv{bi1dTWq9*URk^O+*QL<iW__LlgV;;H^;BlLWUo z@L=qP%Im9i+wZx(ZrCH~!rbO_-P8B!;g9D7pG8C_*QXZ~G22r=#$?){)Mn|q1Vz*? z&H~OBLw5hm=YM**(KyP=Cucu+;lRyMBX3DnbIhG*weE&~hL#1t$y{_HQInaeQP6M_ zB_}Cii&I<-)&hDjg@om}s@AI*Iha7>uX)?u9)Qax5{9_f(f5P>$kMxm<NP9~pQC1O za46ZB;s(F~5Lg`zd4$(r$q(S9seufch1&ooRKMK0-S@L|K6}+r4wY7y8psL({_@mV zX8S61G8{=5Nf`Sre|Tz$c^ckU;tmCFI!9C(TF98hTpRcrx>K1bZ1H0&q$FLvWpr(& zbb9WQ-5=z8Hgbz6N^|vzPM^?!mHRBezBhLUhEhech%uS=1QDnG`}$x1#n1cs1yRU# z#pr3@Z&J(P*t10Eg~y2|L_}zjfzJQs^Ix!Z5qfAC;8<$$fD+2YSOC6ob55Sb?RUQf zQdHHi4qTF_T_Xy_eEec==XWr{JOI@i!WyZ>lTlXbg@e)kUA3Yev}Kvm?`Hv;1x;nD z>2|L$s(zYGDU;2nqcVAE`Vx}m+c01v5>R}0D(P8^?qHluTtLa-M|_bwMj6a?;y4Qu zAlNp1lKgt8LtC+KBrC0me3u_9uhWTNAWyDOS-y}IDDymCz)Qi$OJ-YrW}m%*8Hp>y zmL(iG77sEWc`=<(SXV2T4@w@OZNqjPYqICQJ8y1nE9Zz@u(WXhcgx8s(2Jr1bn7Pf zN7m=l-`yQP#LuY&10w8xekJAYWaWNxD{0Nq2#K&JH!;TSOYnoa=)FElwtP%s8C^ou zIY1nfGEI%sb@W~UMX1Uj30t5cK^*D7PhJkdV6r1l3b52%c(t%&q6%<kO5Y<k(W*ld zy-aLQPB5diX^S(c3>Tt<_9e3O5)y{Ss-Bzj%O|}3xtVP}pIUGlFJa+Vx)MrX8pwqk z$X-LN)v=o#h&ExFhV6fpZzqp!4`16>o^l+vEwjsvReXmqq?;w9g($*#6N=yc`&%dZ z10<tAQJGAIU#+RC{O07EujvQbd$ruEDTSQH-F^pE%XYUXPf4$|di0eFmDCl~Co5Hy z#`1i|c=M@<TGLr7HcfL6x=2Vd*^bB!`q#4u1&O8pc|7pBloCLAf%ep>KRsXguKhe< zTg$uke!?9MP?{g`|IW|<cjEbfew+jVSnIgs-Hws|3Kr?*p&7e~2<4F9)%sTt`==v2 zc{q;~oLt2nywvoyzoKvSHmB26%Bry(>OQIjhc*umBx5zm3mxazR&rdwer)x^0M&OS ztv_5$6`ai5Tz8!?j>tOV4gV<PImr9hU04<LsHd%IVr-V06g$Kp$UC5%7>29Wqyy`7 z-E{vG8}`~H*5pf|e`>Nw+!!qlAsPuK07#e!lc*~bk~{CIhzB!Xbe>9CISH?B`<b?| z=`iODTbAfli!GiGXa9ZRGyF^p(e&qSn!`b@1^3k(_sYwQrMyu}YU!)SqZ1^9m138M z-x~qL#M(@y{bw~lk{uR>-jg`2pFfWl`o5Z$dAGo}Tc}w#GQYm-tD`m$OT|D8TU`8m z{9Dwn%`uwG*xf>slblOm^BEVWzB+dFBlA(lFwKyOM86#LQ#f$yvapA!6d$?ERlLpw zWjP}!BI!?Pn|`N1Odl~qS>Wq!0vcoXztuff21(EVicPdZoYq+KyWv*NT9p17!$|E- z8B_Z16^t%kFZKSV@0h=E#O47r3KgvDm~DicBLcL~UCc_b495j$m6<PpX>vr_JgYJg z)`xK^vuoHp56l@8W_<Ui75K{UQOh@-Ddc+BAkwlOe9=Aryik}ktoDJ=oqQkSy~#VC zn=#>fRSbd;Ux!iZe&bP^5gMN(zM!7!|C+Wrx_rj$m^;j1Hn~tK&RkfS_u<=Ys41&M zb#<E#$b89}j$6!0=rKw1&8cVqcVy>f;|s?uoh7HS->?3S6pAK>D^uo$sQ8|my@~I{ zHdR>>Go_b17)dr{tn4(_()o&bUfP3!_jyp|^m0sQYqp~KnTW_UI+bGea&7%$XC4W4 zuP4cxJq-TkXb-6r4m!JM@*74R2|eko9h}2vAO%TDW8-}HWRaLHP3%HD-Fn(@yNz#u zS?2Ro7lnoXQYRylTbd*b8=gvbmQ{{;&BWNRIyF&cCh3BBd#0P39TO2>Ye`t5HInTn zL&~({ZT?8YjX*l+z+8U2kGSK3&yj=}!Vhh}e5|33gFl`O4U<sa%Tln8V49Ka=Sx!? zkI|Faz_Hrf#q0`=BWJW0C!3XR8QmBH0Wc(^aNlwCQiU78Iqo@9R{CaO$G!U1{)J9w zN8#@uf95Y8=Z$Gg)YRhH6KC^K{vR@N5+onLmgXf_dQB4TQ<k{jK4WsIoh_~(BMI>! zl{&;1XPVh<?1YMsV$AwMozzKxH>8i$!X|E^-oA_}a3_yqH%DEgPmycC9nGStaV4*Z zPJdSDe2T8uDeIrvys=_5--GWf2#y>24yAYpR($q8#hEajnTmR1C}k6z%ZnQ>cYhnc z6*FuQp&f(L^Mnyo6s=jbHUw<>GX+)#^fCAMssZI>9;>!6)C2RLi~t!P_57EAq!|!z zkh8Ofl|hQBHk#A3wy-?CTJ3ndI(e%NZC=`b)_X^-5VK%~C+SLFuSef*kq<BeWYYa< zLqS+w&sp;);TbuPK+B97GxE-wSf`4f(691+esR0Y#b50EdGiW=r%wbEi+Cm|=<KXB zdkrGl-wUL@uQBZnNyFgjB1>~$xP&lX=_J9;H-<t3r99v@F?sr_d~qCouv9u6^cW2e z#mwrnsYYUn-F9&g%otiT`}Q*9$yo;Xt@^mr1Kmi~wy+;S^>#NVhL#R4*gz1^wQ5Oa zyYipc-!p`**aNYCM7(y@G8|PhZNmJHg-D~omWy<e(#<#S43Z+RA13%-T$Ll0X1KII z@X7S4LG&P=Zgrq%MA|~q%jwRW2R_&G6^IY$J>whlzpC~Ii$nw8Uq|iV+*OM7oZ1Z1 zmvgz6>on7++Lc=g5U~zh8OkJEz1Zi&0@U*I{ie5#PNK}i$uT|<uz<YeY_Kvvo@ra< zsjbI=L^R-FvT%VkKiB-A$3Cf15h#;epjv#V$<N*1Fk;P!mw&ucXH~YQcP;hisSv~G zj5;LgI3<dSyr!cEL0%)F8S6I9N<>xT50UJga*4{BMQ9NCm&fIjT57QW9o7Djfzk0- zO65&RG^SUf()(5UUH4S33~H5R$wecN;1h&%FaA5DS(+qleC)7C$7vRN_gjN=_V?Cz zi<7>Lt=CD&rKtsmtB|CfT&(hv0B&nH4m(g@pcq>q+`=$N2nTHnu<5lO760$~XCE2J z6}qubcsqDKc~^b^i~cEEIw#%Slsn{VrRi%ab3|eFKP>ZQ1Zag?rf(q_5FW8oKC?g| zca4e3!g3!`YKg`x8Dz<j%aN(`R#ne8Pe&bR5<S~A_b)Ac#PB$o?XzGlJA=pVI2JXt zoYP14;n3!6%at$bM|hg$hg6zfi&F0(9bzW_4LO8?7L4#M;DpCo@?Jiqk|a!cddaHa zO#OVez(!J|;f)9F#|C|xL}*O-z^=(0G{uAqiayM1r%d}8j-HOEHXMjrw6d}l7i%2g zuRqxT*gw8pRQYLSDP8p`x4}aCiDRYl+_>0D>(wz^iTiNa(PB~V%c*#Yl8DIElAGq; za=o;yj317#-@ZEM=eHNwT>iH75I^S<kq95Ab$G`QS;~Rrj@AY6IWg-T5)!FzIPu@} z4>DO^vnMK*om?OrkAjtSj?DH1;9u_*z`&SnV1X0?L+n`9_5jA*iPqgeOK9T$7!Vh5 z6lrrm32^5hjN^DF@ky$?DVZZ|`ms!EK<l{0k<?E)qS`UqaL+W18~VHrMrRyKbZn^U z5d>4!s^d74=4qlzS6W?)v`bna`$D;tYQy7v2FSNy@GKAC2Nwx4MRcF@bFo?={RwT6 za-Vcla`Lg6m6w*Gt|%&}Mklh{((%RTRdL~=>#v5(U&2D_wO^sUKgBlKN$YQ0V`r0c zus5N)S(dk3LR|Bb9HmbxEfGWKI*kOu)rrh{4i}^n?|g&~3z?ma-*{^k{da#!^&t#7 zMwc=j9~WPl%+rTh1{?4Z+>S)sFyU9DLGc6?hv&BU+Q<D6YG%e!7i!PTih-O*GcP|7 zu(d;%vOm96IFUDVEOem4a#OOhy&u#lLIcDiM@W<SS<gJDqhYWcSSorY`<>{%Du481 zik-=g7R86*<dI!uHIYo$r#>KSI7ob|C94}7V}zlmJ84m<HTo%!G1jh!P)b}?l{n~* zHyAA7uIbKT1`P|2M}|dWjmP5K0V7haaP}~4PDuD%ClOCnmuehrbp;MnnC!LvkZ^+F zp)O5|z|;bX0qGhOIZeW709F(av&3evMR?X;P0#m|)Sq%fV&s8v)9MXmSK(}}SvZY= zgwp^N!c!NJM?Ut5W?8F5lI(%cojD5O-ZW9a;j@n-R00LSJd^F6Xkej<j6{S}(qOVa zEEtbVi!_ftl0hQgHV6ryB5+`p1)ETy5K&)Vacg;?J@v`#kMRA&H&NT8N&(|tGa=Yk zQs>GTuw=dv8E@eyQQhgXd*o7~t!u?iPmAAEzMKx`^N1a%x^OIzBneRUWIR2PQnZ$b zZ8YwBY_^@Y1hiUNW|dVkR{>re&oXR*q^E<1Hyz7ftm5Uv+YC(h^j>q8jK{R%#}9X< zd&iX#3pAF6X!~d{8Cv6YR_Y^0$YA}vv!^;MEUR@v6k*+3x%Y80U(&whK)dcuSjeva z+q;F^e(j(I7v}_dKK`bd@i^C$pXo!p`tI-Uwj#FVhGv2+0cH_yt)K@!fwJz1JRxPW zntbHBimOSLC6>WxVTHmvF{TsyGvW6ITcsM~+}hjt*&kBz@q~gzj0VUjG%+{qD_X-D zXN=4G-^X_shM#z4ghrxP$juptrD}On)A3sp-J_JCIvWhx>EiG~>w+e*!e9}XFMpUK z8Xl=Z7dOuI#;b<&u{2@M)!FW2u6K4*t)U8}btX5DC@E6%dDy=jNuqZK$qBEDrDKIt zB1ep?F)P|WV(Uw`K^4S_lk|fNXSq*0<XoJu_w}tzUE(qxHwQC_bQ)Hv&`uwfaPV&} zDwpdS=T>LG@VN{ZWp^3*Myac0m(28=;A*4QMlQIZqn0&MPli6#IJ2d>IXW_fq2-XJ zQL)YaN@}_fKiSUJ+sypKfBr4+#e5JBfZnAwHv$G3*FSZKh!;0}3<)Ts$~0c4j?qfl zZ;OtshJ5dSeiQsE#kgRdqLt#%G9t>4v)){1W~S6$ky_JkudSWhe`$-7SGTZG(6T7* z)JEw>Imynr%vC4{+3Oe@Vdc#CFDdx6Ial=0^%pj_Y(0`zw+Hf&EBdhn$c~2G0X4AW z2?f-^JwUNnFdig*VTE8yzh-}C8y6@|#E$Kg<zHk>v>(l$qL(`XovlcEo&>L?D=zCz z1+5Pjs%gi;Zf;$H^@W{V@%T1ETl{(b4^C9R#!JGZFY?+BMOFDSKPFdN-kgjptGqZ; zQRt_4OD_Cw&zodVYebkAr{9dQOwo}X#Vud;FexlQ|0>RIwBpd_ku_5G5I-Lg(g+6L z`}v;I9yPmaSAC+QZ;Az!taCDiT;K6OGa%WyW)faLRxzL#dG3EUJxkC#Gd<_}M#p_O zbf!x3MePuks)CS+@^|g&?T^;lAH%v5OL+x>p!-t_8iE3mU@UC3-&%K1=3YiYk1Dho zX`VSg{p(jUYP4YwQ|)x>XD@)K%TKt6X60K?3BRPETMssbf~-E+6mjy_LM>-+pthE9 z)=PMjTQszb2^~&C_PXrN#u-LHx&`1!R@YT1P=UWV*fd=O3+i6)rSc~?lf+4*cyrQ< zQdifMzn%w;+$a-fm+NbdR5;R@lA-REjg$Z!(y(ZohcjUVV4ml$eLcit)nk%Kk^t0o zqsW?Qwk?bb+8HPFNJ+LA<3IhzkYNQvBra<Tuo*xCdcg+m_%VZ9OpmZf)KU};HlCLT z*1i9r(M#|qw6mHk)J%Jk=-O3(LKA;7gOHuWRvJ7pb8-3c?d>~{>tCsE?NXR6g>h}z z7ZP)C59sm528g-%L$b(ooy`OAD?;W?bnQQj<*A`QbY7FW86|7&MlH1`y|>psWS9HW zN}RT*h9REJz!iv1F~PJYnq)~3Ep@6tO>Q-+?*BM^zA*J8+QyvlU_N!65CcpJNtP2x zHXqXM`Z<>6*Hzl1XyUDxlYu3LO;%`SYf6xy>EA!af_aDm9b=+rC#}_ta2~+q=A~g= zP;n+GXw*H?SFY0BF|2s51?<usAG3i#7;%8mC~Wdu_y*E{r<)EPOOq4&5I;6rVnWU@ z@AU~qr#&H3c$BmzHX#`yfjO#?giJIx*<|AtbCMj>X?&bY0X)-wV`Ds<0Gc)_fE+Cg zE8twcu3HUa;%Dw35ZXkuZAGj>0x2_7NI{L_0KrDs@KpRK7{$pcEW!|2nQ5haB~-Q5 ztjG{O46^bk<PnZFfpwd4_kH3ur{uZUakS|KW%7kqrW>T3{D~(Y(GxNMvH92Dd1vjD zYj*bCmxbbp$v<CNSu3sYF6^`>;{SZxWhfYY;cMkqpBVhF^~2rf>&Eu$&nyMaX1uQ- z-|)q3-tqh1+x4pHjihAX-HE>YS9mvne5v2mY=3vw37{icTPwWPXNX!G68y!r+1<o( z_OvYO2;7}xWpdlS`o&YZ$nZaX3llOBk!!u;1$vpuj~cq5j$y2**{<7qBkKBQKswLz zEqP|R$}r0j@tG3<R>5{ZPtR-vs4GACw~H^bUP(Go<WO~_(#)r((8#Fj=NHq@{Z{pu zI=py{=I{|g-0q?FX9cP+oh)@VrR?TfCeGhq-wP7HhNPZtPEe-Bb2sQcBOxLYZVF%* zT~azfdQ!e0;aI0=GocJSB*$ma_es+d6zL8kdA{rLiK+gIS0aoWEGD-p_sbz&G?4k| zNI8|_g3ah7g~+FB9oolHnDj0)4H8en<||(ou1jTe<GZUb2;BPJe_dE5qV&$vyda)( zFhimrk9Z=EunBHq1GH>%5>-BKstj4->~tT>)8ve{73E4MV5NYkiMY>G)IG${Grsn- z#Fu)p07Zl!$&0;W+yP6!13_C3(gXY@<S3vYPe9M~TOwA?Fwsi^OBoOsq5GzLn~|E= zP=|wAyv#oD{jd#xmIOw1N);AK(|92{YZ*4F(6~4f)FHCTE`Fu?`rkYPaT1l>@JeHb zn`1kREkixEE-PMcnCLl)46|P4y~FIGLzIHTH~HyhM;V!YeOAP@jI7K~R^McBA{PCg zluXg%$4ssqmPi1(f?qeF&tj5}##W0rHJm?(jc1Gq|4}4AIP2hQlnYP;aU?+53iJxA zL`&uq=^I`N*^ppxw0;<u$iH;fWsM?tNRlBcZEIuMisGJG)IY!zR2qZu&(q9f3Ro&; z#-q*lQ1Z1fCKpziDk#&UHRr$jm#-lhfdt;?A8QH!8nQUc3U+0SCI8}X{9kQyV_a*J zBhN4o{1{-gt;w&o{QRN=n5ZlJj}m1!Q#pG+JpE>v)WmaIP6@seiq#YdohXaw>H-9P z6BvPR(6i`YxD7Omj^26RAErh(sJ~(=4q-znGFY|3T)^GjSM;3y{8Fhkp$W70r{Dkn z@mW9p=j)_f&AryA{<Jxm)KMJ2@5cb)mkfoevF3xH;g2hxGnv`fHl&5JGm)8I%2w|x zMMW?7q%XLZCo#GX723AV!N1*@-4ANqyuxbb=i8nA>cO%;uOstzkEx511!|khx@+#1 zVZo33^BGSlMogSY+e)$s$J|5O!ozNr+=%NFg%as}YwxB7*%l7pk~g8h#DDd7I$2G= z>ZfT#K#8zYMG7}Ek#7*MqJ;{^O-tzCrl<|g26>m4n9AyZD;Lr@3ftPqqO{dVEwP8d zEX|eAQ?1ZBe!GiQO9)6|Jy5}}&z3G}|G=XNS_kC%CL^6=drkCnr^>*I^|eh5ROV@$ ztrbc8rADj9j|%U8CD#V4as5TNl%x2TPxuUc>+>I8RA_@ezNu(>l}(L4v#Z2&`oT1q z^|kzhQ+xC`zBFBJDB&I3{u=Z3n#kvq@|&U)ovmp{!%}P929naUv-pn{MZL9V4R>}& zt^0~*mEpHZdId%E5$TNvuaaMNn%@lU)z&8;ZD++ibR*sUGn%`xiZ09m*>wR>iXe-2 z;cZj^#$%92?w+RUBhu1=m=ef;{x%fPDVfT+S7L!~AXR87&W!M)GLtE5NDwC6Vs{`e zcA63}MFP#-@V6L_dqEEzrY+cPeT)guVKwJ9CNM9K8t(ZvjtVxZa>&#UY8E6xUnobO z3Lv_kV`$;RGo_dC{e22hp~sm*>~I{Un<Dera2}4DAYA~BJg?VAAb6RNpAL<e;~Xuw zhYR<`l8cB{8^d4vP8RQOIZ#`lC0#KweD(QmZYB9he}Zn_h`cI-<#3>2U{q|ta#sCP zu4}m{P%Yt^uuax8E`C)BtY2SjKNTQmB|NSmAM!_(Y?<QHSp@#PF#~`*7s`<e-M&)g z05nhAX3mrIXkk;oJo%R6R@z@-L)8bYTm1W>XpOD0ND<ve<3-M2djbl=|M^RZ2sN<f z?##sPzR-Do9=s+>TrYVX3<&fn=ku$r<^^&q{`FQFzE~g<BhzI)jGG(5ASeCkJfw?D zx|XD+&2(LAkbtqz_pgGHb0~pLhnTZZO>4c@w#VU2SaW2WJL0RIAr_K|sWsPC%q}LP z`RMN8_48I@Dy<i;zT%^cEDOtdKNup3JPE@<g1HhdHWG76ctLG$v$q*1qMKa$WYzyZ zZ0%ZRu-%oKg@<7GnbXX){@Y!fPj~H?>v)X6>YZX68SeJDy^^wz^x3;ry{z9@W8yPg z$=)y3Q{*gqc~GgKHr!BftFX|xlE3a<0YhU)Xohd8(-u>`b69o!zmibRABy*eQkv<; zGd?z(grc<fE8SIMp{4dkNlj?U*h7AHVbVkld-N#$t1SMLcKI=!iZ~5Pszyr>7qjrZ z^}XwV{qin_#a++Wh1L=Wj~G8ZL<@SuV<IsmOk<w8c~NNPNd#|!DRc|*1+H0}o05(B z%klr6Qj@lEwT%gv)xw7WgQ;**b12PJ-`0?mK&@osOqnb!`mkS!bm3pQNyEv#EL(Q` za87frTFCl!o%7RLdPtdc<D!^e<E2^B@df0pfmZM5J`<rB<kZ6eL4J{sNLA(H_cdg6 zZ0cSv^J%ooIJ8zpgcmOy`CJS0{ObMIYS2JI<ClJvlqIw~one@GP=lbLE$mUN-FY+` zM~v4GtRZJ2#f=~?yq8aqp2x>%3x8SKTo7}O_~8CsQKUrG2W14DnuB1}&|v`|_<WOa zlQjhGMX#CYYswMEa0uQ{DScafI}EIWnCOZ@L-07B&{%@eKr{`xA6m29P!Lr^f|^^x zi7Xfj?duZ`2|VtOB#R{UujmdXfifdGdR6(yaQt+I*~egjf-Xbs$PNh#H6}mjjqM^7 zn#O<y3lpyH8l6c!uzUFLljP7qG31+*B^(3EK47Bf$HTzMpr>NcCXNZL_UpDZ^Xo~I zGgQ+h0z&RpKN#ITikc(yajygq>e)P2{AGklVA0ANw6^%7>7(f=Y*}m>DJ%I<U0A*t z8^RBs1k*eO<|-(~SC*xHHNbTa0`ie3-ItED7w-m6cuQ5plNa79oDbA()p35SD+{d> zzt2mob<+3cOjR5xx_8{5K=Oglg;WND1@I@nvsVq)B>#SeZSZaeFIJ%6;qFQyuas4~ z3=7SRW+8%8fw28y@o7f1ths=iR0hAASUEUx;Kok}IH^C(gcF8_pdk^7M1XA&5)8$V z0y<OhkO7c9oTME@z}zk%je#9LP4^fE99TDZ@CapcNY)ibNP{6WH1K5WL|$2nOno6r zk&?c^C^nrr)3i+f5YzBWkaD4jM(tW^vU(Mc>~2z`Ss0#8yj$37?-=4LP#Xrr%Q=;i z+h__3-$n+zw-_iwwWxpQy&vOOFvT9n5GI`X`gC?ut25y4rG{;fw(I>n4;Aguf324? zIKFvA;8MZY^$xW7$$EXm%wCv{Mqht{Ykl+A>!vpETjO8-wiMdg#1jvEuB9V+{{oG@ zfNoLNs)8tE?#a)7smXsk8Lr+&Zu3pf&cgt%W%zzAfaj6aXx#zbGzc0KHKv0B7-Uh8 z3exjghX0PJNb3Rg@{B)9khZ9P#!n3=q6B`69msr%qUg5h7Qj$yW~(_?c!>`nv=t<P zLV=E8FS1A=a;~QXwmA!-uM_fe$FRy@;9Ue>O4qI>56qo)rvlMro=^aQrQ3@EN&}4T zhcb*2cX#|Bw%#(RjW_E5PD1eDt_|)qXo1q=F2#$M;98s_Z6Ub3TT8LxPO(yg6nEDG z#fw9+mXas@?)l$yzj*e=COfmUGyB=Id!6fC=X_63F3gm4wBm+QMpGCBm8Mk4m&PRm z%2iuz5!v!tE?G*Dml$EZvpB?Jr-GkU){nQhwNYECD`St|+1p|v-cbX?LHX=(d0bmO zs6RSa9lY+Ni(@G;Ja;Lh1RnX3&^0--x`{=c2Fn0XgC{It|M9Uqlz;(0oC1_U0_z;) z==mZ>LW8nL$1yq)_j1TDnybvFl20zp(kqVTO|h$_Mqc9$ZdbsfD>^v)7d=jX9F~&2 zzbYjD_guRr;8An2orc#w3<Hn|!)|PPqU#0)VIWBJ5~y*3Nx=YeYN4RLT9Sw1^}p@D z(=bp9BK2|PdTsam`Ng*|EbMh1Ea2q{&z;lqakKxwLCb4=0I?gG8i?RRh+#$mXqysg zcwqyTbalCPtPY{$S=2H)_0;fBpP`vdoKHCuCsv<9NZfj-M!+{%sj_dMaI;~85M)`g zvFa^CjrLRUp6AJ<U=CI}8l(gzG>&Y9G!4R;Jp%|WREfsILl&F`cd=7|;xQ6k^Z<Zx z%$^56F8W2FSnQm2Q21~zJu|Eu-VN;9fFj?K^Dskz7>J3ixSn5!kyxsf*Z>)6Phc?q zK)x+5Vfv{N9*lyJu>=okN8w7_3+i07)RP!*uOriZGcvWzqiL?CyB_hR)m~Ta32#m( zO@Kw#qajPLR$=Qu?^(}F{|$sYyfxo9BCHE{(lw|&>aN*{%YABljPCS39C(A8J2^?` zZ20^!V?sxos6$B3A|}Mh?D*;U`yoD>Ad4)pIjXJoici(5DJxe$UH@Hi1>|5LHy{uV zU3}TyS5);Q|HU?O#!{W#|2zlAX$?6dUfa7bov!o7hS?RTxB!4^#(iOSPhY7JT(FTQ zDy-g8g|1thxFChtdOdj}?Z!*UO#t|xz81GqkwjTw_5>j1B`&pkKB6eqo+JYFK~;zm z$J*bFz5W|LhM+VfCO1Z6Sz)4;9y-Dt`ZX{Xs?aOH<>l3Cq9WhQd6ipwDh<fz!jUlW z`l13Brc$`D<f<&0)DI5Gvrk#WTzGGtOxe9FZ^he&mlt4toce}Grtf>ei|hP$uN$e! z0V>=D^*ixJjhw48vkH9|&FjoS)O~B&-;b;4TRxxbMBnB0TK~J&+{*x@Po8q<p^J}^ zYlhPX)+~a>xv6480o^qDZ_lsQSUEvQ=aw7ukmCdT*1Pd{X#G;u-BaF>U1zll&%iNP za}?^gsLR^Wt`z!;=&ik`9Do3@EpRD=;IO1&jvyIew<8vqDC=>z<s@h7%G87XFv{t1 zJ9p;h1CcBwxC9^#7!y6Rq(?0W7ZK+Umi{D7-IK3{Ln{l#;|w4vg-8<6V__iR0$CV@ zoY=7ppL?M3h}zTM)82J(DgU})38L;)I9?72um?rH`&1!M#T<-<frDcYlwCKrr*6b3 zvcepD9`38T6;u&!O7Urf6VjFMFFQ`ft(;EsMqs8VUs}2i(PeEx3^M|H><1Buy=N<E zG*U6NCBT!{DwPWZ$m1QjE_I?ZY0)u|!4Hp<+@+n$95SMk_i7+zy+11axjAA+Tr{_| za(ND$H59tLz>=CG+~%kZ>p=>%&zzU1z6afdx)c%2>~n)H5m+P)3^$Px{ZSJb;F7(( zTrXDlKfsQKa5c=V5jS|E%mW`58BY&uw%iF|-pV>RZhXYLG<OyOV4PJq^!HdFY%|sn zb7h|EFp7Xl63^?G1;og|VOB&A#K|DGbu6qrfAEge`I{P7;L8AuCEl!j={`9u2oRi} zTE&J{7+;<1F-*6R$^5!YZ992^l5(HVDb?WY$m!N*?T@jFX*HXAe%xkYn#mI#qiTMR zbcW-1v(EK9MIPb<p}qg^#fUb=To(I0ssj0Ke3B^Bz6D&P#!z<_cYo9cyTxO0Ub;5o zzhVK2&7c3cT+U$HbJSH!c=|tvfkHi7`+F(A5=ggTrLxOc7+eQ0UC}@d0Wz&(gEF2K zP>X<X6MUaV0!@2~wMO3!oOVkxKo~-pOdvRb5qhj(VqVx&&?XT2zz3!{BS?+$0SQFF zUIN{4@W=q?3ZLzXp!GP(>K3G63`Sz;;4~|ry0%HEyo@2!BRzrrCxmp^K!e^8YKR2! zCJ7S{1kUpXH^8K1-=XZ+(V^9xdzhl7`&(g`%XuG0Bzj5EMkLR?S?BlRjGbS<5&qE< zecoc}tjdvb-%3e_M5~?W<K`9@|LW_uo2g+dk%s=i!+-m4(<MGcFkU*V_TAppfC3M4 z{)$|_;2ZiD=ybY^y7slX_KO$0FzfgK-24W`vz>6za@~1rb5;1Hy~<T;$M4{1v+9EI z{uYIbIJr1Jzw@!e&mCvHW}Imn`rGW8f#43tg+p*KK|z3Zg!!cbUV@OtNT8<yo(K;i z<p%(#l4})jt55!o(gPn3rBDg8j_d&dsWX8KZK)Yn7ExrPK#I^rYuGDbjG60?#nS2R zI~OXa0|F%uranLuPK2}t4pfN%*Dbf^N({uLL2D!VD)G0qN&XOaf&c`ZqDwC&H7(q7 zr)F{glR};NOv_E1D=Z{pEa1_fjxZ^kKbyC<k49zaye2-qU-@0EugEa!iiaK!QTY)Z z$FDMG?p`Yj)gGK(y(Iq3|Hv2a0J`~qMv)z@y8l-<{b=J1U9`bPP9nX))0js@X6C)v zo_|Hfl#X8ep!<L4qJRJD^m~ub?|uwKW$(KEd@im|u;#WXWihft?-LA29by(XDBGUN zg{+HVL=GSB2=zXfBquZu^~0cvfQ*uf*79!sr_V+&B`=YdvE&Q*4aUP4J=OU%n_;Z= z-IM!6aX!{lxud-`p%i<K>EL5e?&;bO;aOPfxda=sjLGXNQTD*D*P77OvLLvza+m){ zODF#}l`Myg)VjxUy;l2~Y%lvpRG*BSy^|l?HhzTjhE6Xe*D2jTXil!z%~*Gon`%fg z^4G-~d%_6b(9c~^izmF&F5v?2%b&Q54$I4wUvo1&ueVN10v+F*qpn|kJ@LBFPS{Gz z3pUjM$SDgOwtHnH`)mA>f7A2#-bcB1kLXrkpAj*Fe-{f~yoi3g>6@?}`q{I6T#&KG zzgnjvDfYa*<U`!!L{7lkF6kT&f<M_Pi&mD9Fakg}M9PAnDXBNMcBMuLlo>AItJp+f zh6XyHARp{UP^?k#9biZ6UvS^y!B3o7lP_E<s;=ejjF@1?SO$(rZqIkvQF%(Af^^wU zOLJkD6><%buG(%gbDzB7WI~)*3uzt5A^Yjl8jUWYiz~h!SMQY)v9I(8qP$%^=Z|Sp z8#3EdbpGu}zvWPlW(3Rqo2{nZ$x$&%Kv#$+OxA~`(bTl=XWv$*dFvb2YhWWF%G~(a zNNy7BPXncOW1~;dLlb(t)>?h_<LWZelHw&O)ZR)thk}HcHWuhhw{0r#odc(u4QRT` zw2j?BHlF+y_i-!JIeco@Iul+N_}rl6D1N1?{L3=_ioAok;x96v@!94g&e8HrD)%%K zs_#jr<<@SiRSaW%1uf+p6%^%`r>4U+V{k(4_}i)nK9-_~v}xAE!_S;uc=>q9A_udU z3FY$h3KwW7{z{G2!yd=RkOC&?4!@t#aG%qVP&QUZ(T7wQTy|@}8h0gBN@A*EAS9yO zETGZ4DiUO?Ac*_zYCIF|nTM2-t0&#cD2x<hc{3%~mWMNB;l)B6IxW4)8G=o%u@G$M z+1OryO_DB%Da~W4ER9%o=E2CH*rI{eSx8+nt9Wwd6=sYZl&rvm1=grJa||hUt%^pd z|Ne+7!k}J;kI>>RIlibewJ{;vh@x-xOfr)Y@jw@#RqXU~w(_(~&b=*o$FE{HiiJ<+ zuT-d4s%9d`f@fj8^C9?5erBg?I+O}?X<LCL8ZWDh{DpxwYhwtFV$?veCYK@YJf(-< z3GIpL4B-QxtfyZk($$ea7Cwz*@RPLX%*RE1Wf!gbKCEV9ISj@U-5y<<&o$r!#*e;W z6kl}G);Zm^_E1*vaC3H#tu(!{H;wFA9E>yVE_L=a{rvQXPEVv$3aaEk{*q2qpi+Dc zmwF|+Y_h&=Sf!5#qBYm6uX4o&88&`r<{{ieFh2tQTSB@Na=PBNc714JWJmy!R|Es> zHL%X6v}HS&Uz?EstqDjg7;AQJJ32An&8o3$5UMVDepvy(VX@^gy=wnD`YW_m;DTL1 zNp+OYEsu`oI{JLLrYu?e*Doc!*$=;Gvlqh_ripXA4Db&1Rj0Db42OSm2wS(bdGt_4 z5jbpYqSaCgC-=MpYHQaj<95<N^WfBWi=}*o*4E~V|5yLjf!6;)AjhbcSu;NfzOs`4 zxvd_lD67xL<e-zOxmafWy_8wp*!q`T$=JW2w7_I)2189?Bsg4-c0NPwbY`n}(>4~u zD+Wun{!-$akoB6{0kBFON()c)OOkPrA(V51f&q0N*!+}}F{S<6wcX{IAQ}9(ifM4- z5t5vWTP|5+E-Z;wW4A~+d5B;*CY+ii5+uMn0{*n!Y_cIue>Dq;1AN^tiguDEW&%S@ z-VJD_56*@BxmVlvXb(~ATwa(7${CHbJ9d)JQN<a6H&@KiNimN*$|b~_+y!=K|227g zpFPGgyRN}Xbu^Osrj9}iw~DpYI{WO=an{`RbCYH2*|Y5F(t`C+9u@ds7EI)U*ZVB& zf1miReMp@j>}OxDXDNVt=T(PTXUXGi<3C}~J<r}N|D9W0y=NQ!Br|{Y!`t^op+@SW zfxVtp&ENmc_(f+#hhk`v_stx9wN43V$C4!`BoSa{F;RF{><Qyx##I->q=F$?Roz@! z_i(uWG(B>%*^;rB+nbyKx0?<f5<ForadPjncf{)=3*(#A^CE_dk&^?l6v%)eGuM<a zU+LQ9n_td4iv7X>$S!%7ujblR7HhD@)))z?6xC1^x6>{<9TB~Fw=BB6y!<lXI2<IK z<j$v9=`XCv$(GjnwYBA$2c!6!kU0I;Xj9v3qc|(4%&BQCy~w5tTWmYKb|<c^b}?NQ zPGyJTP?FPq0e$FQJ3lj!*js}n44txDh`N6o_49$x#xr?|FxzF4+LC9!XUSI+^Vicp zxpfA!YD-Th7XB}d|49-5zk3ih;p7mlySrw@k^tI?mLtQ$fN;hOBocSCY_dnBWFncg zf=O$8wUSMc>w@qmho@I2sT;n<c-9kH>TKOk<M<c9D+VQsUGRAUK_Q6o`e*F56f*vJ zR-^j`PgQj@^Ems94GSb<>TwC&U1XzhEp&PGYbdlVe@$3Td&Sj6EPQC|bv&T`{$-gY zhl@A@TjvjLRdKz8LOf-YzD|6WS<S(&g8^+Pb7^;hbYzk~l#a@ey|KLamGaMOnKIjL z49lKLZDf%=7INXtxF;XUA7rPd?@+Q)?U!)dXZE_BUU~C>_y|kDDK{sza+7r7=}up4 z;d{?03aSh?TP~g0DdBP;7$b}nNQQNq3ab`vu*R|y4#v^Nb;p9O&|~nD@(YN8r?HV` zFdd)}FfkK0PbddI#Z*G#5K(i52q2Ik5TXT14~AeN-NdjH@4g9SVBY>j_Wl@%H7Fb3 z6aahkY}-*z`MShTP=|X>Bx&WpViv%aOs&o_-1r{!+=3<5QPJm#G2B3+ikgldt`Iu5 z%t-JRJ7=tppDR{HxPm*?=%(7BBk#lU0=@p*5d!VDk$8L<i&QU8T<glywY9gw*TzM0 z=PG17C;yK4*1ngV->-?z7(KVX@heSV;S|4$Gd_N8Nw(Zwb(t$&?8zwdtmp#+{i5eV zNB!V+mgfT>lzb{blmZ?9QP!?Gik=rny?SdrH$Qt#ME$jpJ5hNA0vM5?gQ$a|`%XC# zl~|+^>{t<sfKVlUSojLRB1tg!T~DwUGC!Y1030)DVB&2R5+{a0_V#NKs)*@X0F$Du zot3>~RTU!%imkI2RsgZDTbJE8ihBJ(J$;FK7C!-GDIAZ4Pjw$t3wdD>V4(-d0bnw( z_X~{BCt1Nk*6Lc4Mq&fefUZJx8DeBL4jY(Q^k`LWRWWgz1q-Tf@jd@%0jm`=j(|rB z4mRZ@b|M%+=XdD^o3Rkl_wtuVZ@3jFZv+ydZasaL?3S_?1ASuDvh2L1yN|>FbqZ?L zB?&9N3<hvoat^OHANQi{gpQzYNfAKSIslC{t@nR?>?$R+9`vK~Nm?rnwgyRlYXJ;t zu^2#50vC5fU_{eyz`tev%Hd-j%VVSFFt%t-swzNCl5^WH6=8;#gzvqib?ojBnJ**d zUcG{>Qt6k{6H<dy%7|Gh=eCLak2~5@)1RhAnMy5F9PNI5y_=U&@;;p_#44j>RDgU& z#7U7ND7deRThWN<n^S$z?3>xvM@y{5V#cH&qIfbJlJnFS3u<=vqr_^w0Z^47qE5j% z8wl^%_a_)SO$J#k*mp@S9#}e+TyEwx)`u8^WeuOR)%srv(U1q}(C3?k<M`1~#bv2U zf8A#e=FHGA{Z4K=@HJ)X{iKcxyW7{+wnqh{W=;CVmor{%Nxq3}gZd&_!zJ;BWaKu} zQT0kQ4}3hiozBzpmh6GiJj#R1za3RLP*sf`tdvFM!If#6ZL0oD8Gocu`+WAS35)jm zm#yV9#kn?53qA{`Oc`k2whjINe4_A(!<~Uxao`c9r_V0IUI5NSQMuaPlT8=KZov{I zQh<`JDMkYnCz4b}Fc>_{jD$gYK@ALK>W$h0ZiISc`k4@|fuP_>E?*DTCNlP-$%djY zRiJYfiC+rI#+(I(tR@2d*;!=0IN$Wy<C6by7MLkrHkU23uxt$qvV2k_o*b3*npN0& zOqWucnW3Sc<(xb@6NCO{M32N}p#C&nX6CEMJLr@C;!RSvQRb?kp^EA21%LBhbW}s3 zw0&b}C*|7)SzqygNmhH{Gx&t%TuaC_5};d@(kfq`Y)_>nYQp;KF$wjYpD_s#a?dP( zwMY_6GpRh?*c&U{1)VFy)+o*7y|{fk3R7*IPV!ObqWnG1+Za3CH`32bHxh$U+6Tyr z74BQ_MKTe&IdDc3g9!Y6U^q#{gn(?37W+wzBKpT-2<%80o{8wDq{~m4*unKg8Cbk= zeoac{UQ_#!E^T@gNPjU*`VavF3^M)l$0dKARGp=U*p`Wvfh=dM^f6$)`m3wA$r`|N z{4`F0qJ}QqY5cMG`Vw~s?Lj!x&Jn2A_K(3l>m0|)`fthOdaKrxkaBqpt&;465X7mZ zcg6^k7$e9me&Ew}A8t?FmWk%}(=f5FBGQ5%xS+|Sh|jYAqVaMZ|Gi)K>f(p^AJzfk zjJt2eLcX17l-+`{0sJ8tAP|sf^R$}fl+7>00LwY~>gx)z0`dD)Jd6OfmJT-V9~YnV zCxEbsFb(&|GcELxF8Y`gYza+4guK~~_lwv0Z6^oktF$ZSufMz(DCsl8kk)AlReY2t zb=(;Iv}O8x?(oe3v@b((In!Ade#(vg8HCB!ZGn#L8N>5sf)FZhs<<#iia?$%a-~!B z)$!Otvq%6ntUOgFC>RF<z`zE?2w;o=tf(p>FfBx!1?Th^$bc3mOyK7@Gg%yDCrj9t zU|<gsJ0drd?aM$4#1)#@L@O&(G2=A&QNZ%$Z4H3%0b5*bQBcG%5Ko2Fg*6*QUOkhR zJ&+eMC=R!|ABlb7<7QwgVZ)HU01_kb>sBjOmuzCK%G;@fsQr3Y<$YV|Gk;-j7d0`s z?(d^1HcB4tC0|B9wq;BBW_No=i|mX`*%$U*cG3_64Oy+>U3uQV?CU<&ARZ8qHU6HC z%##pc>2@%7uTXfr()7!R`+1Vj6n*yx6c^W%eykDd1G~1C0R6g|-RIwjl-*mX&gd!b zu%;JSWwT{;sO#n*Z}SH2m><?{<%XWP-DN*28oR`p`gk$}nEE7`Wm|U?otj?T@p%gT zqn)}tMJHx_)lmhL{s~>WUH=Ema3g<n1zldKs^5z9sGW?)?XM%Ue+++`i$%pf%VDAT zTv;DcuDtFVrf+|dVTm~(Q~q>)j^)S6Oyq%KAOwa61KA1uzvMsg;Ziyjoiopx1nHnf zP*lPbDe=KVLG<lRJ>7z!pxzCompCwxpn#h+rjbiMN*7*A<rm4e1=|!FAaO3Wp(o(# zTFZ2wt!t2@?l~!*9b0vv{WBG=+lF2lZSQn4>cBmK$TGy}DDD%<=|tI--Fr+U?i5(7 zO<j%pKl&TZC+ll;s4H&#g1A^vzL6rhN1<99_9!VL&{2B`w~%4cJu3I#eck*?Qw{3z zx44|l`@re|3chJ8DXL<dX09@kn)ciCK9POartOuAGEHB{<ookASF;+rCd(3rzYAKu z9DWhvmELhidRvYdflQ=Bskt>g!)-gAd>%V*nxsr{z3MNsic3w`@wE81`oKLAa@Y`w zugChaUpc>gzZYnF;6pDpt%?9HPXOdH{50k+Rn>8vr{id4^D?DdQ`tVi-7}M^^Wn%s zq`aiE8ZcR<B7@CV>KbOH#cpm)5Q7r?y>@TIdha7K{=?jdN;lJ^n^uMOm$lJW>rs~4 zGn+T(C0^N*o#)fWiB>m)Wj2mZ8y5DA3%cIm5<sX6i{qpo0UO_*il5$3|LMkM-Vu>B zwfCrd)OKayWz_-7&(+sTMMR}EbX%0Rm8Gcj_nBXe$3bfH?U3=*VDs7TJA-!BPv$2# zoU~4LJAVWxv%+|Gxs#^k^f`*t1y9z-hPzSLP6Gn|Ow!pJ!Jlc#JoI<mv7LMP2{U_} z3Axe$G@Y$~IXKF_+u@VCA@PJnubJfmSa`|u#AgcKLEQ@qMGt%)%cJ}20e(C(3t-sV z5d@Ku6Oj@`7<ZS`0>wjPpu*qm4T!ngy^Rm}`wm6R&*NR7>*9CE<t2@yN<eN>_P4^< z{4mBE{z3s((T-FXy3kJ3t%8>#uJ6;Ie-ZKMbxc>YSV2b+D)%+*3T(uQ8cs|2SAw^+ z3wHTy0`gY-O4_+kPR#x}p-@IPoi%R*+n!wM8r`^4-FY@7MN2d`%#0oJ@rejE8JoSq zupshkowoyXQ1UE=<G2yilX5w}scPn&_`*D5e1{h3h_;KZ)=!=fVLMV?MkSnF-ZwZN zkS;l{PhO0c92l>m-ZnG^OB{(>_#^hird~neGBK|*-J>JK)iSNOQ@kG!Sz+j8&6M&B zKT@X^6Lu#m9DbVezy~JylpRs<gV7(Esja@BccCj6qu?MsQT<Z+EJ^a3R#|5W@q*%I zk-!u7s)W?_VGgc7<r5~E(OfH&*>>uZ4AQ?>cblte$NK%X*4eCV+Z67Z4aEJnws0%! zQTJch0S&H!;wW=e?k4Kw%#`Pm(zIE?eT{kBE=t{?U6J>>rzz>hKXTI|3way4#f;pv z#uzM4pS)|o7D4{Pe65UAN4(Gv%Ca&5n{uN)Ygz<c{*zV^TpJFQpEiugkfe$SSaZ=Y z85X&yo85Kfg@xP1|MSIt*Qxy?O6}*Hi>&D7q9PYAf)oGdhM#aZVuT(&IVIvdH1T*! z7WTc2EZg)1|8WJKEq7d*{O0vf+6E2kR>^FhOC1e*vr2<oC6Na{{-T@RaW2awrRU4F ztQgYj8z_XdR?%INBrZFA!uZXLK#4#PyB%9wJJfC5(_&ux{3pEvzD!K4q0uVs0br(B zBk2WF@@5MG;5xdGA|pM9nib}F<15r{w8~qrjE<e{ze?6$^M?k@o4W9%Be8Ge*|b@- zt0BVXlObRVSjhA-4xb2J_g0&#fE7+mW`$jQtly>Od^1UOAD7T7-^&ArVcS<NK3v{x zih5U1)x+Kk$aQIfuV&@XUL`Ued%xsS10d-$??(KJir6F_&7?Gs)#_%Wd5m<OQaxK% zD`GECj-0e|9c(wYUU*#6xxD$YKd4=|u0()OX{jc4J%5S%`~2prslM0r)6W!kG9?EG zv3>fyTrQ1~s2^J~|MmYu>jUsdS^l8>*Tn_@{Sr(%kfOM)HXNtTM3(kRKj+b-tIDOu zK9^@Nd`QP5ZQeb%{7JxLoyyxeqkkIIKls1S;X|EeKhG2s$;OyOoMc%&Yjk!_)@?p* zp;Zs7PSa}-?*N0UQ8g~^=DM8@=1irqebSVfH_Q{u?OYx|A~enei;3|Gy{0KluNZx; zJt((H!c&F6l>RiE^WVn=sl#|1iYH9_*11A-tZQFTgyF{EC4El*{pII@o3(A2Z(V-1 z9=E;2LVeHU@m`UnREXCr{n~{7VaGIl>mMdg%X)v%5y)&+)cxB9Bf>V4@CgUip1Oh> za<@TRtqn{0uBqsV#R6^Ju2L_U9K)!-o_nQnZGGNh<;3#``<W7VCVdB3o{XD~%Zzr_ ze3=;|()VoXMVv4d#~Jsy=O`G-Qke5imTI$`7zRd$lZn1~vGxKdApt~|N2Y)w+bstI zB?nERgLB}-VqF9x9E?HDX$nW;606HXU@gD(^sJ;2IDjOL#i-h_iOfm^EI*^S5R1u{ zFbL|kQEZR<;xmVvkPiMDnxZsh!j$Zc0uHn#9?VX~Tf>^Jc>lX|Gbz(0poB!l)HOSP zEqrgAs351&a!arm#)^acYY&2;)QlBgRY1%TN`ThDzy@3R{7VJ@G>=^=BPVn(UmuS( zG@ScCR(c#DT$-Z9-!e3?#w@lNC_fls`h*BhD}@IVhk$m{B?k1AJy~Q;egtvObQqXx z9=thlS$yDwQfS0;2JBFY7bU2aEx7D``QGEWv~74bBBy>V?!I-%{$L}7di7#nEq%ZB zFp?CZfKn|mjwROq=jX@*@z)<Hr7atAzv6-bwQ!~hZor8Dnt&L=4h$w9j~Sw5`HTRp z6r^qq0P-i!Cl9BlDx{>J(C7$N`^>HV_35l>4k7RC*yenT>S*bP?U0W}o=RKjeG!K; zK(c%fb(RmK5-wG4Tim;cl2>_@9Z6T$vp9*=7bx}&fhnr0j>)h(c4&SO@JDM!363K( zjSfwpztM2%s(mdQ${Knx>=DZR0@cIQ`d&SoQGSp-A-)?|4)!}rY!TO@Wjz_hQp>=J zE3-!uMsg<wt3BsI>#A_P>oJJ=*;y_1C#&s0{mg6GR6#fB&Pzt|yQX=iJieHS6eDcn zx6rp7IZNE?W!@{w7XZ+vl#v`8#wQ%^BqXt6U%}pG*w-Ea$>Wp_x)J7=iVYaJ(orDV zU>PAEyiHQ13>*w$0fu;Ds}}kso@()DJ7;x)m}z6ndsW$^;@7-WRCM&xt7lUsZ=KvZ z#y-1m9$s5j88nk;=uCa(bR4lZTp^})!4}In3)eF)(K%A!Z)WX^8^6k4=JPm>;?Bxd zb!zm+2o4U)&yS_3zO4$C!w~_cg++<;sEB1dxl$l9yl(>}{;aLdqamR#0!7?kJIq%7 z*kRr?MV5Yy$nT915(Vz9;Qlt@tPYNw7x5p8pPcS0n@T18!tgq*x}4$OA9a$MZeI6$ z{=t6k<wGUx0mm=t-<e<A;Huj8afIyilJ<A|26!<D1+&zg!^5O8=t(e4i*?mwQZTy! z0I<c)8~X~d?NYA2Z>~k6Vz`W(a$k?+pk?ECw<ti_4>(-fI$F}<)(}3x2*HYdNi>Qf z4;d7Ld?F%KV9vYYD;bq))iWI~zbiKj3I4edQK)lxn)a$nh^C+6O_iwO;@!8(6J~A4 z=91NUm3t4kkp_!##HTW!*Pi?J*D5t~FAil=Fhc||)iUw0?Xg&F>umP*WjBIAPvBze z7z;drU$}4p1ebV6g^i2SI5`mYcd=<l;pc^OqY<;Re}bM`@sxX#x`hgJT^J|PdTx~R zl?revh&eP{HAAJOg1^8qr65_KkKBPd;o!geL6pLlgdX0p33YW`>NToEVONBQjCd=S zL^zF9>uXe$g&#Lds4yT1p$T8d!7?v1wYjJ1gTguC@{khnIxBn*T!MidgZo3efY0HQ zAWNtaC1wwZm=;D1Jj20=0LGZh)djWiQZpb(O(|EppyWu2yeI}Z``HJKk}hKcp?&%w zf$rpf+d>b?_CR{U_DlW%AJ5KZZ`5;7e9}sfVcPVXH)aWMe_WW~oX`2aJQki=*iAp2 z`cXdgVee;;<ZFQpajua=u15A5i^~rUN^~<W&nSX6qG`Op3iY5nH5R9<9(%T-eweMH z0*TiAd?0Vbz8+XrZF-8OIahBOY`2SWaqK!S990N={U#Tl(fyrv-V_n|bxn%F)%5tk z`maBVBh0+GuLgkk<}3UgZ3_#{y7cW%xbF!DH7dw2d4A2&4If$kU47-GF>B{|XKkp^ zK&;g(&CCkZd+&zRk{*&JPYtK9U0r{Q*Bg&TC%s8Zs9!>RYU{)fh__<FjmHBBZ-%fE z>xP(03jEd$rxGqr(Gvp`N6^xUO{RYa#8NYTOpl=`0cM+i^ixS8&8dJ(gpSIN3JCXw zszaUa^{8<yK_D5-!j~RG^qR1X3tSSQR6Gee03Z`v*_yz44(HJJAqFh*fVxUr-+;qF zg~dr`@Iul}YOPFG8I4afE^lXL>ZK@Ymd!Kj`-`12iCOfuQf)01zLA8-(PG4_mW;j+ z>#5?>>qbHgbfB9+8kjV~%~HJ8b*aemzy3sLwqGR-ANpHmlpW2$5C{jFcMu4sriqt6 zTxiw5B$Np&8(^J}!%E1JYd7jh(eHgywEgT^s}0BFnVm;JX5RZ=3{i*HZG<n|-Il0s zi*9&`iLYpHyZRIh?2VAF5B=1>!ToCghVNIe%i1-e*_)qdt?!#R7J^_o8LMe3zl55( zEqcqCT#jV}p>Za&;voQNP<4qVrHFGOoxk~x<~T!G?1hQG;wtX=A5;E5-lK2CTUbnD z^2K|1nGrxYcz-_YGyyF-YN&e=r!OR%-{Yl^Pe=n52o7#ORG0<@m5`D67CVi~Qa<C2 z>Q#-vJSp&BTuO0BZ5Vm>Y~GXIhq2hr1gks^C(1~3tXYeBi3aK{U8j(4-b0PfD9d~J z{y0cFF^3i|BLS>gzoZH+zpzJZ3Aem9s~6ojxO<-~nd;3g5@AV`@$|f<qs_pF;8{Xj zI=>G?P{(h#gH93J%*4L(ra$o^Arv!K_#$OKe4TIKp&mYrn}7Q|(LboWHT3Sf6ZP-E z1C;sL^)1RZg}$u)N(f&vNa21n{{souccLCd0Xee+jcsT=YhJQOY3+Jh+Qy^sb`d2{ z1_#OQ6sHv`LNHHGG;SRSJ)L$|Dwu*Ao)i-vl%P}Pl_6~i`BDIwoWN2Scfg3?D~2;- zWfi9z%=~4h4pSsHdaK%%0)A=P9ZUi!0O^H%pwUBez^zJ>2dl6^I@b1d4Or>|+5ANW zF>-h?Q2tP=*r&L2K_?D|vpC5IKGCYF==!)HiFYpDIA~uc0aNF9?;G9OfmhDjldOP` zdx#WEVon570rWIKQC$VxhZCh|kwtH*Kviaf>~+XU;it#T08A)My)pgViYVo_tWNi! z;N$>mhisH^*gE}1`J;As*v-<7i5R@^bf@*iPUieonhCq|-Rw;j@M3{b7iJScDXv`p zB=sj6%n6%0gHaTLb#`#ichxjn@y;96je#v%sEbukUq5z=BOlB~03#Ic(ozQHlfpH@ z;k@5V{a7pt`v5k#L2NssQq-q7mWlA%J!EX*7=84OC$Ux?7bplEVuC|ztBw(a0EPky zBdMUK?$81-JD7~jS6(ib6xqT8F&!tQ{No~*)nhA`$RP0~B#hdsO6!4-hyKC2G~ilT z*>8hn9i~eSen<RN{FAgWZ!e1gu-6tcAml4ut|xx}C`}s^H$H<AszpHrVUqUf{i`Ib z%M1vnsK5fsQ)_hxjUXs=)qn`LGrkyV-p0Jz%O)u3jK4X*aa6ptR9GWTgbOVGREqTt zz1md&`?OKL#(z^1fiiEK#M2l!xQEXRM%M!Nt(rt$$a!qNac?RcsAi~og_64D{?OB0 z|D)=}>*m58pD?s_|8e)Z?F8v}pIk9Y9^jxQdChT-WIJ`tU!f1%&EKxk&Y(#}eBIjg zn<^t0DxR_3uYvFS#-&h<ruxi_+R_C8o*3kS?59HgO{@>mV#LIPMv2E@43ZFC9BXzf zcu+}PP$;65w+miKfSCTzKjf25m2kt_F<1$%aVG{?QM2(p$4kc3s2N%+6{cXXif8^E z*G8PeKxV78yLd~|m&K_nJtzaqCy|eMZ`2kRhQP$xDO-o_9jbGR2|<WuvD~PpX&g%G zQj?{bR?3+wtr()j%dt3#rIVGRozY!fZtGqXGK%*f=st$CiVkA@)W%C5Z%LQ>Ou*Q) zi1K+iJ{*Wb-EO{H2{dZ$6R3F?hzdkqyPGf(ysVSQUr}Z^YTc==TxGVAjrCf-`Spmx z9D+hw47k>Av>!d8hqGdY$i*PC2(zsg4kX<^U?OwDeH*_4{g;q(dp->YL^x=qvvMHV zii8BmZ@vT(n$5W`Q=&^mEWqVvDAvzSdC?(3g(2idYp_R*LqNfd_h3IFa`X}itfjLF zO-~@cfVP12&bLpY(4VC4pNwk`&Z{B`V`>+Kk${&+0}ic?v@5A2^^1ghvQSDjn)q7q zdAqUbD>p5tWws!-yR{#g-*vj(^IUM>O&96873;fK4H=QULU4&hSxLcou?|>ZSxn(j z?Ui;Fi+wp+2=0`7(WgMvMb}bCtrOA4nBE}el{!PlQ;shusGH+o*&kKfmULX*G^T>3 zkMznlziW6)29mhzpDZ6|M$ftIe!Qk_zQ{~_qq8r11*ef8;kxpjFDR4K`ZCg_h8)&w zB{lXHx1N3_LNy;PZs&{yeKwT+o7of8Kh6Y4XielMG^T<#Ai}bsO+*|Huc9s&l`ylF z1{Ng{KZV+_tUQG!#S{O5&(kMf5?-KXl4wAtJ8m4Hxg+*74`yBswVmGlpT8<+_w9nq z&pt)?wN<F3*euE_C|Av3suWhetb9G^@L79~I@@%Kv}W+f7k&vd?LBuAUHZhJdiTRf zfE+hJp>i=2KbQk22pK@1Vve?pyMQ0_mptb6{TKcLm1{PF=MsZVGPVAkEI3~F>sxC( zkC#M0-Wz<%O3JG8HczWw>_qJx42IX?=vj0VbK?`UiQq6}E69O?`9pF*MbTdkN715V zLMyJ|&`dY+r~>-C$()#+3r=cjUQ#fLz&ZS<ZLU>>1ukF{t*>nkkkO*jQe^I<Z4@X) zu%y0tgzmUPy1z+!HdmGI$E~Dm9W$7n|2xt_LOlcT`*eh6Hi?x@!vi0;C%xS2943jt zLO~N9V{Y%cRofEun##L8BOv{xamx3f=+#Gg!-cPjeNpnJg^dwpOUW<MW04ta-g^p( zNt^m)6V_zqG7bfWt%0&E3Ssuhx^wGUnxreiH`d7lHg$fJ{6&BAR9K5r50g7p_E3|n zDpDqVF+J&@0ANfxiq3F#z)vQ`cSU_gT$$MCPepLo`$4+sZlTXj1szOb)gDNTnI&5# zO&VNL%lsBgEoyiRyCV={=@{)C41jYQ+6)GJY7ts$>Cxk;x={P`hG6JoBhREc`$)FF zBN5%iGS>PTpG{vaIfhBIN=PI)1Od=f7r;ww0EN5w>A$x&9dD<YuA)XIUTb(soNP9$ zygk^qK&7|1Y<>C_vSaeVr|1>Aggi;^9-uu<oIjwdOZaL{$uoANQVR0zaig2Gks$z1 z43t1l{$fa#h7x0%GPQ(aF@oen_Py#PcK?pw2i_oYrh@_YjpaxDQmA!lKSxXi#YdA2 z-f6VOz`B-wPU>$0RxDF+JOq~Nn)z545QYFtb%Mab?K;X|r>Z>4xr#ynX@b=W+sCY; zr<&@GWeTwucd)Gjqmhdnk8ty$w}+aKn}?pzj@hC6H>nY7A<x?sn2`*#r$)V*j35Mp z{2Tb~Hi651xlB#2vE&fvTC;+84Uiff%}MYMtz^YQ9ti-DbCF|bDS(pt+VGySchS?R zliHA~glYHvrU@IxCS{AF7!f8H6e=A;3K?u65e1eF<b+IM;s3_3=Ni@$zVCAv0MM>Y zPE5!fAsjc=7Whx)F^wDxRYg(GPrsuh$MOAh`RT5?qMt>Dra0jIlb@QLbTyQxV-DoX z-|G>JU?m=DItW^<;QoH=%e3H6@4?BneR9XPQrM@hpr{3CnwgTk>=}Pv_GrN7vJ*c^ zVzz*Dr{sFwWYq5&+Y2&-K(<+<-)JRx-KC}nKgrs=eH?XyI;tDwKpQIR><(5?k=HT0 zkZ#g2KJQtSu45N~T5cyCvH3BC;<rlsA-fEgiI;bp+|)=n>j*bIkeYqG8D}zW;4*wX zes#3mx>19^%1kel_ee9|{8>(?fa7IC(av=Qw_`kP%v^+Ge%3-217QDCg|85d8QTjL zJgw~NN|q6iUVpG3Itg_C55`J9pai5x6KhIH0fmUo2xIwV0TBpfK|S`9(ZrtoTCE9h zIScSIFuDY5q_jm@Obq&q1?O}(iIm$bDk|t2MkUT{CIq!yf1EFeN$3?!q41IY`xq18 zfS)!>%aW8{lh8f|d*&Cj5b-Aw`XJ-9ajNgNn!RVf9!kick9lXw=@$lWy?Tq9hQsO| zFt!Y=qoO4^%N)=K_{(}yC;hb=>W@mXj*_mJH+gY=R1ZGI$ap=z(0$QtPM|`dd3}fL zQ5CC}ms+tEX_<Ad`;@#A=OWWs>K9!SAm9i6hI^VIt!3Bu&@2O{nwm^Mn#MA`$2kh( zso;>Xu!sodp3rPrbn|WasV4zR5Vfx-r2*+&CcM=a5-aw=M^3y_&>FBzRNMy{6+0Xw z9B~t6{4)Rfs0%Xb_@zbA+nmxmW%v*4o}xh|26LQ9v8DS=@u^|uEK)PH#U_T9cjDs? zXwGqeBeq3a9_b<VjJL^`Xg9{`PrFg^eU$391vHSG1N2k;?^bPqeLfJlZGtW_n!ON; z;wtrMMhPTHl$7ldQaz$11i^{9X+tLD-F8V-xI)AGUwpjKFh@Ul)FgKJkV8n3W_*%3 zx7M0D#X0%9=({wM_C^8t@d?fr0UjIzs!z-h%%@tce8NGQT7h%s61p-=p_BD^pM&!J zjV)&W34;tFdV~{?V=Zu(2r^h*61n5=@LMeLj380|99(Y0Soi1s#{sz=<+$TBf%GWn zX)lRKrGHZXtG{p+0yFz!O;2=0xJ4>c=7U;3byIiiL5X^MdcQwR6i`EVB5@?~Vqxyu zs_3Je$n@8Y9@a9H*jkKuQ8Vy8Y$QEQI{Hy?7_V;vs#Bu_ea`hG$o!)V{cFD~w=>-^ zOpFcxA~26m+DB@NPak4z3BiIkA%S96Rqv1lkWiyE0Srkj#sB~y5_9A?qiiPRrWaIZ zno3J5gm}3TnHeINE>;U*%`?|&QMZVZUyv8A`{3wP@YPoNUZGuuz`TPyR<TM)(X&B= zJfdnaGgj<)Yc=OheZuMVs+t;eg-YA}zGr8grcY;^sTC>pWqFG5qsM74?yevIMkqLa zzR$V7optU_g*9?BDly&X@g)!10O)Be+L&xe<BJ_1?B|Q9wS*Ve!pFnufDa$<TW*80 z|C~H&x{SR)2iMN!DTN0L_WJ6lZ0^UZjN3+sn{V?9ymy<D16ctP(tYC5)V`)x6MmsV zSdVDa<e7UnapEA^;lg|1sgeddqLHrzFncgmnHnsBT;;$F#sXjxhGQf0rD?+A*hw*> zz-svsg0;qb=-LgM<$Rq`OtG^}@$GOn?LqJTa{KCo@jlnJgcrkpTp=|RU-^fxrCVsi z=tMLr8|SEf-z`EzX~b%eUJ9EwY6*GU)D6(4VGDU^@88HKofkGY#MpPR#EOQp@~Ss} zcp)avFgBL>g0f~*8D0>hVm|`mT)KFY`^qlH$e~^6#dwbKt(0qj?$9d1XMAP-deK}P zC%gi@w~P;bPGr<2;DAm1T1V2qzwc1<yMcF$#@qDrq;Ci@@FSG|lSmTc7l6UQ-@z66 zIOR@{TyR@4X}G#ViG%4Ot1@<3+FW|V3-Ke?Vgoox7DR9gScCyfb&EnE%8OM%Iy#!x z@)#i5EjK$XY=+zyx_s)&l%;!jDt;^X(rlTw^>N~{NnGjb2>-N|6`rJ($&i_Vl4qP_ zxZ^Y^Vq?Cj*81<9(PfpWZH>95X=AVhM~U+K+)zZSj7s`zvZpfNQ&`S7>a8q?6)Z4| zFv7L7GOG!rF~5;StEkuvQ{Eb5RLnhj`C^y9f3BgFu8w|j`<HrDSQF=H%x8@hi@NhE zx+JQYemw~Zi<@V&CnIB>7}@{CLqwU)>0awPs7>8I@VSubm9WS9LA?-|vzhnyAEkD* zREH2>x#Xv%FHuzC7}x?<&)!}7NkHV1$3xU<teI7HY2;#+<#9VlM`g4JyBtNw^kgMh z4LI<pa#hK?Jxau~$4Gtb`$6kZ<H?_q<_ba?w{pKRvc;~5F;ck;iQ{MK$tZ^zzc)0W zdHvT)JwvFBdZ82=hRaW9D{mMst{4F;TpXDm{H|DOK*_1gul=^LTkcdkKqpfkk8oM+ z(csp_pVq8PHTscRZoHmmcD^hJqaan`&pUVtI5xt}xaKRZ1NVBQ5aG(_wBc4V&FmP7 ze7uH0klbd_VJ@~z);TlvI}Je?<)ddfyGqePON!5TS*@mbUm9+xreN+Uq<nWs&*Ukm z(>5IZ=yvA!zz3xyDPanjURW74d}nxxKbW9aK4EMFP0#BQ8Aw?1gYImcb?qGo|L;o9 z|4l&r@4tMOtX{1^xE@vS5mq?mW&I5)I13xP6{H-Z!b!zvM>CfzNQ2!~UQJoA7f1uK z4LbFva~!sZkoU8EUe+^L6(N(~piVUrSAVC+0YB_g9!SkR7>^mw_<@1$Lu0QJ1XaE@ zP?ljYWXvWl_mzTYQ(9;1xYCMETBpCRhc=kU_S`4u{`z3j{^z@OUzC*~n^Ku9w62sr zT$y<%?3oU^bwRqk@_;lKz=#g3i&Rg5D7;GNFgJMbV_pF-+ep$CAA~T~!O{Sa*+^JH z`vd<^7I^`;cPfI+VFR(z|HbE*pT<f!SiU9W#n(vIcO2l+cdi-M3q9aeAsVQf1xefJ z6Dkmi1he~2>Z?MMi5K+<XfQ-Ul+MrT2j2(vz%IZ5w{ZzlJqmq50a)Z6X|NRl&zgFY zzzt{mnXH<RQ#G$jE4Q7$#HvxFLYIe8RrzG5zmX74bZ4b1A5b{Mapp^Sv9QXY?7CHX z>xs*@8Pfhge6POO6o)$$PL{sqX`s<BzB~Mtlha{Kn^inC{qIeNo+$U!kE)x6hJ=6n ztt+*wj^``9%Vlbz<x1YGnL|<<eC=Gh8D_D4-fv=fZKT>>9q^tHpY`CUF88!dn?^1U zzj5BJNXmIy5yvMYveG;>?`%-zm;q_haLO{CDbMLN{<kptEccs!pw`UgfBrK{L0Cdt z+XxAu{o~-hynM&iZje4MP?8m3)6}lkKWm*sX{v`o;|#OeP)UggY^eG~0@;~pc}|fN zsk~4v2xC1H(on{_4_XV?BIhB&l$Q=2!UTte7sDwXg)un^Yh`e{FoQpVq-BDE(2Y`Q z>Rx1FVxv(l&XgH>IVol)HDdxy=+AzZbG{20*fF^yLmw2nLjr<^vBY9Quz`qt#gQe& zMcVIH%mDWggo?DqDJMB?tY*db>)(3gLijb@{`NGLL>ep#GEg`UKQaD0B!Kg)lc?8+ zk$iG2^|V>$;_y;#Iql5n@3ZSf3{Rjx>4`5{fItrURB$i+R`#zncf%7Hv9%ruge%Lh ztKy8zoN9%Y=p0t4hGd#PykGxpdnIsibJqcQl-L}@d?krbuDCYpr!k~Q6kn}=Y{d`l zpKfg8UV#A8=cK^Tz8tjKAE&F8Ra54rYBb^3jyxqZxC-r^d1j<P)o(HRd9>3ecH{O1 zmURExJ{im{EmawB-fRX>GfmL7fJd}lpD!9o)zlj~7$`o}vXn4<o^e65$iQ0fZ<+n5 zHpxw0^6u$L2&-~2IA~_zTiom1_Sf$puRSI3Frv-}LjgEY83^=oT`;wYc1te{FP*IL zAWRi(k5M^@7G`CEgs^rGa9Bh|XAbNqM+5~+14`)GnZR#<Dmt6mDD<h5+gjSRX{V4r zff;by%P|v&ke1S%)+NV<Vth{ex<t(#X-dMK)PhtRG4->R{mFI*L;P3&<!x*&VV0J) z09X$p0?PQ&`Mj#de-X@@L>-`urNlirnS_VWZX}GI5v+yE(D#(qjhmuSVQ^0TJ1jkC z_%u3dZwCa{j1rV}ukMA;8feXmnDKC-Jl3K`CMMw#esDE0VgNnm+Bvibsz`_$pTd!a zfN22SO~1pf9?>>^v~*dT@qMK_cd&svvRH9)y({nVs~D+rDzQ?tbWP(eVc5}*V#o}5 zHT-e!N{^r19Ci69bz4Tw(nH3-sY0Kr`FLfUA##;%kix;bI{S{ggrFd&=dAt7wokEW zwME=p(t(RYni?G@zDPMjihJXVXFo>AwI_$lRm-+Bs`?KH9*F<|Pymn+3Xo17P9F@9 z5qc6%A&HmivD3zp8%#p+U_XrVyb{q*vXB5=x1jggVCqZ8z0ZP8wB3Uzh`Xf0JQfcY zf@c%%tT-!qDoeQ+jpr}J2ww*@TDnuxG=wpK%86hxIFU*<@!3AB+*sJow)eO!S0kUM z(KLx$>QRx*IdrP|ox!beV$5fI|FWSYcblPHqQS#fr#`=VqpA*z?#))Y5Z>FL=*;M( zyAC0)U2$~bY4!oh=b{r7N+7r0WtJ)sW!m4lR@{^6p&Q4BS@ExKIqUvVSNB&&*4nW? z&*)q~p6Z1##be{Vh7X25rn?75b^%|cMh-Pzg$vaTOZ}N^n0q=qc7M3qH55AK`bA`= z-0H^Whu!T}^6za@Qz$6_fVp<atO?htp1Kp=aJR!bv!Q7HV<7wphu`z_10Se@sYKXI zbbYQA7Q0t>G`&#xo<#(qHED4-ZU);kYH?*tPflrpi!i}*X}QO%%DuG^CIV{R^WN{V zNQy#T*U_>7Q)6!Heq+@J5oZT`ftC$Lfx}Lpluuk*SiP$wANO!Wo{2aQm2cRg0#Ci3 zH>(Ah^=)NrA1c3o=jb9N@<K;<^gwDDRW>n3_Q|tHhYz*!VULm)FR*$U?TD|&I+_pj z`JlGre+aKe)HWD@sD4wsGN&Jyn%D07c<p#LN1~sr+<)44!m(Q9i3(R<`qbiB^xR&} z`K<G6z3yZ-`2)$9PgYPJ_r|EZgQYsx!;{^bgRdt(e^B?TWu4dQ<lezJ#MuP?nZ>V@ z*=6Els4TUhr>FW>r25!q@<Hz(_%O)NNaST?4}jFmr1kn{w4N$x^;Ff##^%#qa;3v| z-SZFyKtSOR%q;!MU{R4Bs~%8VK7|r!xVS_lu2Z42@LiZYMew?p7$+Ag{m5kU#tP@6 ziT7jb-SACjq>7TTvf&{^R15LvZ=20eU#MZWO)XvW^k%ZeuPd-5Q!fu2=sNAxRXNp1 zI6f00nl|ri6x|$VYQK8>@83H$Zk2xaIUhe%D@z4uxTRYp$_L$FqA%xYYD({@eR*|l z?2Av`iXG#D_j&sZnjI-0{>^1ikxzHx{yYBnPAKzE)U4Y4?d6l)6Zf8v)dA+HF`I<@ zm)s^jEdliC%I4jd_)?WNFK1b1cv&!^>W&}*K+C2oLgx<Iy{<*wQNjjE!F}LEFFs^o zjj@6RkVtmRY=H<d)wN*wL?aL`TNb9!N_sMADwG|HhYuPV<rMmp!4pJbJpI>JUXV@b zJ^6PMWFN7lo|UEmAQKpZ50suGq<)<lGjaIDd!%joB{1t5fkoc8QI%aqC9WKKnhQ?_ zA6kH3R1}pZ(Tu`!KwiYpLq1>i-qn6v=#Pf;ldkDZHJaZeHDb$kJasGI(zAmhe`pkz zE)sr!sGQj%-_Y#K+jF6dot7_ZoM)+D{F`O6D4M5K^4VS-Dev}WueF8w1a;hz+lX#R z=FA#e%1R1_iT9}ak;LFqal)YxLlOI)mcA~;2B@acK3a9@NB$`osND(^R*kEk8FQRj z5i-#E(}_<&P=O9b*y*chcc(q@QF`)~89jf;E^AGvfAXoXDAPfmCPMzh4P|z`0C9eD z3(pnl{otCR&3xdFHQCH?C|jSM>$5Pc7Hm?4idFt=I0Awz>+7HXnahxmXvgk*5bs_l z0k)#=eKuP)$L)dv#s5duR|d7!MQeu;+_kv7ySKP|2=49_C~n2w-QC@b7MCI|?ykjM zN|8(7JM+o;lbK{DGkMP1Cu{HZNDdZ?L-$^^Ll8a+nFv(HAO!{L<pWn~NSTXn&mz~v zwuARb@MTol1@HE=80Y0du$5AX-j0~^OwXGEbu4;P8tM2A&&;KYwE>wNl#ZE0wUl4+ zOV)aeyzDcv*H3>I!H0z(U%nmqDB>7Pr24u)jWELjsf^6V9GLKIvFVOrKvigp8)<sc zq8MYBSYB7Ushu&#*`w4*KiS&%lTWvo)f2x>f1Ft!3NCB#5dl~MC{Rl{8{-8c2a86X znE&D9t%5AzDb-is_3Xygk>8s7q|{J6=!$==!z;1nvN0N1DN;>uR{QNvze^TA5h>>_ zil*qa6wzg`m10L=N5p+rpAg}lL*~znV_?b9DpG^t+NH?N)*A{yfcvluh-xZf=bco; zRYkSaPb(G!(^Ng`@YG%nPM;B|K7KEMUQyZFmYvhi<OS5@!a`;>W;7&beG!zQ(6Mqf zyIAFPDtUlKlV;@3jn|kdEIQ&$;oPkh4_#=CPO<`;2OK%_A=F4#xIrS);K{~^0!KRM za0aea^F~Rs54A%zQfO5a)F7Djs*d)8_}s={IE%LGgF_=|A3LaD@47y3&U=hko|Aja zH^!DRdE(8+yDS64Ma<;I(5JVI0!f(NNY4M$pDGF!3*_EQk^Z(z`0lZzZD_x2PE@m6 zc_M-y$(Mc8b}z+f19_1%>>`H8a&;_>9^ClbP$|0<LTvlF^GgzE)Xag_&8NEO?z^xY z?$@sDR>xP`thS<wVS%5iLOj1iK<ycnmt!d=MrIX_sixBlc^whmC9OXfv-egG=@gk( zEwP@@OoP{CaVBMUmj-y((o#wSNaFbCskcsI4?=0N$~~*g`}vCa*K|>D*<aL0Gp)3_ z5+z|w%<?1E2aStThU0HjO(ikJ`V+9CGr&Y=`|XW43#BxgkDj*YU~~6K_w+2%Xk0v2 zy1t;evId|fEwxGe<Q|M3R?968Ruq%j3;|V{Dxx5QEQEO=88-e4?k$~uIDvmCk%G;= z6z{+KvFC9Si2W@-I~rlq2h)m)ow#O3u2bdpS^*2`cSZXgTOB^u+sU6N9-c&1XnKwl ze_V#JCZLZ2F>y8x3muZ!aZ^_(#4`?0K*J&7=C=%5<O;P*W)s<cip2uM_rXI-vt=#) z()!JI8V|mjcd5ck0_n4q=&r3TA&nd|8fJqjO;}NC8HmWHg^MYy%Zt{;A9f+9nH%3s zZZFnrEfn8AH?>S=y^p6AUb=bSu4X!QURU0PZk@>c^qs$DX|6mdqw{}ZgxwV-M+Niu znu9sWZYza@2fLs=iSjuyk1$_r5ZE)#^JDhjaSDFXl2Z*ABgjiQsxH%L;$TS`k!xPz zNj`+>Gh^`>ErhmF<!)q9IS$$IG%d6tw{rW>|FD$Tha>jVbavltHsH#CYIJX^S!_!d z*G7(Q0Ev}S5=ATlCYW!b(nhtgnZY_aMv^;$ShY2}8@2FCkdA8rIS=dq>|Fo<tOwWn z1LTmTEcxv(Rw9_y7{IMD)#Bx=;7sk}<si8MKT1*A0hOx66D2fpMop?}TGH25_E%l7 z&4Q$v;;vkQMXfI5c(h6tO59Z;EdvfT15H1@si-%(AB%#OEu)izG`+SpgG*~=x@I_7 zK~Z9kkc=SMV-Y=x$|19)-R1*04Hv{OQNmn?WXpIkqgOmYEs7xrEkTinA+Di6Yk`u4 zBcf0b3(7+5MnK45kQ1X%gQOsRG*;u6kQk5DE|=S8jEmVTS@>5!gWPKZZe;Sad&!yF zj}h1@!BsN)o;_a^M>8QQW4`DjxBzUEh%_gJ^5g`VMjCe5<sWnPD#kfUr+Xk58&pCg z$2E2Yz1t|W2)~(ZO+U!r(Tand1`KeE9F96mU!bI9b#@tP?o5t(le}4*8FG%ox?BC_ zH?!Z$F1Buqf?b^y1-(zf^<4@QWbnXvXaNKeTl7}r)gv#-D4>m#GHv{-6(Pc`H=h6# zB*uaW45A%qN?`g46b+G$p{33$pfixcln(Uj(Mp!r*=DLILLetrSilVoOq8s`q5w;l zwE0-!Km$u4<2Al_7Xsk=T~@K{J<cR1oXiiowy7;<JWeg`(@fT#2^b<@f!>Bc$#iN= zeG+t54W7-O($W6Y7raT{3Qz%l;$?GmqkgwQ{x-MoioUfOJ-bmgPG+5Ib+%>kuRzEQ zLNO~~DoQSQFmoQA)-7a;^Of1ugTRQOM#iV&$WN6@+68++{S(A`^}Y8Dl_m4{G+|_h zf%=Pgfo1sZDEWSV(CJkHFRy5V=mbJg`Ipj(-zshl<0=sr>~7%KdW{_X^7VDiYe-!I zQz@x&6$6?UE@Vtoj*yiu$?HaH?MIjda`KG7Y2nmZNa`R~VSOf;6(5SyMzp0}U0gnL zaCWvT1(FzCK9$gDA_}AqmH$bUj}=)4&!}G|pQj5v(A1gJn_oyTAnA->w3(#oOkFT} z{>IU=QWL(9aW~e-j?aieERIDZWabNXm+6~nTRq7XYKrw=KGD6HivU^i0Ls+FaNS={ zjR`8p`1owuO$LU%rhK^sLR!4)iu+hKVJ$U%HQc-S0ge0zHw)g(%u+<_5BRzP6klj{ zp?~%n$|p4(fD+{@i?uKr*<66-*Kr@(R*f_>W@tHY=gn052bc&&m=djn5_jrx*0L*? z${2tf0zZ)g+jCkfzeg2cHj6kW`zDFNlxpuc=pcoo*Y9ko(3ED^+BOdcG_>uZNCUVK z{igwh2=b!a@f2v#l!*mTFoEV}QHthu2KxNrh*b)0a)<c>phy@MYC5P;8H*~=Zk(_g z-2wwoOlnO~xpFU<C#0J~%G^vgQAv58ItBA5brFoenU(*68C!d@EmA^!$h#KqYOe-6 z2`3K@8`<PElj-03dXpODKLrk84aV@|u?<q$pqnt~Yqx?>iqiiQBoLPG+9npY640BF zb3J@)2yYhH6+XW;uNRqtgIXzZ-Df*zZu*q`aqh9&Mxp%q&bg3_Xl06PyF8Gh)ub(| zA`vu)|0mnuL;zpzIqmRw^ELNInp7fxp}H;DRj*DkVLrOXiHW7TKKaM%N}C=%B0R!8 zxH+w{A7TvLS;_vmh!Pf6zJF&BCJbq=$%&;qc(>l7<~F&%bFMNE?Fv=!qRcB$vMt3K zUleRyIQl*0*>>==E;O{igX=|XW!aF2MZcJ|6o1~C9A5PKN~9ZCszrxnVWam>W0Jo! z?QqY;F4073@HBGnBeuB%68S8Sb0|ad4ZFCQUa^5v69KByfBA6_l6w4GfN_#j+sd*S zKKMT-ADx{FoeTyTq1)^k2-CRbyjtZ)J}z>juVK&qieHkjbE9L0-WeLY){5q9vtg33 zVPd9)8Us{7ToiSuy`qmi6O`B1aLNyGt)fdPco;mBldSGk+V~3R&Kb-(Rr`|%&nA2k zm1;Pu&?iXDDxpe1w^2-Vl6!Bgh>e@B&<yJvCG!)fSfs!3vF$F5Ir6HC_|1_t8>N(K zr>m#eAPlbZrv2~iT!V2!c(3J<h&jZ0%o=NX^yR>0)fTyO)~QsZbn2NezFHkh`a_Uh zj^(H)MKaHk=$~HNj<W)xw4yoYeYt`}yjfz&EU4^q8aFu_VH)G5Vg)}YwGKH_iboYD zHrlGqd6v|}#BKlEzu!ul@o&M8lMuak*io#B#Y<`Es}Ta|mZXbf^Wn=j6JZrZq-FTO zmTXgTM6?3<-$ScgX&GubDHCm2kym1T+~OLvZn@hb%5)M#ABo)Nt#a9q+`#Y+k6TNB z<^D|5%fPqsYWvKD<;Bic%(~tf?Q7Xd@eA`{Pg<Fg57w!3MIppHhBD7$M*GxGas_y+ zCRu!uaS<KovPh>(_U7UeBN(j)7iXB~{3tPd<H%Mp50pw5*VsZ1P9$oO_~>ai=3o3- zv%BA!04S>guJ#~9q`6&pTWb?zByN|nJylP*B3G#mbUYr|rF7Quh>{*^dTLG4@RpR` z<kA|Du4$xA{e@UkEv}4S$)UaVfs>t&Ve2wKMP5HZ$lKF;+V$W1x)Oip-)kIS7AhQ2 zfyTOtrpFvDkR{p%?|iM(#Gi2@`kq~Y@#rriMXjD}v-KA<gD`>TY1qEqQ3^BTcc(~6 zf5R5307C}~5I5)xV9OnSLc$e@?yrHW9P8g~E8%MN1=Xt2RaI_bqOsn++uMxldn0}9 zm$qgmIELm6@v?AFDtPO3Pofd9cC$qad6!=Blvae*Pw`-?BKBxdMW9RNB#zPNvSIBX z5>x@?$B7_ewAlnmq$Ej+pv+tYnfzojo9?d^@dpaFTCB2Ps4~_tPU%|mSDcCoji?2o zu5f?7Sg;~z@$Rw_=T*bMY)W63hfo$u%H<$pAp|%&wH3vGqijFH?1r=}GppnYGtX8e z+t;lX5k_Dk^i!}q{^t*HtH8y-oj6XCT&P4JO_gM;hNZK^e~5!S6UtMFo4x2bb;edl zzc6z?(K6HMsjW#%X^Nq@p&^n0gP{{#gkK%4&WJ0E8A{r=oR)MMjzR$lBm1pRN#bkx zgV5%DK)L^2Jmq$HS#AgvZFZ-^hT0qdC|@DLEIDbe<r2Fm<X;<d)oYxV+*_bbAJoD} z14rSCtj1OrNxeoTdg_=MU`h>?V@RMA3CSL~YzI9dcb1_n^a()nj`~QconRlEIMN5d z^W6*AbyStx+AFbz+Kr`BK2?q<WHe|s{ZZ;q4Wh@zcE#Q~iZ7G5u3b}rO|WlMqpo6; z<T8#DW)B@V_h%Weh~E5GJ16<WZZtneaXwBs$_?~>BAWmCG~hAfU;Qj-obpcqW-r$o zxzlh+s8G)a8YDcesGyzkgJ+-0@?ka}hA%p?ulN$vc+aSRD?H#l;Bp$6#be_=cJ2?^ z9~Ok!cUji3_(~I65aWlf>N;e7&^vnEtA?&X%!)3)6}Y0%$<ej0>c?Kd3&tW(?2RdG z|Eh80EY;j=p%lfZ0lRfCXm`Y4Jh2}pM;ItvtVkE{Zxeur06i8)R6()ki;)~jw@O<D z<NrN3d^g30-kBc(n19b)<`kPjHdc^0rmIw1Scu=2kh~oN@{%XW5sTo+z)ge=__^Y{ zcf^m<yQ)u?U`EH~ufdy8Obx1~-WP@X0ObY8*(HsyWE5Wkqw1?3C%QwY;%}Z?+S{Bs z0}cZvn`T;yH2X8M1pJr(6{9l8znnO`0kR_9#$z?g;2)v$P$a`1!jlvsjg`7g&$Ri% z@0!G9(H?A#DIR}E{<RB4^)ul~mCsli8BznXW~lQ?V1|5NikOotkO`DpNaUEKzb}Jr z-UetC6OCiZRX@w%k1bEoz|f5dXUE+}%~B)EH0M>Fpa$qAGq}8femd93Q#<1%J{##% zWPeO!3Tu>2us3#T_0ImWM;-nsO%$44AyF`@L{fx_5cqkj|G20U&o>u(e{LeCTKj{i zx~Cl8{%*dfxeZa3gD?tb@^1*@HPMpNFr`lLBy()6la1iYWYV8v4itvGMLx|kL3vJN z;7`Vpqr&LkWDnWcpyV`4y&1QUSU>K(m^$(I)-v!Ex2@&LLn%m@tN+!Ho{9_q9Q=#{ zU_aOo&79vqhuU^Oh)F7tT*Nj~^b>Q)Z4P-EJsq#IkxgMBkf``ExjhUl(iWPe{rZ9^ zleJU3mx_MD2N^9g!$^J#6(C?m4U{`*r#?FXtA}Tn4G~x);$rTZWQr7!p3+{+s`Mkp z6pVUtiON@qxtA&rbhp^sjo8pHVTY@%lZA@bzmJe^y-90U5Lz)Fm6bKXh}_7U8DO!` zsNs|-8hQ#Xj9^HD@TYiq8X2~zfW*ccmLIL|uxeeG`i)DU>3*3lueb~9Rm2u`^-7{+ zt2B^hui~kv5SoFc5jsM2+~`y*u<QXraY^iO%G=T;p?T$c1`gUHFGOV|aP?PBaI!L{ z&em4(I^wh)vecjKwsHT{e=&;(^Y6=ylVr2eO(R^7K`KXSL4ODqgyiH^se`IL9&4#+ zmNt0{3%Dp|W0=Y!0|pN?3)kj~pI_WMXig=c7)>L)>D3RhW-QE{1FkAZyH-@_vT@fE z819lAJrUDi@><$>MJ+WHio6mXx>rlW6I~{LARlV$705XoZT&(x5yHx3NZ$d=Tzw#= zPmy*OGv+Qgx5x<yMb$8~u=N-XBi4KCJFT^mvaZ?LX_$PSn>iRMUEu1UkeRG@!a~fd zD&#ewXc_v#7_f~g<Obt9*P0^r0(%Gk(>4-)W^fO6{08@|VRAV<3CI<vGusiym*hs4 zCVSF?{^EGX#2>uAc>HX`$xi3Z+<NO&;Y*ZD<xHEBpm$yL2^+r5F6LkTaEK}J@0E`) z6U9$k?!iO8N=paxPLow+tG%d9)ER5==S3Z^6I$hlA*skh8s?&1H5@$ERZDy_M0gvf ze)S*K1b?rBAXk;jfR2WU4z^0GF?0SzR)TpyYHQ$meML}e`|O`$CdN-qni7qjI9|eo z0W!~rTUulyK%!hXRcP@Ban&KnMhpd91v+LM$q+9~xj2grRjuebe}<$;HLLg?4<1J8 z;aEEsQ;Uqj#=t7(77*uRNe=aA6(z$+jWG}i$Rqotw=SW&!snWDv+!}DJ^0seTfGO* zcuc#Bqe6<wDn<6WBnn}GjoW*81hs6m#8s38;(arGH=0C@L5LJivXh8uO&D^Yn_3Ah z!C=kleW8r_);pWvCvo-4fA|Q>GV!mKkCPyqQX4m#S{p3X(!|eSS#{XQ5tk3pN`qbb zYdF$4(|zh2Yc-{+2*t=|#F54Fr^{cY8B+GU!($0Yt+N{qQH$oOM3zRg%JUkw1r(Q~ z2;p1VSW@h);g8CqshhtOsVuCZ4fzz~%>{pziE#X^K_4+7s8dJ3PBqEt-$jc@C9I@j zW>ZEJX{Vz#thj`<Rxal&FjntaskAghUFpHNu#|&Zu0)Gbo+%5bs23gbrA?WK3%7b= z%#ZefMI{Y!;0k3RDpA&XShKxTjZ<2S7*iw_5ik1-m}D>q?~G`)*rS3~;!mw>$S=Ku zLq%E(R@5HU%-FJTJc2^)Y=vT4ZuqH@wBobnQdLAGvZ!G{5#M7`O4cm@;X^NR`(OV| zRC7i*Qi+{7R?xPiL4h(_&hy&_S5xjTnVq$~oOW6*65G3NggTcq^<aLCU=@hPkrJWQ zD~@U7I-{?%4g?me<!JRAF3rdxB&o_jDLNszY-I;(_9(IFCL`3gDSz~C6;mwMp^t#X z8F(xew0j0Rh*vS~AEi)K*;tzy>sYW>-u&R$B$b?O_e~IW9}Flz7oUr0V?{5;i&uun z(Q)RRKJ2`gJSy`lb<AJ1@Rp*P_n90Lk9bHC&0#-w=Be8KFicCO$EQ?O8*j4|0pbTL zPs1_p*+v|A93_y9hPpc-1-*vc$#bn(WP7jEQfU1}pz)DH!Y8K(D{80}h?PnvP_+XG z0>7Z-=J2>b-#V};Iy+wf!-q~7nST%PBOL%jla*9!Ag33#sMjH$mrNbQO-j7t=QpNm z4VB_I8DWXFLczVQ0;TK8cDRO{1r^<-s^HRKdYa7Fr^Fiau;J#g`0`(0PZK+VJd})l zgdd%!GlK_qf&xg<WcMsUlvoA7`kF%a6@ew`c?nmJxAwU%>W-y)4CxXPK8l63#Z*|+ zi3rA~6|1}7GBK~@2U8P6C#hnC#*NrIXc{wrrs83f=2qOKr=ms5vZ6|;n=Kl0=}>&q z2;X|`4~0&VN7*)1<>L_0>T_kdW>*l=QC!l~aTctk|A0SH;(jc4^K~Dbfnp$jz8aAo zMt3tr7aL@v<LBYVjHMih$@o!C{6ZL_$&xmQ7Pat_H7pIQ1WJ9V{_lMLlkn|3KI0^7 z=qKiOb)<hJ>>_?e5n<qbH-SXO$0al`+qW^|oPEe24IO-U_4<*HhyUZYE@Ovko8_lq zdDaZ;F0&Xzk+@YQNu^WzodTbve%8c7U^2L)i>iU`v%(0ZVbI2PlfPeEdOCw*ur1-! zM1nVRzqO^z+XlmI9qmlTwRy%KH&hhN>n77r>)Q0x6s4>pR&0fzNw&%I*p}KRCj8lU z^Ek@Hf6H!m(J5$kHE=F}wS~(YpH?<{>f&-Pa$|groT@0YgKo&B$F(8t;WkKlEAqA* z@J;S))QWn+ugkW-PLVa8{P-Lre=JGie4<<enzO;ctRRXp%q|(2hyoHaR#W98o_i%} zA}wH{saC9=l*R0_@DW1_{?(7F$iMm-C)J}8)0?47p=M1eBmt*Q6Jj9NGevl9-*b)T z==4j-Be5F~Nm>3*!b%sR|NgO5AKaj-R)n`8Ajgg^;gbEkDPpC~mX^xgfr&sa>=Hk9 zoxH(x_aR-OfkQk9=J!VQ1?~+qd0Je$0}vLE%4gmEhqUBh3ur+i>8SAiq~GiYRXX#- zox6+llz*NoDcl=bbkenxV}F}|l9f;&QKK)GrA&%k-jYrs*r|_<tjN#?^sA_Q^4ZGK z>sx2A)W=DgU+R`V3}7t#!0ryr@HwX{f_^2U#u?iGawSXEmRO;k>FrZGuQw5DOWN6; zE<w%>G+?bIK!k3#nBvf*CMC<u#htQ2f#Y@{DoF1J*IygM^AmQA!;SpI$5ddC{}e&X z2q0}qqlEKrz*H3*Gj^;)&JxRzVW^bdS*}jPNSkdM!o`!ytSgp+8Z|Onf0H3(`2DX7 zx3VSVCTa{FZ(9*fKlX!B(=jWn=Y)d3$-`_D3VK`?pDl+nI7evwM~c8lrfQ2g$Ph;o zbjhK#N*$|qX34#tZxUL%#rRK;$Mw%Av|2ljjRcRnXC3o^!iY+?2%4TZh2_HMFeocL z6Qc<FK={BLK~+vr7_@XEKT@@2_`OMdvHb<SP59-x2xe1-Ie~B@-h_x4Mg+#R^dAOk zII!*q*==$&FNfP-#f_P{>+KyL_{qYI^UGi9&5~!8@5Bj^ImLIi$oLl2#2GoJT78VH zq)G`a%_IS0WMlN1{8VR(|MiEF5M%);-*^8gmrhl2N;sn{Sud)Lc8u^;$<<||N%!ly z(w6R-jRe{0K4an^zN8lB=X-Ss?QUM3;4^uA83y^1ck;HiH7`=9ZD#2H%paHiV}b;) zmMyEZWgKR1DjjVY@y5->$xjbQ6#T>H7q`x#YC3M@($U-~vZR{Icxk(BEjrTIUXyUv zG^Xom@|4_^jyPO}$~2KmGd(w4sm0BA7KQF7lf4bmtU75w$lJqV^H9c!$LO-p)m&7U zaVqgjGHtcMA{Q38?b#Eg^dXg{Q)3<M`D}E!^zgk>+#m7wdF<Jj!(=T!d1ttYF~tzL z6YS}HmRZj24>j#Xz@JE0H5yh81kSTHCf{323sHPTfQcMAmdNo~v*-l<s~-iqQvqb| z_xvV}2XgOO3G=B5@O-CFo$*{uJ4R_YF)YAwDT*^}jXWN4qu`WA8Ky8ZqufzsggTQi zM$W6-f7__H3VC3i#7F=DFotMxF=*W^)ua%`n%ukZ#%r3Ond^h)*XeaF667{Bk>X(5 zvpwn4u64&UG)uc3F1R1;Hd;cuYF77zmBAA)V_)mS_;WgMNGDtk67P=$O%6V$sZKsW z@7oHDe`9{B>JGc?-%!6Zxbt19>6~}&bS7wLE~`p8o~aqLGynC}z5n_7YP>sbalb?1 zlP1$}t*3rX?D1w<|Jo}-jbcpra0`Er<#hpiwO5N*=~%u?$krf(ep4_OtFvc1RH5W} zX=G?Ec|9=`GK*E<Vx|#3*MI(XoO0ef{f3#N(7r$59b}#jVCRFvVAHpfr9wrpG(_lB zsK?noMdwhGiU$yt!h}SziNF0>i5EUShx6Rx$s>&D-XAeLhdu9^&cO0{Xbi{78JK3_ zI^cKqIr-Ic6y7}IRQ4@L9{KyLlc!$HT1Apf`73?Mx^Airs|>@>`X4aeMh)wK9-P^Y zpOw?j!c{fWW-S>j<SCuM<h;G!_>5~deFG2lu%5o|KYs1JD{7h=e6ymQkv@Ac!LT;- zYI?bjyiMO;H+OzjXSiQ$+&G&R#E^YT<z>usx^7@C`^Fk8jMl|X|9xNBrGfF@6sxXj z+96h*yfmptA<mXgphsn84Mc}9*4bg8x&ys8#Q_K~cHKjUVk1Z2F*ou{`)~hFB;45% zzP&mM1w|&>If?I=jG)h{S2B$~t<?V9xa=u!@YIu#5)x3{y`Zx>?`zV?JWP6s$gV6e z55qIoKUh==G0jH79>L(61WXk5KR{u?Q^{yI%s^4rhNF;cAAi@Z6YTV3TgyJayyu!^ zkfe7ro2_fn(W>fZZTl-<Iq<Vuki=H8amEt7kb-E3^03eSQ8*^Y_LFgHW7GOY-sH{o z+kDM_=w*%HosWa=dxY-O^Fwvx!pGOcucReU1UIekr(?2Tn7#_1f2$k&c5-=m@t`rR zwY{?V@O*gqH2zSfj1<#}=%pr|90n=fh3EI9Rzidi@b5e1{gLVaNarPn1ru-s>pvhj z(1ZZMRx*(ShJ=2KQz%JjSc>NV@BEofLWset|6Hp(ZN-AR1wFi4{UC>mmjf9rhdHEe zOkO*1haS;`%$>SX!Rn#wkz!?Iz1VyvKGXckj77dUPf0bK-$NAvs#h(JFlbK8JU}6Y zm07;>OoSOLe9Tk~x}Rg+d;z&TJwm#zEjjvjXn-;SbosYhon~PkQ?3tOWsfRbW#PJh zHtuubCve`;y!SxfrMr;Mks+sUZ>!;(X;-Nrre0Q)9x!J~-UmYCnImtIYak1U3okh8 z(8TQ+OT4iRT|2=jXt6MfDXHG;tLY}DH!Y$*WaHQ=Cjq<o+_Pq#Fx9vo$z{bxJ7HcT zBBu+$rmtA%2?b{$(%@xN;ZdRD!Ls0A))8oMQh9w!M?^@M!b-;m2Kax7aIOFM{NWJ0 z<uHQM?}fTPp@(#kwUgW5*ars!Y#GoEQ{4=Q&1}le^kl<g+4>5xog?hzC|;MUsO$bL zrgD`b;yD*Adb+<Gkd4{W_aY)u7sauyc>ehVsrB>AW60?ad+>cRe%pM$zS#Ee`gOBF zFmm(yp!fFT*Zy?)_WCW2`}xAVcC9J4`pavrz<}_tcf!vbAy?h0#%*1;yLo3}y0y+) zNN=X^QN?#omSnXdQje~Vx98f8x2q=EoGiy2y@r*H&GnB8R!7v_CyQzOvXp7N(Mn%6 zm@IRoU!UI&Egw>w(tjQQ{saI}Q2C2ss$K%<jYGfX$FnE?ivJM`Rmf#r0iq}j_F|*+ z_--LdiU(maQo$@vDGHeJ$RDY|jrot?!Y`%A;J}^j0R?!0{R2b~JFCFGjQjYR(AsS2 z(w^U%ea##xRmtRa2G8o9Bd&oJKJPRy;L{lRa@?kgD1rzlwu<mnXy&O-NU_R!k7i=U zwEL|1i$;9%hhf`<>S^oR!FY7VQeO2$AJ*uRXqvYsN6#0hoGPqVVY$(a=xJENEW*Xk z&t|4UHjP#5?PCWQf4@Ee$1giMZ%<gC9{((u^rcQ&j10C$w8guZ%|&c*j;OkSPR}Z2 zjF&5BXUkD%%r%+bnnc1e%89wH&(ck{<S)S)qVN-TJu=NP`0E`$6}rEx(-6>FvpADt zg~>@=nJpXdf97({E*M@uHV2J`SkWA>CFz5WLpcFfP}R!h$2E4%E(tJYC()Qc`OiO` zLyk(I)G%Wd0AASSpnYe3W+7WMwZc!LY`PJ%jcU0B*a1XytQ+uf15!qJ5<azK$tc?% zn*`q3df(W<q^eswHlBlvEA!M~SUoF9<@lIWd%1Yb=va1#@<zWqPB3RSNvs(%ss4ze zmM9aj+u`I_Pm%h4?2xmT{ld?PiT)~hQAhq8A*EHuWUX{?@{_jlgJ}<Tr_DUoS*3iF z&zRQ5oMrq^4adM7`6+#(QfOOT=g!bt-LX4)m-QxH$nu5oTc@SaP6l{!Eb}|>{tcvS zT-Enn^XpexwfJF<TtA!=$iulU6Z1-n<)0IR^%UjpRL{k;Re_SyRE_?hdbtfl&!iSa zFhJN5dYf#4D`WpA@juAP8uV%E^b^Q~|JD}~uOK)b>RtaXqoK>35hm`BeMr<!j)Cwe zW1s1i=3zd1)F8Z;^twz+`HRZRn_Roy+^i!aonZtoy_6A9rP+6M7qyfAn9jv6J;Pn6 zw-@sj@AMpU!TNflS7);5Jf@bz%GoJu+gN3AEYDH2mWtMpQmHM;JZ?Ci_4Z==_OyAC zSGTzT_I>&AE6S_k<}dv2FM4BsBZnd5OSW%%hp$y?uRM3cK$WT0luwS_IMhE@bZmKg z2vYxOH_)33AE3Wmpc0VCh+UTd76<^g`;kqILt8ovsZ1cMLqh@OqB*#(X#1v+A|6e9 z{p-m@@LWq&2MW=}Q9w3oiv=Oix_BiSknTSY(-Gd++e|IqKhnkd6^eN#tr;?53!RVu z;RELHA_y*Coi15t><7WI`8Vh5%(ZQV6*|<|+DcOB*ju;lAVfv48q_i2%=F*0Fx!cF zhjZ7x;^ueHmnc;l%2^eMRS<vgCjrojV=kT<jxZX&UG_{WGU}Ybq2PQmffSYN6x#eK z=dXK?-}@N9hno)UAM&Szk|;P17irV_M1W#Y^zt?H-s0``>q}|6y1I7eAck#WMl2QN zw6H)D0}ZBAE}M>)KqZh^i3X31I^7W$2D*rNMx1dhh)LoLmA@Kn5V?^I8HxN<8xt;o zD7i{`-?^r;#sS2ZKotScjS-Q!L{j*z#_omRxziU1kq3WKez1bXr7d4oM#UtvCA8K@ zHsWCUq$o2Xd&#liZvo{q^UlRz!{hl<*Z=s>ejMH;Dk%CMfYmp>1s?b4nGfrJywzVX z9&%=MCBmZA-m-T2<Dz#DwZ1>x-Pny^z4Q3KzP<c?d;3`veZt+kEia%$tiR%cz&E5J zF>2iZ*Icx5!2m!;27m<tq@dDqN#FIf)AOKVoGc4;{~`c>MF~RmUPeQHgMZBu02dlM zG$?~cJlaw%6ILSA8gqLxr|;6yL+sB0ULtfRlvqZ&;6?{bih@wG3=@QiL5d7ll?=VP zAoat<H)FtF@1TejNWj|G7}Vz!L`TSGN*M^n(Mz#GB{rw;_$uXr7jA(_U=a)pOM%EM z=mZUi$C;l`CL4kYtw0=#SBo>`tDI5a5Qh!IttQN!ESOcbLbc$M2*QA2Z4%G~N3s3a zAC4Fe3V4vd?=K#YCG^?}+v3kNUFZaPDFFKDfv1!lJR$F)WTN?lRPq@FSci!6o`+1u z4QRvH6(a`PoLI4LHj#1sz*g=9eG*$7#~-gPObs!VpF5q$Vs~r*4y`J=yWSxsYYvMu z6lSN$7>?R6hB1s($rZ+pVD~?@NRfu-SvMOYcdRc{>ghW?ze3W?<}UwUVgGIZGk~_A z>Av4|D46Y)y5V_k63W}&b+mDWoM9k-30dpRi?e^N{d8STJFB!t!Alzmo61YSIaRO5 zj_tsaW}pmy%qyT}Do6t5DclVK#bxOg<UCW*BiszNt!jqTxt+3D<NGP_qIn8T$p^WN zLu#SF42eWw0Vj}7QG%o*D*DDguqA}n{^w7k$=Ah@KJ=bH*w7q{-;lvlXeS<Q!a=?4 zAroZ7NM@%9TMJM|HlYWblRulIV7+Txvz*yli{sLfRIU@Mr<~z11eB7fr*1<Us7n=* zvQsTYF>XuEG&IfZUs!EEoy{f6e49Hjlf+i3oN%wPD?5)|80Y3Q^99WvOH%0s3BSkL zF@Tja&1bPO6(+97mlZN)o?ur<$>>XssINaD<9kxsTn+q1&@80|<t~)lum>($kKH2D ziWgSUa`~$uqrNvVfE>GGM`S!=w%=Pr1=O#Ly7}1f#YvJQpUsqxehZfr9S`znx@xho zyVo(Q?_97N@r#x$L4KVfTqoxND>T}a(+WDYw21b)qSd_ZBYBs7d4DEDXp192y}JLM zuZHnJ7(76`LxPc(Ko`L1OYDwPK6e2Fc0#V0QZh2tQ>f%)Eko6(jrP?)I<p_|-kz!} zhh>cAsK0&Rr&sxzA0j~T+mt*H?ALRr<jP1n7sVcex}5@V(pfH2k|GL)8Mx5<8&?Yn z`6V#Mf(it93`Z2Bgh^Gar5Lb|Dx7F|k5wBOu<Hdpl10(L!iaD?zw2W;^UnujTw6lv zsoB&x+H<<kuodk81}{wB&P^;-1X!Wx_oZGBm9iV!e*UG^DDAZkKjT2RTTbDC&0u0R z*Cp7!?)H6WVjbnx%xTbOnqwI!XO=pfT0F%O{qx!mm@(-9N0HG$Kc}a=LsPBWi&o*7 z<P)#kFF|vm4oa!L-(z`=b>f6NR$3Lq|I6R#<yD|DLO%IL%%5)eNl1nWl|+wEg6--z zOt8pSQ~*GW8afa<Kb^q{9_}uMHNFjGZ1iN6jqB!UV^mL!mH;)Rdb-ijwOOF>U9`7` zjf`se?3cdPkFfc4A|^9Q|DYfp0ro9Pkf!65zo-=cU<yf@cw$`z>{$4z+c5q?_wTt) z0?G&O+k3-Rw%mh<PC{Bcegu&Zm<<U|Sp9Tme^QBD)AKNEvybw^nudx!>{aiSr8~Xt zMI}o<%U9b+H0;i&sW$c8$o2o^{?}dp@Ac>UYdKEz%QNq^A)n0L)$XtG1my?X8d-^> zpU#YUI)=||3M0g?c-T0foddWpB63CJ-0lR{V8%WoNp@K(3Fs<z=V;Z1Mkp~P{`>#B z6GIj-!am=Krgo;c>&gps6p(MU7gIt2X{qte|G(Thz=#N5_z3OUEhso=uRRr}1~zmf zMf<0$-Z*gcaQ;sSSGq~jbAxVkH@I38mIyK6gD(@tqz0ZAe7@18oCO>AWfCI%bBWG_ zzAgO5NA%scnGdEB(LDP@n=+UIk0lA;FJ55MHVh43kGR32lK7Es=`-k;_o_g5dsN>I z_qRa-G&kQiKEpG(vLB7Eg37#SjSoz>y|BgbBE6s~U0mF$L24*t>rJ-Z^JKdS65P8I z%Ha}|{n05qM_(?kzXuqhduAND)2cNk1PF4>5m3|m#frlS+0m_B&j>0nwd;j@!qZQF zb%|D=w<S$eo;ElUkRek4@cE}7^mkj%tN!w3&Rt&1+0H1(*xjX*lhCKd)DqyEJM`^$ zoJ=V_8J4_ER0ho%=?_nZh?*YkGOkbR(`LR&W)dPEODVwPFvIsC1g;s=x|=wf{IJYZ zr~UL6x`5C3wdj)_Gvjk?&k2JnBfbhB=#xKlqA>yB#XaY`PgM3ES$94GK>EST4vJ6^ z%zAk0m0jBYzlgznenGYo)ZphH>r@@r4Qrc_!UCIxbd&9?UO3>Tp4B3KnTge7QFJP) zu3AI(;dZs6Y{xPmQ$K2%Ad)94sedo3_}n5ba0}geOBwwaD$F-5p7$YumWcpuN`t%W zZ$#KgHhS5>fRyYGg_+jT5{`Yb`-1@0YKAlx)iin<+x7DGqel+%>3{gV$q4b3<SLOu zly4!@ikCUkO^0fOr8OkDw)($-Ff`qT;^(+uQ6kQk^CsAeDJugw0z*1A#0a#4L(r?} zosMyI93hyS*)deIU;YxwW!;$V$n$inO}v%i7AUO#ozUUbzTWdvZ5?p(lO`?3*40p# zgK(!DO)w9(<-yA&MpPK9^qc6F8@CbnR*F_Y!c}3lWwII65nz;v<@R_unWQV5>KnKj zt@JV)@Gk;a=i8K;nK|;NsU5{L5^X?2)sSOOq@d(FytP=n9@?_#FY&t3lo72?+#ka* z{Fo#LO&Tx@yS53-f!K9gV(~}nw6Ywe6N)O)IiFU}1Tw&w1<tJUEx`gD&8O;Ytz{}p z)hzRhE{y~QPClhvX|xLe@VOEr5(x2^&7A|T>7^*!)kcTb;?1|_IFIhlm0Fl6q^4L( z7T9p5H^EV`?R^^egr^08@*>)9h)fqgex4fIS&ByyY5mZ2Ihqi^7lEZNLyo{si=v&% z59q~Y8{qhLjT(#F8;b<ZE`%Q7sFHfaj3%9Te3N}+f({D8Vh2bF*`@yRrBD;OYS`*+ z5XVG#8nv@H!z7@;3kek4y4>I6-hM=b+VYVv5XIN;65?YD`ocMofH?&^`|hYro5z)W zdXc~``TliU2WEb`Lo(6WB<OiMcVlUMn6M3GQ5Z#42+156cA5Z39@2DRAempx{?S;O ztW-(_;@))vb}KBRGzDD{{001)wUUDt(PY0{#4{WSkr=bTNIgjQzx?jM(i89gQQk}0 zuo>Y6PgE453cihBO6Bv{3|jD+o=d~isfd`!;wX;s=uwezD~AffLc^m2%=iPaM|dX1 z(_=WHn7E;sbKM^&Xoxv&L?$KzMA+ilO^xB_0`TDT5$=JgdYnLJ_T_5O1f-U1M;tj| z)$q0|$8apE9T^jy4gm&Iz<@KzF=Xz1xRNE{Ddtjzw#;*|P&bs)ScYIR0tCdf3n8P% zO^7*bk2|#hWjlnbB{i#CKd=RxISRrJOAYT*T2!kkY3lr0&d6La?P#V|N@HkV{5_v$ zpvD|2OP#mZpvWA1rYUsqOH6fI1MZu?>nuOb#a{Q#&#tGg)>V|*_VE5FVbT(?9mX*% zedl$^QtO@C(PCS>kiq}2e$GYL1VXkB(tYk+>F0bGY^E2P?N=6>PrX)ChGtI}H}q~@ zlUE!j%v@X+ZhL3w_1@Bi#2oCb1HzT?!{K?pfcr7~C1;KU#1vtC2$IdWhmOTWL*dal zGthNV8z8^g09)h+T2XW3h@(JoSk*9mZ9l{on2e9?2u`nX`E(<vL7_XoTEr9tc@~s8 zR_bPxV0m|u&4gWbx3^4%-$~=Q<uaB~K+r{;OCZSB%hpr5DZ*k~&d^G9!gOiqD3VXR zA=Xlyvz4EkI*gKjD&YvDOhz0+%tdyE_J-hAiS(3y{b~F1%}~ED0_i*@?wOv*)CSiz z`<VGK2<G0FqW#`$0@r!3GWG)lx3epn@&{bEA@=+niI0{R{)gIR|LITvNGS;TzxzL< z>wN$IEqZm)U=x4w@3%Kn!HuSBTd!gI3tdxHN?VKrO@?zOMg9@vUpUr%Kp;A5s6vKU zgMVNY9KK^iS9&fF9>crXp%@sNE=ryrNLCH;2k<cshc0Bk*hYZtWUh>;`K=Hzrz|(j zBaKy?MT+>C_69&`Mq*Gv9W(MScn=vMATj|i<Azn^P;uQ7wF?7FCQUz;?zkKY3USs7 zkIPN8al;=tpt6imt-gGc1S6kUad<C_!gWeqPJCxyFk=1J%_tmVz7kXZ#nM9Nc}atr z?g(J)0h^pdQ+`j^-aKrxB$36u{?<Vw{CL*$V_9G?ZuUF*!m|4t$4()3^;BX3uy2JY zBLD$WmMTeBHZ#{&Z{a_F__a8fz`H*-?(_LM>pIR^P0#D1@`R!u56zBgM96-e=rIY8 zlr7z}Zt^{koBxzq5Da{DhY&lM@0q(vC<NF`#|Hv)&<!H<D#0X1$S3UECj|EyCFBcP z=di*UdDK~S6SoN%C)ZI=-S;#uSdEgvW4t9WR26sbC`=1y07S^>#%GKJ$@m6^ZWIy6 zJ)nq!2UG_{`T~0wGn5FRFi6+{*EH}nvPMN~a4_mKSB#W(f{#Bxm<9T2Vud9Sd@fdv z=e8Ign44(3K3cT<W&zI_nu~0X!d|T^9RA0~A=Z98Z8KRr)sO4x;M>ObcYPsdeH{f9 zzFY6$(!xO}fDi`GiJZ4zZN-d939Mn9<EbE&<Exz~bjsbnW?NNH+`skpC@Cah1~^b9 z$_n3csG-)Wt((C;8VIsiX_F`yM;k{SxX1M8lLkPo_7S|xQTF_)ObNFbR9eJ#zQCo% zl4bCQS62f@lBNbgB2(|<Cw^1Y`5x4rD#oj;^1M;N0NXa}w>5J3kdyeC&iW(vt|xf* zDr33Z`h};IWnXm-MuO;mNwF-$(G@z`xiRA=H8N-|@nm>5g1`!D4+ZN0%6fkQx)HvY z5lw;f#28m}anMSF>mv1#rI<IsbwBbG14?A|c_S3Mq^&9f;wvbejnW$X2P;&+b|}jo zg-C0|2iGOPQtQu>whzoYizk2b+;cXz^VoC8bLwAZ?2HX!4Pl0I5rp`mgqSCFiQ@aN zI<9=mK5L|@fZb6J30u(r;d2k+5^$zDaLION60ij;)I0p$7|RRZPZ2X}{r)Ru0Mw)` zP55>Ht(y0N`GWOrM9bjfe$xKfkhQz(fML8&b`|T2=ZXtvkv1pO;Mf}Y(WQ>#x9bP* zje~Ll#sC<ANHoA!ULQb#InP874vODKQb$x6EMqqx9nYub-vb3sc(H>3cnVGk=bxG^ zETCYP2k>?bc-Ds={7Tl_PQ_bY-PDmc+Af);x^100q`g+B(qlrhyOGpN9j$6(rCg*2 zVI>RVId9bG(3oZ&!c7Ng2YhBBE`3amOk+@pfgxArzS)9wP>%U92l~7`+_<?$XmZeM za|2xI`63;%;!Iq(W)797wlxUO!}?qUnXaX{n2ZdaF}jepxrVi`hX3$slY$5YNE~Pr z1>-@)O|F#v)HEkEICbt-zRTdDL6>WMNL@^h>xs__{QJ>xDAVYVy-hJ^7byV-wh4Gg zt)oyAW9}qIlV0w5QouQnE8~B!P5#@MA-Ulj9@i*(!>Kl1vG7KICbW52(94`hFV<$3 zl0Q6tP-HDB4<3s1XA9_B)`dNzU;Y6iEQ7&qc_Gk-c57=+r@xrn(9Z?3%w6<nLt74= zLrJBAEdD~9&&5{x8DZc<RSbM&QljKi;ENW@fGIH=f4OC2Lw5-Qw*jQF=s+>9Q1F@i zT)eIdE3-A!;Wh59nW~szq)csm&67Y?I=KKN7R!{{=G$J#Pqp40KrD2jvxtn|TnxGR zg44IAi??N^)!{|&fA}=;{1r&vTKEv2Ih&hcO6>mGUh~U$iv-_!Y6PpKb$K#D3e;Ma zV9~-+SmE<GT|Yt(1{aB5f_(HEW-FLWwtzsAQpefeUj=Z-ve4<oUKZrPMRDYZp`mO< zoKnW@LQ#;y7_F_BJO`uCGTFs3YE>&lf+Z{b@GL738291p5$*)u61k+J#*r&T<Ih+* zl~q-iv3x!Mz<q(w41&?qpwXw7E=tC^o!w{%hQEnwS$3qeHYYNNEG8?RI>}>=rS?Cv z*by_tMJIA0Ul2QT9E)yPCnQ)~JAE%z%+LpXM?%D&oQrXCLKWH^k9#+WyLj6YAEx^{ zlMxzJmZFfdIgb0*nc7v=rhb7wa!D0#K&ddQtM2RQ_u+$G=;O52KYV^mRtv<09vD2k z5|<!3UXk#fX9m<=^}IWTuD;$|sV%<symdnC_Sf09X*S0}#8u17n;A$Jgo^)nYesUS zQpYK*j*K2a2@A+JHi_(qLSRJU=~wA%*UdH_lU8`5GI<dM^j{Sc$Tojtzd~^NK5ayg z+e3dn_pN?!3&KT~nurew%Fr6f6IwPLZekeL2Fg^}NE_OEHSY@ZOXb7<g|sd*a<rb; zI?AFOe@T49F+x+}m_VQ2ML6<D*)PZFFL&i>r4k(r2(8C2hrOK^f-aZHRF-|DRf~Lz z*>mrLwSW8Yc2als_I9v3U%+pPYn&)FX|I_%rm5O=@>ch=PJXC#r$s8g55f?2^sH5w z_fzhqABE5VA3iIBJ_6yu)$#iD>cFvST#sqU=D%(5>YuiRpr25ilA+8$LQ!Z+{PDK* ztl0x<Gie+M@MScN0GGk3RzUo4rBHPco{^5JzjeIaFbW5LGMNxQ+i1MiCu2Y+IvszI z|CXdUwT^{x$-5=G26d=~RrMzP7%!L{TV|?4T$x}tT%JyqDq&w<8|(g+hO04F7e#(8 z!%hmlrsmYQw4TPEL|=aBz)d|J<QP7sRvpn$Q8fVn4SAgO#A%r7>f@ZE4J%IOH$nQs z+VdtF6i^RLzlH>yzJ*K)$|(8olNUoIt&=~x8DC$8|NW-F?_(w1k*jg0igLa5T8`|~ zU3p9WxAC?UpYd<h9Gm6aBAYt;n7NrThE>(O+!s1k|L}Q}lxI<a(HjLsV9Fd*^_Xz^ z2j{C`s7yIyqJ{?5Va~&Y$oxT`)K_#^MA95uSkvoVB95-~p+Oip(DJd8kZ@Tekr9SU zm$ot*joD0}at2tFU24Dq8?xg&lf}v8fRhXgq%Ho11R!7>#N-Jjozce(8nSg6<4HHK zwnzwo2N?Hlj%IbX=hDg@Y`W`^6mcYQ<Zq|JLPrdWyjwb$QnP<E#dhMn7ngA0DAbWf z@$|KA3&u$oHO_#8g~4kOO#s3K0x^HFK*rYl<X|^YO{_|JnALO^+<N>>ppcyS7ukKV z|4J%;!N-E8#mlD;^QS=vWfQqEc$tjCmcy(+bi5l8oQBhVi{}8M7Z`3Ut*za+DWKXI zNrEr8|Kle-o6ZRcO=m3uD3luDp-GtjvZz55LvuU7=f6Mv$vb(=x%CNM9bwtSt=Ll? zJ$Zl+J)9!B#S?kNtHutXV_A-XZ#9A!F^WXxKc|G1lZZqWeto}F4_{y2V^H4yTAh49 zZ+D;aJ$$#yE=j)P>lg_Uj9ceY-Hdwj7nq(RtIuakpyN9diUe#G^RU8Ct?|57cu-NH z(_<6h;R1c;p<n<CHTIa3fx(VuGuF|AdPHO<5#oC^Uf)4z>`sn*MT4ZD5(hpV2&Dws zGlN1tIBqRQ57GfOjNvSVKqeGN&)O2d+1_Qz;CT>m3lWwDfev#{zX)g|BLDF(+tv*- z6<6;p2CRRWXzxf+v6!T_h^}BPD7zhg%CLDMl9%RR{e0Cn6Yzn~S^_H7$B=)f4e*Dm zGy|lgqoX77{%NT47GXYO*A9z-L!fCK=`a>6<sizgn(O>Y=%nPf4myosf)aO7L)MvU z32!8X|1Iz_pL;_}lO3{}X$2Hr{mo%Wp)C3Pjvaxqt$)Rb`p}b5fG9V+K)pn@wG;o0 z3YyHWX|eo`Aam?hA;6DsVi&<2)mfWp=OXBKb!;<Jt~r5q{YYV;h}xE2SJlBrH+j)- zZ!Q(D0&O6HL`lD<GI@QthIRi<Jk_}z)6GWH3xD4V?Lm@#?EdIM>~^eTw_$E5>g$VL zkEi;}%vwo}+v1@ufjN0Dp3|n1sSz9jszW{iy+O7c4N4sB&xC3Wb+8Z#ghK*hB4vk& z)&MzpTL0n0BvCBj5A#0%G7ginLceIb?q$Dq>fqyw7aKHjAmrgl@n$qON0Kp9xj`<# z^z<Abo=q)+w!WP8SlkMn6Kask4IboDlX*e%U)#7lVCxmc6%t^A7Lo~l9YL20ynakZ zA5)JVl5Ev({iS}Z6}QKa9$YP?6Bz)C>|<v#K$ylq`)KJr;l~vjAJtrqQg*WM)b&>l zZJ~3m=idY4?au?sMVq@`r_R}lYvZEhubzKzzb(`sc)wUyDt%@oYRZA-c;@JQdA{li z`}fh?gWl`c_m}OSx3|TcuTPtf8>D&L-mbNChsQ%d#+^gZMK<olPVpn5Bg9VO6({)2 zbyFba0CRMI%B_5vdZ^gJoNw^cV#lG#$aFgY;Ujb_5MH`LsG_PoBq=iKfxqdJJnad` zB!DKWg8j=Md%1xTBa}QnUWAUm2Lud~!O>^a)3iej|CA}06H?-8xa1-hs(S&ygC^^w zry2c|jX=}(x;N3&TXk$^EuH7{2)Z##V`<nNRF-#rAna#(Hy$yX*S!L9yw8k1R<O~# zVml=hy1XHsRkNlhQ_u1Z5!l`B_it~7E6l}dMKdpdZB;uMIB4)|euzPqIlRnc_2?1E z6&DO;&&_fz5-(m1pL$*xrgfDFnGt33Tc|SnkNB~nyA4Hy-s7G7R6)Y*h(3MW3?V8Q zu?V42z%UR-7Xu3yHPa&1j)}8gI$pG?=_5VGWk|pvoRjG9WdHc{k*ADGg5F^2==NQv z|MXw-yxtT(um{P{b>0HZKYR=pMPL0a%5^D#^1JY%<ZW}@BBxA^3spdWvK+-iyO<Q( z3VU5mIfk+KnOBgLjK%`+8*#taAZC4SwKJ=<4Gvjc$qBU&`J?ZTf@Bdp?%EMbcf$-t zS?{OQDYQ|MWOO-_phI<y_hI1X$%galYn=5j2CY@ZAyi6oCrqnH7ZWYA>v6%iw`KLE z2m}VQ)6K{a;j3hr^@3WWO$tC{A}m)~U?{x@T0l~KKB5Wc^5itl|J3%?L2U+ow@7ez z0u&7vqyd6Mp}4yQcbDP?N+}ZDHMqMKx0d1*0;PC?0>z<71xkw*zR>s1H}}2&-~A(* z%w(Q<_T+4K_w4gKr=5*kK=<ow9mi)l6|8x{+!HQYQZzz*96G8zSypS=8YqAi7ovit zE0w5XPrNr)>CU2}1#?%?+gR@X)TC`3#r>W~5Psuil#i|>`6y13Fzk@leYB6ccGC7= zeq5NChM*T<XEKhT#=P$IlQL#XjyW~mGQd2mRV;H?FWxmhCUsY}T<W_@a6ABlW-ePQ z8{;p&!4dkQ)TrtDzV`Q8(p?{2;(XnjrId~smm&iiF69#@qP1VPq9~f*)f&GXA+g@9 z8;hP@3_sv+nYR(bCo5}E8R#W8`uiSZH&XZGNDNNF@Iki^Ch5LITtU~^xKAI%J~1h6 z?{&C?My9?v+7iCr$e23&bDHB*idUzqyE7xU7+FY8f$Y^TBOyCUgM~$hw9B1$C+fOb z0yJi0r!$k4h=aC7R%R2tUw}HYR?#GEY8oNi!>sZOFArrFQ?B$x^fP9SpJIacx40Qu zlMY3Y{-b>7ZB)#nH1D5Ps~y$)Ekvi^<n+V%Y7xnlNUfUFt?`G-os`y$;@FgLhAqdD zOM8w@{EC)&e}6xU>gRoE<{#4624;lz*gOBF+J^zS|E|1<bz2A@QmbI`R}#bMj}vNR zZYW7)titvibdjBak&e&1fcE6cRj4qy8V;@{4&?FCX=5~n5;P%YJ8jD@bFVr_{At() zd)+&_!8T741e+Y!_hABN`NaZ{>xdoMR0drbD<-g|M!h;SoH(5Thw4irSO`E6GcUG9 zR^;uC{5&$duZih(*PDoM)Fp3SBiPC9Qf!=ExBKt?RGH^0bdh9W(6w?Pt2Bdi_x(*> ze*77~XnVEHV@N#2tAq#p;HeKA?%rS)2se6m-ysPRsP|~SzN;GHb@G7Ex)h~E81?)t z^irAMMr!1_^oG5;3?-Zlpdvo{Ur0QZ|A(R3qw8nr<|&ViLXgWl7@^+vQO|$NBSfV& zyE8?(vbyVm2G&slbGp`qEFi7<mcju?R5b71*K;i;zpY6X@6QYtynKjL{H*E(EDjC< z1&_7+lpl<Z;l65kn6U9yP9``Ky)HJ_?hGp-#XQ>e-Y6#JyH(=Nm;0Q|ik3xX?n^QC z`r!}TWN4eoS0wGQcqPLs1sRtMwnMrpr|Xm&<EgeDA$erWM=!_1`4K)Ve+=pHr|67Q zSuub=f(c>ghSe}t49GggbERCks=A?^^aq8IybR&$^#MCG?Q+AP`-u&K-XqT<0tDr3 z6uD?X5BPkSZI_7qJ<LR#)`}O*mqrXfwowtoyG}`aJQEpCM1Z&x=Z^6w(La0JRFt>& zp5L(1))u3O`_<0O$V=irQlJ#o4*ko5yxA(#!LRn;=<U4NNOwJd`?h_FlB(;=O&;G{ zF!1T=VW)Ih3}FzPd}X7pY8q<xYNu$a;(J2HLaFeAkHi;w?gixTPE<&SW?Yl<dgI{h zoAJLI{XLqaT#XS#I=$q3z@-@u_pBgDA6p+K?yy_2#L|V15GK8k4cI0~5&k*pbItZA zrT2K-0<S;v(tHZHzg|~spIAyR;)A7DNa(4^#pH52xbeDd-$`ZDsOX$)Q}5{p%ckzW z)!TSu8HliPmNjV-fBVK%o*tE7e6qCKUz$?nC@}Nl(E~pB3I!5DvO`w;d)Hv=BH55^ z{oME9mYO?D3*mg%hfP*p9iHCHs8pie+l;n>x=OL%DVEBK*KCZ2X_uKhTowt9Xes%8 zFX3Kgc@i9xP<=z6!KsK1wmp410hB@bPhVYr!)K>N4i?lOCKm03TNseO4a|1q<>Yd} z(jjwlM_=`;jC!7_6#bNhx!ogK9N3!(s30+!I?*oVP=V0^N^+6k=TPTuA=I5ABoW`^ z_4&)Ql-C!Cx|4TCa;O0L5!8xD#O_$<Ptx|6pr(@kd|eOaz@!|e_f^2M4k1CtdM@=$ z>dIEqazh@<1DBxa7~F1!l%3|x8eBbpJbj?vP(+TIon+?hckH%y?hKF9r1*jtN}YZm zEd&G$_}w1xxsq3qc)1yBInM9?EXS}{y~Bfwj7X9xeL>(FbWKRuVlgDZk)&Em?xd9u zV<C!+io~TYQQ?Y<x3xv)3RFRzfwBh9TsO^Z_zj9rIkHw}MO;f~!nkT$tS7dmW`6Jh zC&J<K&=`(jK;{Y;OBDkIYvh+w`7?sRj@1kLvAurRb$*456fEVuox>4IvU;9He7{fi zv;6rX47~w;#Opl-F>W$*T%oomB8Q{4M5EiW;u^v>Chl04jl!yiw+%|JA%vzDI2zA( ziG+iub9^`UjT+0pEKVxln4Ug+{<;u)g>>j|WEhne^W~Q~)meLqUqe96hf!I+wFV>^ zVZl`?I~Tmx&sY$w?_%c@%!(N)ch<J}TG&LDrDfD6^p8J^i4y*@OSHE_jb>-AJ>26F zZ!Wr41Wb1b(jPqzI-oX0aY&RQKfaZ2r?ekIyDUhMhz|T!niR>F?n`Uaf*^=Ni^t62 zB144b>kKBv5D_pu;*4%-c65o&X&biC&}gX%BUKnp6xUbE@d7L4Y{?#BW2u}+)RaSF z0bA4Xa5*@WC1GK`<adLt9`KuYe<hOeV4vQUl+~Q%U}dpVT8#wcWRKKTxj0!IY}1zb ztiNf7ep+bZ>!<M92?*?46IhT*MH@H)l{x=W5)s(n`VsXsh%-;xLA`waMGtC$i89*W zCe5jrTC~hn=u47{WIrdAZSdnV&|rCQedozSVu#$!lmo=bhM+Y+0#ZME4CN`4uVZce zag<+^d-s6PrQCu<NXbU_n$@d^c}j??1EG;&cd)D}V&0pj^KERI&MrT;OhzUez~z8F z7R#ilAxHek)25_K!qT~tW6{qox{4M99bLdHLr)xwJUgu>Cl_yM^QqP$6l$(i<3-Cn z;Y!GYrh``MnD-P&XOp7j4Z;D8QhtF`k&08rngK>tK%C&R&S+}$1@yX2<_Siin1rA_ z`gEkyV>T@Hjv*<GK2wibo6XK#peLzhI9UkuLG*n3f$fJcIG*~n(H5M)F$y|(`}rGX zrkYT3gZ)p9!4}g_?+hP0^MAKHJF}CmBB_tO&+7wxs(PLgnb!O2y)p7ID(m5CA=X23 z+nxK);x5*Gp5#d{zJFhgl8gS6-=zFY0*+-ij`r_jJ0JgZ&BZ&zn+{?As>13<aEr0H zMpy837DSBsTu3ex5{!+>2a<=RU{bK^l0k1C*%4DQLXIhX(XHzQ{_^h-L9C(P1a0b- zTM!9+3Jlo@NGX~Q>3UuvN1mSnJ%252GhL1fZVn}V7S7XI-hou>2`KKQ*9dX|^d4kB z;N_s22#?0Z+9$>yBZ?TKLJOB&_*8Z|&|p7LIX<cnntZw(NKP8>$Q2g0>4lF*J55Rp z<)>vJ6u^QCkV*@PX$x_$Kw=M|%|U`4T(szk1bC=0Bm7MCGKiFlfrig<4ke&@d?kVv zJz;AiWk@_KJm-*Eg)~-{4P6%N>|<$cDDb&uz}uUSAiMZ4G7tPYbJmc6q8A;avFqg8 zRy=Mr=ycOQ`(~$ud5Q-gD}^AaZJ9D;e+HX!ri~>yeY^`bFJ4FXf4pB0IiI-s{zszo z#x;9kZEh}%E-wtxAri)nA<f;4Mg{;l5&_0IqZO;XzuEZs66jtj2zkG^gy?4^@r?YQ z`>-(<x_Gz%6kOYid%3;ZmOGZEzW!>h;%;%w^}NH=Ga*+y6!ypH>@@Uj1L(RDXfw_- z`|a+wZp=#ay58|rqxq9NIF*!^G=T!WO~oFKz*#U?d8!-sK|xdOk~Yym1g!#Fm@Zuh zk9lloDzo9wG@&lD3RDV;Ay8}x`iilnLE5^lSOaMAGN5wU9Uum%DX@zXNTBmV+PO3o zt%2^Ka#|`+EqROweC%|mBobxv$1zyKP4fA)?dNdNeoKL!2;QbUpG+rNlNsI_f&N6} zz=;Vbps))S4GO5+QFw>#AzDOse9Kf~9DgW%l43G4lQE1?QobT&K204s6epEN6%zn# z1Ni-nw#Mm1i`ez#89A12y5TMB`crzT@oU%Ol>}8=RgaJ-?>A9#f(O#xEFcI%DsE4Z zr_Im=m#Z%9oWCQI_r*bz9p=*fUEWi*!GB&99i6oU<6Q;SKU5IZ_;eh-_!4#ZO6HGc zkx9&XKhCGTju#ij{;N5&`eO`^u&p7XVW#`Pua0kuL~FVCYi-GSzZ-6qfKE2MfHBKT zFd{?C6FISA9%n6JG}yY5{p^U}8zF%~XW~#`6N8ziUPS5rfDgYUR3eRLH4u$em_%m- zCjlJx_`UkDy<9r7t8NY>u(>UzY^|)2vZ)>P3)Z1m=Z_`P=E#<7MTPgcys}6>m$5WW z!z}A#J;>d$)o!0)xmjmjU*jY*ikN3z-u5MKKUet^ZF;VQnM~cs{ddp*GkEXsfsq8L zaF52|cYUR=P^#yBgQ;*mK0QH#DMMZMTwj9Tb&M-p=FBtGhwuOF-8rzL-)MPO@X0Jv z{Z(<Saquvi&sFX@g-`*lMuQsXxW3<o@=CP6pBK&)@o@LZg2WWj0COxldP$94IxjZB zTEUtaYYa0?&ZaZkk<aGHS_>Nk?GYq|T8K~wkCBZICTMKep^Zb+J0@Z8^*s6(0_7dh zqFdWgQ+~k5KwCpX1MT<5+nn&qDp+XypzJ;dvrU=2+B2cMMQnI6wi3MZp8UCO>+`e4 zKYvRMo36VB`pu`dMZQ=dswGg{#gYWwDm6R)C=g^zRT~@IS*7*bGd*+ASMEz`Oz9Cr zd5tIL0*~$l%|Q^J)W-#>S-np3OcqPHm^f<*COUo)LtZ6x5es+3Dbe8F1r9X4@$>il z0-1DZ0Id1IR#`E{Iq5R%<wwc;V{Ox!gUYMzMg!7aAXUpFYl5do4dTg|stg|`_xC6{ z|Dd28<LO7eTD-ciko<msPQ3CKTP6L8vCC={EYyY2DRASUBQjh}53G5P6RRF<+wyf- z6B~$|0|nR&35*m}(V}BvVyjs(HScq9VBnG-+C)C!!>wo_VUCW{SMw?jwgysF5(i4) z$1!QCUcol9_)1SAGYc0o!Cc~`*sZn{_=D#A*4WGwuv%I^zHhSbRaUi%4Z@C$Jy|t- z03W?^pkak9moQ6-OGvU+^4{~Wf1-kt%qxe9K3E`3<J9fMoJ7^50$2ZLf9fzY^DHt` zR~0W7SiA<Cgb2t33v<c_*9wqDMPI&r^lw3R^Zx$z=k<#3zfWDY);k(HrcF^w=$*x# z8FJwkdZVwf)gADw*Y^0FL|-?wWrJbz{$ARR5H+9wpN4qg>gCy}^Jt>W9?58~A_7&P z+CZb8SZAm$G6Gg?-vso|<7+l?mpl98z>ppBIZlc&Y!Wh<-M0NY7Q2y;BBFc!0Ur(} zbCK9rc>`z>@yungdY<;5g8Ot^h&$dNGm^TU279ghEeshlEnF|5+Zsq$eZIJ1aFJqY zkpOYWT5B|D8XEp7jCtSsP&U;WEgBQRr$qqU>uZf8D%`$P*z#gn*n_`z`*y20ZGi9` z_|99`@pt`=XT0Zn=<=!YMf8(&=g?xJ9lE@J+S4!h8=kx3Ts@*uTCcWL25fa|4O~UG z4z4pL14?=fUtGQt%<M~LpQ?GSCX@~9;@vkm&(wP(;=(uh11e6aC-e9IaxquSAiMto zpFe*m!ClV+vQY^V_xHynW5s@d3;Ol()zeWB|9$o+`?(YQ+ZI*51VdG3yKE1S&dSxs z{kx^5!-zm<T*XL0h4=njZV~f{fAvjFiXjq1RypGsmYfa+3OTsHu)mY38<4lq!nuan z!Fk*02@Pr5N8e(0A(M$$65r>K#p(P27?U7viq3Bvh|vnjvcV>;lHfi;%(2cRGs4rm zSN0Zb)MxUZ0~$wbkZ}1pd6C>_zlwz3i1POvbl$ukD<kes6II-gt}R~1yU1Mb(EKdw zs=(d+v3G51b<E)>?r#d?dxM1*)vp1sP`&%vo3Zo)_5Rz+jcnP`M0z!7sYmQ$BX^1X z^rah9pSH4{Xn*c`*DGUzzRcEBnma2L?KTY7uZj#Svu~?D6qo$n`6JVRe-&!{=Z)~G zMcy#~R)b`t@4cjd!%7El+RhVN1`2$7EbB-)jXaG(p`-|@t9n^k4zP-S*_iADe;C9v z!OmEp1JU^%o;noCrLWdGXpDo5Z9ePFAIoFQrM)D4Lc4zh-)*dL>fNw@I)AJb*Ptk) zuQmG~%%GlGdi?~=hgUuafeE7HP(6&=Ye~wo5wY)5t1ZmZDQohYYSdz1RB`>h-wi1@ znm^(<W7}s?-RfSPFJcuL%<VYuzL;{hy7K<+x<PsQaV%t|vMq5t?`jNnlxG~YW{OJK z>|iqOMZt3~a}d1F)1TsT)zN1cO66U!`taW^Jm&G;;uZc)lPI=2WYuZL_J^q}jt;ZO z1b0xPoCG5~t%{@PYi>jYZgy|kk7x$C)~_C9Zs%Y5ncF(X-37_HbI(Xvo+CVaQn>+k z<w|z-Iqr)(?wMn1vPtcnmxN#713ns127dEzJEXVn{usb$7Lv*iWz5$3%D{a0F7~WO zx$2X%Zju2?1G3<%d%72Ai|^lcG`exel~?!`2<tf4e0?u3jf=4@Mh=l7Hjo69kjBY1 z#y8(or(8J#?eqHYZ>^Z_Cl}Kr_WN+yhzC{TAdmQ%>jji+>xr-4A@1+0UZ?Y<>vzN_ zGJ>IspbSGp0&1nKOWRWJL>*;nJ$i-!BRUo=1TQIua;I$q_u1u$1OJkHl$rho8Elxw zYh}UaK2|wzPMYQ>!Fgc7{s$N>K@JZa6WwmmL6(-_rA=<f3>F4AGN<zBTfq9F>444% zsNK9+WoK<fBA4K-&+Aune;!*f234ty^|J)*?)BE8(m<;u=52A39k5+pocaSk$zlnf z5R9GC0Py})$mMB<ZLwEQPVNdUw_#*=(p5ky>eR(indZ=W!F9JAj40vA-6(G(`{Evi zm=$_GrR$z3AM_n@aznMc=%63g0$X&z$J=3Y<!#FJs}K4V5B}d%*8h$@z^ZbbU54O} ze@;HAIZ}l)@7ZW9sSVorHZ52Rux5@qw9o`#b=RiG*_f&)1Xr#+GgMUsUW<S^J=6+6 z0n__WF09k8o~ty-%up^+eiq~3cl#L!?O2;A_s$3AZEI<@f8Cz7_Yrsa9J=cG*=*!9 zH%VN@%-e;L>x<hvw}U`>gBblxzn8D>&#_`kaU&k#fbED<&1}IjPtT<VcR$^OzN~Pp z;b-0b+kaK_4~;<psC=}h5+1?Fl$a#6<p?EtR{um*cn|MCeWTk;&At<k#vzOE#L20$ z$_0Qh=#WA%OpNS5v!74>l!weac^BcLCfwVyE$a?!I!q7p)?1?|Un|P~km3Tt0)?rx zhh)*1?Y^`E=yYXgX|AY1S)>C8l5kowUI|};nq$y>-1ViIEINxU6?fL#1`-ZPg%O_Y zd$?a@QZH6h8or*SN#gZO<F+UD34vE)?o~9**<ryFtNAR-aaHRdJr<9Gj_3w+KYPb@ z5dSc1dAR{#ce!rVrkL3>L{Q7C5?hN`kK`NG`gc>mphMKLkB}9#rUCWz<_cUyyb97( zDdOrx(&gW(O{?LtTwvXE+<CvvU4CooQ+~Mez@KaR5b;}#&mS6o=q6u#l%+^jd{#Mg zK!posI|Cd4_vinQcPLKPE}!yq+B&ckEoFF{zI~p+td?+K=l0BrQfMAVMGuY7E^lE> zYxd@xIii*Kuev#P+4U4_t;<q|iPp$h4+9r>;U%AKsb?CZVyoRR<l>y+>ASK&1q__q zLiC7t>}ObKvhad2y`(14GBQWI8%*R4+<3XvBMXgZ7I_4z3*+$NmxKe4h#(8~I?HLf zStfNoxhQc<y(;zWM3+)nnLjZim7le>n_c%WS(hpu;~D5%t<$UYxXKGh6=sftMxo<g z4eQi*G-XXv$mtgf)Y_mPfCXUD249niN{Z<i!6S~c<WT@=G9)b+#^@2%{i}cfA-f=c zOF233ZtTPI%eM@qSu&s4*)gP{IsQ{PEB1$%H5#>bDx-CngsoS)?f4UKP@YXV%87<p z0YFQQLxMiAiG|}jZZc_sm^Kcwtf=GH!PPQVMoY|F_`N%S8cT<T4uQVK&L#j>^%2;k zH5-ICp}WMDVAI*^|DfXax1Hq7kI!p1qMNQQPgCJW>a#ceDB<d!EpbLy%VOr<FT(#o zMapbl!~F*EA&0vrOR5MTzy;G8-&xh;q5;#f(yPh}Lji5NWdH)aYP=2r+wP7Xzd3k< zXz;*&NT@-fumXDt`V5z-lv*x51}|e<?@-v@^_>Jfb&71n<`HAOz^qe=0*ALR2&B)a za4?p%$Mh&ei>oAAy$sxpb;J6=pL=Df_%6mb^0(gSQCf6{0%n?IrJM|6S|AtS{lvI- zi>!|YoH|@4^rMWa#5v5^stpng1h&plH5UIFqxq_%F`sD9aVz5ugX57e{{3PsxEv1Q zXzw_SDv3W~4J^E+NQsRbbJ)5?*@~8E*Sa{1E$d96`<4k@w+U?>jt7`FZ@z`eQI|=> zHsUV8RDgMky&cpEy>|rWDEZJRnF~s2oU2F}iX|`jWyiZ}$_nzwXyA*@%<Fv(Bdw`h zUW|tOdau>txP!YN&mM+8C;Jt9-bfkqk?Lqm$yvbWDcO!&28J4USkPDEph~w8$I%|d z_bhwKW+^r9WPBk7-7byIEQ>zl=0{OOND=*#zloKCD@bA2zNjM_Dd=k9lLve*lqkj5 zQ2r#huf0Owq)_peDe))6;@QrU##-i<*A3L^{O%N>EWX`>!y4>VoZg}RQKp+kbR7Eg zxj|H;t&99J*)eWq<eTtcT%fqowjrdUt;(h!M==;mtmg85@N|g?VDpE}5Dhn<@hpS< zN*cF^^Su_4$`pZv9zfRp8Er`Ho*Ij|IY-p4dY*Hp<IQ{4?pQmU%k`jevCVy`Yn&%f zul_d~cK<kA8$qnS!KcUZY02!LuF__p!2(?N`mtX#_Np8f@kw+G>Bkrf8y+QDGK!1C z6d$LsEW*P#^R8`fKdUEF9+JKt|6S5S7U!|OJujk}LQ0Jj*}#@=FI2&D6XAqIGhC|X zk9m%vjEm!qAOuA2UpBc51n&MDU*F{x9{hinak0U~n7YecOd~#CIquQw>IDX3Ml7sK z&B;=O$A+Ye0b)9^vrt>9$dpCdD@@!vZO)8ZO733)m~pynP(%3f_G~n3!){X9(Q?1t zqC=G0Ydag`nydqP9t`m;cKzGT4u};=#?Oo^1GC2)gJwRwc1<i?v)%%AKHSr|npKrJ z!HlnfWDB!~NAKU~`%3gmfh!FFe^+~YO?+ORDs20S)+301s?$?M3^nrRO0JlvEf@*Z zQ|rX%Dz@DG#+$08JUOHNaFn^`ppd*6Q%^K4e63WusA5;6q57LDgd#$Th?ACKri!YQ z;-v{*;(gd=q?>uxC)FC*?AOuY(w->OuL=Po^@eyOA<rr6D{f4rBulDUrXKk7TX95u z6XhQTSnhU{A^4o`ua{}>X|CnNS7$9!U6Y<pCh#n@!NOu@){E>1C&#)|aE5^oeRAgK zdkiJ3<uB}_RUm!d^x)m?%jmBjE;?5`1E!I)bIh6@iHJP=3th<Hj9Hg9Vqo$CICii4 zV<z$Y81~6l)z~09<q$l&pJc<-gQc){g|f@A<^ehKM-5^Oq_ZP4yAoFQ(^-RK88+gD zq@4~DeDDl;d2^Cnuc=jWeefbVZA)Y&!@x%}#h^_51>d0CT2)<mYDI2gtpS(Kih1?4 zWBZd6f;R77UKQSD<vRWNqYJ%sNxIb~R`HVD1p<<D%AdW_47iOr@&smY8bzxg<t}-7 zMMYthqV>qzzSbls@0oWju&jlm762%HOnyOpw+`k1^CY^U@TFy5a<`*iQ9Ms)H|Msr z=E>Sm6V?WH2BSqhq$<KBvZ}(MF?f%+NT2w{6ewv*mlREnnMY1O8LC82K2lQih49BO zdJIC`=|Uj^lf?-v-VEGW^(be?IDbf49OzX?`I^qEbWBUmQuD~__H<Bp%}o1>WN`9O zE-bo6|2WF~N1UythhaBgIx-bIoASnv{#Wk)mBL)`DzPyq>UiWB-#6zZMQG8qDiN>7 zdFpYsAs@fJ%L2~K4i`x*DT%Q6VQW8~qPxg}uv<R*_E+Yzh&0=99?U<-i^*jc;$0j} z5O$t5VAkV=TVa6HnLxz`6XiEfaA8ZlGvV|i(oqp)chRIssMfsX&oWOmXms(<U;RQJ zAim3mv@9^>amVW1xk{49!waIlH!5{eG%!pLOhaR~_du!(#(^mBkl&S9o7a3>3~|`? zhwz5o6yf9H`F1nkN`WD%?!-{M{6sm2sL|3<V)z`-7?z_k?;|TMcs%@PhVGMS=DL}S zxsWplC+4wjzkzZaLHdImjwu!iY2pqTLS7peyc6ZZuxqwNxII?L&qK}2)%8^X-dX`d zW*xlm+3itleUv)gxlsGu*D=sJ5C1uq#3Bv2`nXcwb{}}Gs#3-B$4GP%hix>Nve*mx zjk}l}(xTL6i~L0;G>rEF<>BQ4t3+9ww*Q?ltqOem3OPNn+=eC9f=(7x%x*>hVUp`< z_m`@Xw=s<~hpGq1Qw`h)qBErJyAS-iRjLr*(n0zA1yItD3Q7ko|MZ%k(l14h7L3TH z00e-53)#2T@<~*rH(3Yl9;O-00dvBaiNcklX2}E;cJcu3cEM?(DD+Zm-CRIvIH`qP z3%~g!TsKrTK31*lQgvdr88*9z@8XEZ*IP)$rHh0E{p2RMX7Pfy{Z1`PD+W5#T(giG zV@kD=8eh?OL(v&vI$JqwfS(Fa?TSk^=&^Oh&2#I}spQhF&wMFj*_UTHY){+>dMRil znG-%ku5jBnd-BlnU92Vb?bXSqtlOe166rlP<qhD;BCUkcPbE^|JF(yGO{f8Udd$j$ zjurTAiC>|5xWk|##Kpje<Y^02t+{2=Y|NQseUgbOG7c%7NJd-xOE)PN?*I6&>NAMX z;LdM^Q5M04<0c}I<AJmTY6VX6Wt1C<%B1L-F?`&kePUK~!{P-a+=d@j%WYtiDO;8l zP+v7<&~2G>f@>0*AYQ%ri50fk&q^8|^v$=qHW+e*ZC>wXluHJA@quUsqP!>kX!e4O zoip@f7q1^v$xhTV8kP(@>X`hhbZM!yv3EwdQ*$8Gj#}=^J1#r5a+)f<a%eT3(I2uv zc%Pak`MuMtG^%-<(?Jk2ZMmEgHp%MapL)ur&zsio=8(`FJ@%E~XH^90Gd;I5tkO<e zI(y>}ykU05!^G1U^X`n+=N{8t&aaB()fLs%+%cjK?VPO~XL0SS8p2jAh|Of@+9G&r zxL^|6zOvsQ_i^_Imd$2BoY^S-z@J_d4e<}oOH3gz6?O^Ijn*^5n|PNetP@tDP3nQs zGpI+mKAw}ict(;u%jS`_`C#EZNP)+ACM3$L({CM3y9CqkoT1t9NbA=b3;6QxLptbS znkPg8{wE62uXl-VRs^PHD0@c19cE;JynAjTfJ+y0Z3T^P!EQI?MZA8l$;~>wGmI5I zn+{c;eL^+&Lj^ZjhtS>jn`RtoMRRmK?ErX|cKnk#9{eawuct{7J##J&v-|lXjU9JX zf=2v)0)xSiFQOmjYx+x7un2N*Z8LO{meVvPwuu2XgbJ&vb)afC77+-1J!}p#ImE14 zn9`yGRX{T{tlE0a`u=KYv(8H_GYY<wNWtp<lBN23w~t$stzjK>pgOKe)O!1XkCOpN zd<Jcv5iQA@5fg0|VmxuqJ;D!YRN!Uv`r!lt&hY^bD|L$i?ZrSv-Iw&=lz2<tiR*91 zLszG4tdz<Yly#Ep<7&%>xUtFjK?iE~LMt=FkWQYsl@O`}1s0#6uq#@ACCg!S@)zO= zdy2s1;^FpUgzkwJ%ezPH-RtE0L0*&|ds_rd$}B~9zC{vO(-fQ$<l>rw8(`Q>dG6>o zH4)}iL9g^=E|i<JtRymfC{PBzqJb1L!2*2-;@UP^)^U1AF*hqg;W@1w2e`cq5xr_t zLmHP$kFl1SMp#Ts*qyFA<x@y?v>0kI{T*`Nr2FGZ5ZMlv*DO$BYG7=WMo!=gPpQ?t zVAVoKMMgmrlJof8kJ}TmD5w6_FLNtOq5QFtmV~~uwOg<(UkRPxv2~)7;$5v^3M94u z-#oTNzG|%rlax!B&cMc4yS-VEjTrUou0Ih!k;wLR4|#<;O*|(b9pM^7uoFeg=o58{ zD&Mp4BELyrI=p~vy+<{nC9qz8vph4rRoPo(NG)SmT^gS90Vd`ceSuqPbCu1@`L7YT z3-N+E#E0%46L$Wje;ktXgpwD>{n*#K6rNgUoftb`f&;aSt-Tt#S8N)JqV6!^?54Wc zQ93V_!w^QL$K2RG8I$EJpr-n<PomZHU1DmA?N?<#)3T26icD4$9kZ$&<WFQ<naBv8 zWq>o0vr^i`GlRT<A;B}1^h-4~7PUf9q@KsLY7>zE`f%6X7Oo{In7Fr7{=gs3C-x}* zyr4znI&`=I$I1o;rD*a0HY!M0?W+%}Zco>Y{{+f?1Z3jpkJL9DwE^xU_}TFkzFil@ zvhpOv$*BrQvK<!`&8D$yBKXaJPJUntZImM`zRestZmgcFBv);Jf$~12SgD)OnER2w zHIqq{MgEjjLMtTQ_B8NNPH%of1IA8KG}N30oH&|aHR>pZsgc9Wo<EZ21LDf2p5&+{ zrfUYE<I+>W1Ua8{Kr-2JLCcN42vb2))hf;gT-m_OqfQC(#Kgob<KSh#m({FlKLK=c z9GvZ>@VYptS)A$<eiZ%j(Y;c+lv*p3Guc26<h5E9l`1On1^hZa@?~VC<1-=F-2)+V ztfUbGfno#^VW?OLIoQ{sfAg0t^j2(*@C7+Dz^$SVS&^6YPT0Q(6}@HQUT$ox+Mpl@ zs~y_IMS}8FEAkSCUf=&+v_LiEyQZ?(Sh#{9BRwryDgzi+YgdUoL0D!8+7h!^TwK_a z{&;k^>p<7r9v`b9M^<t<kSHr62xXxI(vrnuIj&?4zZ{Q+x9CS9$x8H&j*dK0H_cIY f#9s(*b>T>_04cDe?p{{=|IrWrJAvf?|EK=}lQg6) literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/agentsVoice/browser/media/fr_david_neutral.mp3 b/src/vs/workbench/contrib/agentsVoice/browser/media/fr_david_neutral.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f08d15fab1e2616ccaa184d805af53c6d91fc0e5 GIT binary patch literal 182925 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�K?2RG@Rm5t^PU;2_CeYEu2 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 D9!$th literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/agentsVoice/browser/media/it_eva_neutral.mp3 b/src/vs/workbench/contrib/agentsVoice/browser/media/it_eva_neutral.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..88fba1b93516a88c6f7eb5f4dcd6b978b8dac900 GIT binary patch literal 182349 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 V?<UDgS^g(-`2R2J@c$n?`d<J`l(ql> literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/agentsVoice/browser/media/ja_aruha_neutral.mp3 b/src/vs/workbench/contrib/agentsVoice/browser/media/ja_aruha_neutral.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..acd0de8515e6b4be70781d9f2d4c7bbe9c83bb4f GIT binary patch literal 185229 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≦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 literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/agentsVoice/browser/media/ko_jiyon_neutral.mp3 b/src/vs/workbench/contrib/agentsVoice/browser/media/ko_jiyon_neutral.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..270633bcacc349e181c679823ae67ee8474f577f GIT binary patch literal 195021 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 SEU)@6DA50X{`LRI^M3%!qWjhW literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/agentsVoice/browser/media/pt-BR_gil_neutral.mp3 b/src/vs/workbench/contrib/agentsVoice/browser/media/pt-BR_gil_neutral.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..98cf005d4979235d724bd4a867efb90a5043b721 GIT binary patch literal 137997 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 P{ofv_|Mo-uf5-m<2vwf% literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/agentsVoice/browser/media/zh_wuzhi_neutral.mp3 b/src/vs/workbench/contrib/agentsVoice/browser/media/zh_wuzhi_neutral.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3397d8fda3bffe6aee46f3f6bf701245230744d7 GIT binary patch literal 200493 zcmcfHWl&pP)F|NKQY1idcL*Nbio3g0G`PEz;;uo87k4OLthj4&C{o-h-cpL(@_sYl zpZoL9+&#%jPIS*R>)2XLL7E#8@H%L<wX|gZJz@g@2+HQ(P(BDd*S{Ml*MIN+&*J5# zr1yVU6`Y|i|5pBe9{vje;q_}ssOXs3xFA9ja!MLHMrIavPHsLy;kV*aGV+QlYMR=5 zhQ{yAt)O-e&aNKbegQ$jp%GEB@ky|>kJ-7O3QNi=t843<TH89m_Vy2rj8D$YEiA9D zZ*K1%9G#wD-u$}%`}F$f|0ZYoZ*mZU|DDOdCv*sA|E>O;M+I$pz<<yG|1bXe3<87@ z1s93}aMw|(6bao&n%<L%B4Uspt*+2~rzLxk#tWz34h~kP81VogV&lQa?nQIpAS(yL z+s1Gsx15(53Fg}-MmD!(fDmqd4{98m3?VqGEAvmstY6}U0Lnh8&9-LiN^JG=nl^~< zoK8?r;t2zQS;Rs+t&e93?J1FKT0rC3V&~Z}vFJb~031!Sv98)|u@fDh>;|Vv8xZZF zgKw+TA(otXbO{8$GU%5o`Rk#cZ~FtD+X^vNHi5WbW<OIlfz%oa!_%qq(@Y?cN(0eT z-NJUrHU@RcB42!P0VJ~%tkPyE8hRmH;tdi<B|<D1L`|1;Oa9GhCY)phi&#gkyfbhV z#7(mhAw-eUvwNzhEm@hlGs^!2ef41xN)y7ESMvpMmq;W}0FtRF3e!{0gn2*hhN7`h zG^tFJvry1A4Oqmm-hU8w;9X8f4JByx$hZ~MbZ1{miwq@=zF_#uWt&;%;xJI$fA0yA z@2$uRkyz!$)bstaAlvFKI_T$nxocnJczP7j`L`zl7jb6y7i#>kpwC?b7r&JzHgz+C z$Kd5Mvwz@FP6$uxB=SM78JKO;H$7sN-tKPi@0Gc<Wkum8pX<Bro}^9M`h;Jn9F`T2 z@qfT`iXDfFZEz_{t&zS>M+H!Eg3}X2$HD|qGZiqn@-@DriepLY1NsW&xeQ2CrD-hE z#fIUi-q3Lgp@$&?&<o4fT!1hn3sPA*W>BjML4HV4{d$d1wrQu#ZDx>Qo(RgTkCy7B z5S2pyHvpV|n&T)413(|xO||RFBIMN$-m6=j&{v$YpmpNhA)rXKLPrCB^-gq~jWy_B z()p=rn!ug*0gezo22v;;ZS!>_=R?_krnzBl!r>opjaCJyo~8<i2`KG1SGsQ6q^9t~ z)5O=F%J+Y@vIq3q!#4`He27R$E2Ys*<GAsi4flDozi>_Heo4F;k7~ccxc&l4)~;mo zg67LR&q<XAbW4K<Vib9h3APOh=>r>naxaUK`~22pTEcf+Z11YF%(!e%cgS$EUgzvD zYwx<_4gzGzA2Yj|c`})qBT!jLi^pTKDILE3srd~Y18J&@LyeWu2zKE17g{j{uFMsR z!_yR66icmFiVihad7zS0W6UGS173Z=a*q_PAF@Z`n{j}wO}L!nW&pu)p&^$al0PKX zu?*Ae%|TU@9k?&Oo;b4IE1QVTW=Jbm>D18{?xdCWxPyB#y=h@N>e<R?qs6mT%62u1 zC*P!C9niM6#Rh%C9)9OY(wTJj@gIKVF=QP#;_34z?(gD+LpSuh;5gDR2>O&*zNo#t zD0FuA(701F+H+y3et3EAw)k?W_wpk0OxEE1sf7->SNeJ4olotywp*DCFzTpVLv36T zN}ksu^;IWf<e)R*gc$UseA)AG;CqU3-H=YV&%n1a<^`puirYN6n#mB%@2u7ToObJ8 zR_Bv<xczu|arx+ATJ&+DB{e<76GOf$6IY6aD9-WSE)EPGrJO#vZ8hj{VGOkK>O(Fn z{4@DttryOp@NT^*qfdQrh$nQ?UQZ=~^@)CJ^^2H-l$1SJ5K+~s5j1YUJL$YZkwRg4 zhiqso*RQo<(NuR?eKoWBS6RY^`;gy+Z0%?$C$~F%h${R89Ec{{v@-j|x3FB2ygFU! zeP_+Zu5ONq-^>4uzdzsqaGU5pA1sBm+3b21_ap?m?^AA1`rKQ-R4qJK{Qa??`L$L{ zQ!lGhSLb^mq#=@*b0F{K^JdWRu0Q$jA1_~j=K1C2;vY=I^EjK9%)5$-tqn_!O!IF^ z+hU+E{@++U*KQ*O(i-ntt>U5iLZw^UQePH$%u>WPHww&{R7n+l+~_t&3{4NjTr0Xl zIk36VqIg&1J7)&z|AdQN+`jq<%dFEx?`>vo7)CtADD1Ob74?(W6VWh?t3QqaxV8p@ zg~qx~eBIEZJpds}R~jW!{X<c~aIuICko{C`bRIIquk?W6G94L8ym;-=*}mYyP~5;h z00R?{RT=~UbU0Dlvg-OED*;K>GoC##Jf=?mP8~r14fRDXS%N&i`AAt*l{8!m>NNDo zL%Fe&1;S;UVlT-7ri8?i)K<l+pwfc||05nH2`HJH6yqCg+7Iu3Z+|E)G5qRg@d-3J z#sAwb%l}!9$B$rq`zD7W?vaoZfnUa9c=Y=sW!lr%8<)VLygs-)DhK5*Y4|CLn+~Tc zBotB+IqH~G-IAr4V0dMoi72Oy4DItlVJSrBoFvTAk~*0<n0yYC8}X|TpR|=wJk65k zqlh<C?vd`7EJaz46ews2PO>)VG*nF@glifp{(Z1K<z%wBEk)q4{kTXkOFa`*zE8U_ zWF7&YK!gNgD+3h?gUB|G*$aRGCxeKKj!_Rc#^f_BcqkrXJ&6pfCo#7}lt43^n)pBm zbyT^Fo$AnrQT#Mf5!3zfUo71Zznj+_Wgos%PoXV|o*pBi<4&Zuvv$jfkIwZR1c@`Z z?MgbEjgLZCi9Q!F8914cINct^T&1Dq<Z=&y{V-`$9~<tT_XnKOHnL)qs3CjDpGB{0 z;=2Rxm?RCVBw9KvjY_EC1R-)db##4UX_?vZ8Zy+w`4+SorXqv;RS4WJD19sj^Pj_O zl`DzDQTHnv(XuPUZC`!(MYw+^12!?97S!jQh6|!A@XQJb$KJ64Iknw>=+peFp68we zwvMq|33B5kB+2N8rgtQA50~g;EQsRNbQKWv4(8+(*g<k+7F+Z&)==OmQzNNZ-KvVX z{l5XTr>Ty@MQMWUm{~W{j4LCFKHP`V(30-AN=ti3Q-#}9QWc>e2?`_;dpLWN?d;FM zD$4HDacMkoFEN;yeXGI5sWQfxDLiW$%0IlUPH9hmUT|Hs2FBaAW^k4*xE9UNwZ=B- zGa7^vZHt@$7U=lBIwK-mzE2h*2Nxcl$MOFhXR>si@aTD4Cxpv<^ZpI{&Kdi07%#jp z(rtFjW?uw5cdkimqUlwZxx|9LMuRLGQ-u@MzaV^<ghhDd;EYiI)yIJM7Lr>uF!!W2 zEN{1euaN30h_eU$&Agbc)-LJ4KE2s`eRfseRCbhL<w(GZvNU7?Nu84eZz`e=o=k>* zWaO!0nDb0YEwb6@z9a8GWadEa_j}V_p7sm92iNOheg6}mUkmz0q-6JmTj!5@Mfb~h zS_Wa&rmdZ)>V&dSSAkCpMm1}pZ}Q^U-qOrjV_M$_m0q)Q1=VP2PS{!9WS;Q4mo$Wn z3oeEb+UHQ6H{(f6^46+SB1Jf;9{MOh9&(zEUTAP&J<AIQAobPK(h3Q#>&KLrTh2du zeqc06x|_9-eDi3@`T+<#%EQ&;;^tSR(R3<G<VM3bG7;QqSoeuI2slpo`^Iwduu9fe zr6Kd9i<`(VZH)vtv`5-kpEUjjNM^ynl&C1~e(@g(%*Ay|ZayQwfXps*tuNfhD@o&D z_g}J%OseYaia!Xe+~ikZj0Db1$uP{jmM&UOYC1Zg3zpfD6qd-a5hL+1wqu}64(KRL zsK{{tJ6Y${`M*baMQ>K1knA5gH@6m(YSZmgItE@(hK`;=8yIGoimE$ay8CgV^;iQW zPmOs6Hq(7>eCo3x=yX+CBzNYx8bESkDjC0DN;6=vOgmyd1snTibE7Lnq^g0?BB`un z7N<#msAy7)TJyQ6&v#+hW|m>z$P~>mj`L)Af6RP$n{T?;%Bo01v_O$>(Fy4km554? zg^VUFMfncQM)^X&`mQee$7;_-el#q1ROHpCnqv@>XYa4Hzk@y<ZV`*^p4Y1(!Gf;} zwlViaRF)Xt^(K<clEmA<IbJJj^CJ&$MuCGdP>I78N?czRvM$4tS{jh1$_Y_{Kn*+$ zg4`)Bc7}mUXD*^gMT-6)|0l}rE{_)7q4oL({tqeHN;pDt5;MAk;&c8?Wb<{|)uy0= zDw503-zQQOO(FwhEBj-PrYDFM-w`&WB6tzVcQXz4lzjeUpB<+YSCjcZ?W>zy!)n@R zqO2+docmyu7$mAAh<fYL=wR6&5fs!l`jGl5W}uY>S-8d+vw#=o7~Sl`SIqpz;SHOj zqF)YQ%&qT4SDkuXt}67pXSzOk4aEvI^o-lkClV4h;tN?1A3Us+*XUm_3H}N~Secic zef4>g<`D`94C_Cx8GFd>8a4_L)Sd*8?d&c_BVG7qVD1^Ks}G^+go^{L2E>b?rWVM^ z0E)Cib#6>l*;FhHh_pB`tA8LTOCJs}fsN^#jASY)ZxPJ^Yco@bbValb-7CPbmYN+Z zWFIBg{}ElhwKs6zarrcHL*S%7Z=BO9p`yf4vnIm1>y<0d`}_?>UR{S(f~19h)&>~T zNEq05nhQbz!l?RnDM=XI&1RCn+SFSiG!C7U>B@l>;Dqa={onrVYHFwm8m=+CDB-NP zv#?W4u~aPIJ-KK?<ty6B*cezVj6{(A?nk<7_2#2}-2N=>)yXXU)x-wW4{R+Y+0Sgj z=I^R<q*1Vh!<6A@B%R1Mbe$D}MHxy+6Z-1&BJ(H|U9pKrbjn`DG>zRfIMrKIh4tJ* zFBGGQ70=CE$@jk-OV#T@1@v+@pY6yqm&--I0TBnqJ!ChA2U_0zJIaSl267vT_u)!$ zPY-@W;*?dUMW~~@gE7*$#pFnB9Vn`x6fV1C6)d0`8Zf6x{k+|Dt7>fLMHCeC*RUtb z8t(Gn;QmUR!ljR|Oa_Z?l7aiAE*Ep}d?G^U&IoUX3SSjXjIKy?Qrv0#joD~_0(n}E z2IkMq)O)MChZd;*`7172&&zMN5~G(ZDF=T0GphA5g<J4~{r1-@RRu1ls$h0@ZH#&% zO7#dQY2`idj))1)+KXx_>?kl_Zt3#kwtQ~5_;XBj<`|(57jOQZY4(istIwI(tx%Tt z5*|p5E8~Vp$FXrbKDn;=wD1o-pTcmDBRq*cBA}pRK>%O|kcjUa1Dn^A@C8dk$uWso z3CwTgB?gB=E%6K!TXp&W8h>X(K>w)xS(s-wd1r9fhH8@2vhGaX)y&#b9?3sXso^`+ z`EU45n_J|w9ByWi9i>8tygNlqhk@AHPJSM=H)i~40_Mdp*@vM3l!x?%*BZmit$QY* zYMY5;kxO$HH8Bz`By$d`2uO|rBOIC4wNZn2j;k7ny?3eZod7$xaYixA$M6iJU;dpl zJp8-geyaVc%Q1|kjE)i;i%R79wy<?z3W&TcE}{l+6Z?@`de%ES`al;~4qs1)!Lt47 zgXYQv$9KaH(7`y3wxJYN{eSTz>+ui_IDq{z@d%B`sUp<n7;4E7@@uc}P;^{m11VAl ztwRzXB||!?B{+%J6ApMWd8svsJ=2u?dWSs$XrbazQq3IRL3H`7m<;<=oZKG_$C~JP z@dg0S%v@9v(I63ZUJ?a*Rm0;0FY)t!N_m3!6KSHFb}Q~@vW)nZO4Nzsx>PnMzha~k z{Ndf-MYh&gVi<<Z;ZlKOB?(GN-qgVgw&NwyNHlHA3VP!QW8}^0ALMzA4XdE_bsZK0 zhM)a*llPS-U3M*5t1JU3|EWABet3ELy-=)YKP>5Il#XR5oQ}vrkyW}F2Wv(>@FQj- zfPm?k8fz@bWm91mtFgobw0dh*;f;bXGftefsg2)u;?cC}W7dwS;$D63rAolT@CTYt znZ_p&V$OqfjWPn=1W8h6#crXZRWtOn3U*AMPzil1csh8QXg#aG-dGCNt8J=qIL;ax zP7E|oG-1vfOwPwrfv!5Pe;E3*=!j)(QP3dtonW|dvlZica-2iWnhs4*2`}xU49)T* z>hHGj<o9PqWFIQ1SF`A1R`P2Z%Jbt#)*0_^16KoZOzE%Hd(|TfrSUK%Qj%dkO0NCV z_7Rv6Hb^uHhI&S>IBp3Jdj06n1a!dqy9e&Q;tG%MDMJOljLw&r)8`MzwUKNC<NG?8 zqh*nZ^Zh6>+<boVcyp<^p%Se3XLRrYVf;{P5jn&@V0;@R9*RSzLK6W~P{xOM?{$w9 zto5wgkrzNbv9^IOIH<2akFvRtK)_GkKM=rrzL^0NxximKV%TJp&y@RbjosGTpu2L9 z42!LnV9F(N^WX_(&S-Ug`9<Y4fVZ;Zn+znWBH0L1h#8towq~HR4A;)WRWUbCHs-@V zM4oOM#ElZUWRTFOK09d`-#4oJgo$aelrqW7>`@zk8w!<4OUl?!w_3|D$_m%)Xg!nM z&ZyP-8geD&eh5fX6<7EsP@%L>GYPMxO91j6fWr|pre2{4Nr8E(OyZ_%JTu`eYN7R0 zJFztGGykzYJgOG^#Af<?x@7WVdgJd`1wIr1rp?nT!;t;t!Gv6`pX`$F15oFs;Es>0 zR+JTJLj>&AHQ7kP5!X{7#g3lN-(QbYn?4wQ@Hu_)KQFgWyE~xdc=frJb`<hOJkWor ziJ!nK(zh#ZOIlGvmb3rC%xb9?I5|1FO!DJ%<3p}uA?*=6LMDb}mOc;;UJ6~&1JJ95 zf#OEprq0i`@}5fs1%jB*<9f~cbDX#CMHtY_2h<g0i{t9&;vpaAS&`sYm*vXE<YF=s zI9KMeWVo!N`?6mKK#~G=>WncAk;1RpvW6&HupvE{TQ^_bOKtsbcgi61rPN1L&bxn4 zX^4x%ElUPuLL8jp1mn|#;Du~@K}J9fPJ0Y_?KWTqk4rc>k=wG{c4MFCBAhir9YNBT z959#!k?KQ>@4fz>@#Kyo<ppf7a#J37gSlbdt=IWEDsU(J<&W9Zp+SJt>dmODUd3QR zyk%u{zXm!XM6tgrI{ek=&VWqF81OUyo{MF1RNJRqxeU!!2$^Y_9bQ$Z1XaP0CqiEC z4IC4$mgfsKbJ?9DAp(v=^m0DRMQ*jtLlRskkdotT_+&(hDxCqUGh@Xe3N#kiWbA}! z0CB^lZqTa!4fFfluJi9bEIgBP(;B~DUhc8!oD-nFyxCgZ@z^xDT;?9PpTjeE|3t+R zp=OP%ebhIrnfo1j8Y#R!tiw@IhI-P#dJdmE%D=Fx_-$Mr@mq^oZ{uViFt9%~oJ0`> z2V#^mD_B8wLyD3O3tk)tab6!%b;iz^$Sv9p)V%@bL{jn%?oO;nKFJYa!eD?V<P3eJ zGL?pqvOOIF#HJXM=>EP1W-=@^6k>+s>{#KkG`oR<<QpY<rZEh?SD)f{B|_?eAKuY# zD=lRZAcMuiT&le_8hK&C=<&GCptqrgIg?%t3*E$~$}K1U?+!));##f)KGv8+2h|cx zspD*i#23$y4WE~no6Uow+Xj6CjM({ZoI~^Ew$G<n@tf&-d9Xq2)bnBY;&g%!8}d26 zJKVfkijshyIi_Gteq8;pXZOiS5XuEcDObGGT*auKgkv4Hd~evNtJ1Or!?O6X)rs5_ zCf3`6w%Pr&CNn7byZmA#`?5`L3B4M3(TmK(sGT<coP#0)T?1R`&&wXH@lvmXwiZ?# zC6-Y3adOhRtjc}3OVqJ|M55zs{B)Txj1*RRYEGR@T*Ne4L>g#+8ARH$QVxa4^$W`T z@3Kr5=1^%WN2lRPX^U4Meu)LJmF^ne*tUY_eM@Ql>D%g-{V*~IUZx{j)n@CbriZ7w z&2*(=Ev`yq;c=I&C>y^U?yN?d7X}Y_XP141_d@OOvPQW$ltN>{_GmyZ&hR>RNSX5l zqOqAeo+5P29{^WLb&=OhkTsS!Qs}b{CeE!CYRNdsv&+iL3p+IkVJZDq(#FA1DfBji zY%1Fy+StC={#{6kUR!!;s500l!oUE%G`mub<-^Ov@0X4{4z~#2U1FzNGKJdM5^Z@x zS57=^j4HE9U0J2l#5}%|rIkWMjRYBslyu!K@zsyqnX{RNA)PS<h~km975fCEX43Kn zBcuP09KZpt5`GvhlMPqVyeuBX{CW;tVTSG_?!0g->>ru(tM0G<tB<58D<3Cd6Zf&a zU&njK61U?{IB3Sf+JV@acy4L@m*Gd9q8|L<G{=Pn7Y?OTxUR&;*^IV%6{H}sG1lOc z4m%E(t!SkO#3JKbq-dhS0X(5~$*v_`m+iI8=Mz)xwd{28%<qP!D&0<gtc-=!5<*;E zXGEDFpV+MDO=fG`V9PH`HLwFMaplZyTo$7<kC~U08J6b@j+gFk=#PZ*w=wuN^GuP^ zeYlJ;9EK7`uat9qIW)P6;i`AuOR%ch_-4ysR%X_haCkUg`&hKXqi+${sJICO)RTpm z?s$lxfh3+dL-{Sqm3maKKObg)YEJGWIh_*ZQ1pvsm#Tk1zhNey{YA4nl?aYdoG52t zU}nZoQ9#WDXKnLfTPnT!m<bs@kOAiBo{$Z^{f*<Pr~yUM!7LH?I}H2GKK$#8C&LVq zUN|i}SbE=VcNW$QbOhQxycZZ0nX|Z^%aY6CNAq>B+)r!d6%bC!V}(s)DvjBlGBPl3 zKhjCFcaw)_DA(j~;(Sgs5!9=Rd}-JS-}kr^+uiOIP%X(<)JR(y;y*Q+Gtl1mSuei+ zG~s8i?OUM}WsR(t_!)ywxvzf|ehhIWgvjt*IbBB%xx3voqm2ezLWbsV)nO28!DR(J zBWc>q%(ljKsCA>jLS4{PbaTgT$61+1N2X$(mdfCnq9sJH)oU|aZDyY^ZIE*8!DeG1 zqVc|Kp_OSpm;NlvYgG#=S&{%pC=dTppIjaNC!K!K8j6hf5jH`{@T*UZurUn?u!b^K zUt`1oOaXJc@h<dyiDHqr<1(;tH~4wW8K4T@&zxV-jCUXJ>RWQ^<X<AVcoR!dqx%j_ z)O?=CVtp#+11X$BEv62|>QhKiPNNR3z0qHwHL6OKMm}Y!7dl~}k38cYAz<hocN}2I zTqSlatFWq46wj%B|0~o`i7nFnqnEiloBGJ7#p_)T<iW8`rV?iyHH>}aA<Ma%eNG~a zkMpI4Wm03=HRH$MOQ1pdg4s$}iRIGV#aU*8g9<DlMY^KZ$0<0fbVy>EhYW)#lft)h z|AU}hX8f?WJjzK}yNnUcGduS-cDS98q;XhVEC6}N`^L?UheRRa?|Vp>k=ol2v}ev3 zYCJ8~8~t+`Cz-QmgKVa+K6QeuxIn<flxWhN7SdpvV!yG53lB{5R%_YL+S*v?a91PS zc%bAdhv5frK0m&f@5toYX3BWCF4Y>OzJQN?<A1Nt{_jl2|Jy4lu!Y7o78qK{97=4w zL(1N5Rn->}3a}X%X?*!+W!T|77OJGgm_D1htm}~KPH(9=tiz9k#}eCbV%kbgZ2|6J zbDGnDvDpWi@LA<r)mPwJgUa%Cc<uk{zKzLBGKk-mg^u&jX(B@3VKe#oF<3!c_kK0W zP=g`6@_m`twv+SR7_UBI^wrL6-I8){z7<jHYH~h9#^K*`!a;m8gyHHi#)h0!1fgOj z<}1Uw=si-%Ge1(2;!fMQ(<jDY`ZJgwN36J7V3`K81XeEYt@J+5RQgC-8NzO4#V`T@ z4#Na_$Q+c9zKsTO6W~aV9tvNku7kuM^#)Lc;uqiV(L^zJZEfXIRq>AiZAHMYmxD+M z?pu$!k$BQntaffh9^mwC!aVdP;K<y}rTbwKx*>g@fIt%9h?i_I^y=P1lRBqg*RX<; zsh6`XdM3fX4Gl~u@?e|!Tw@`+x|)Pc<6f-5l3}J{b`X0aOd?~uv+C}|zj$;^h&gHu zMYx>xV4DGF2+g!u(j*Mo$wrIhXC9|p<fSN-z`qBruYW0FzIpX|kp~H`112a%=uWO$ zd{tR1Zm*`N<>S^FT8v1p!o|QI%w!6<?dentreH!zg5xM@3j|5H)KYFOka^$Hw`kbf zf~PFrn?45;qbdUY8u>E%bbkhs+RynSo<_;P@xkBgXyR{mxGB<w)t0NIIajQpQuDR! zYnAKjPs}A!3w0UqM4FV`V+zS?bad1wKL?Mc?q{VPSO_gnM9}VBS3u1iCJqR!#=4s) z-MOd}oD0d-@JAJ*KeD6g6mNiY-V$pfeXZon(4_IV(0u>J>jQDA>*mT#IvFo|NuQjE zn2yucysagiv$_U&dh1C)SiN*{ylqCgZ5qr^Z|FrUcO;q4UNi1qB_p`gDq8Ml=Aq6F zo<-wpFI!6q@K6?i^|_KU7Tf|%P!I(l4;}wpDbc~ZQ^|$NvC?Bq42`|@vbH@}R`pr3 zK1|U|^mx7l(5A#u<(Cv)(8I<sStV}=xh>FlFL*_D?Guf{DGten9)7OpSJbTf6^<lU zT3O@;&1%Ge$CKn5#LPUVbt<8TbW2e?l78eZm3f?%Xk-jN1l~G+ZMRqE=9*X;^D=8R za6@2gW{s;96)--rxNk<Ci)P(7)P9LEyB!@3yuq*G=VrX@WU02LS9CtN@39g;iVDWd zGxVknlJiGz=tuja2FP$1{c&<QoklP2E4tgE^4V4YXglkUXCKXYx<HVeC>ug9Ok0fT z^lLr>g^`&(DAL%MOrrhGM?4q~a74#?H}tzj`$Q={3ag2f^MCb=pAwIP`|=Yjk3#O- ziNvUigL}B<Rm53l_eEVAXZ%~985UNEoYC<L)_(L^S!7jOY>B-d?%9IYZ=T-%)KJ{j zK*eK(X~_`Gvxk&BjWbjf3V+~^ot(&Pn*!cn&~wSFgoMIpqAceMx5r=vp5#T_FBJ|B zM1-<XpB!W)A*>v1Oriu7B!$CA3_cs+C{$sqG0QvZ_p-T-J_d-|N-)+^egdV>8mo~I z@QoED_7l%a44z<x4rtL7ev7IGn!u-g!H{kW<JQ<~E~H828KR!n6u+|dqSV1pTu7fW zU<M1hDc<+B#C|4B|AtMBEz3}WjrqLL+I*L1dRr9#^g!}C!Fw5TK*$eVr#I<6&<Lzn z9dcY_o?C`%mmrt!evO|$QXs*7+X+hJbCEEehV}vDFV*QxCMye_4|G5<h$@(b68Xtv ze}u6d4cj$9v(3=i$XMIH@cRulPN-qSY<ei%fM$D2Rti~7`Z|y3cZqg2ushx_K8He# zi$&IB9o#|>@=Etefls(xGl+R<ND|9scaT^bo<a2_8qZvvPe=*2$NY(f(Q$U7NrH)z z-XEgS{5Tea`y->9LPPk*y(w;p&gPv$z4cnP2J$2a+q9@>>}#T9jp?ZPxyJn~jdk?s z+^JcQe@)h{8m&}3vrzn~mC}KTdT9xImgu!qHFjo@T9O`9fft<=7MI3uvO{ziD$J65 zM8wkL9MIZ%VmCpUnk~HaD==23&)&aA7Lz=W=iFHZ2ZcVdu4a(@xBro`65LOopddS! zkVV8JzWqTwhRI7&h+_iQFBBjj?J%<sQ4o-^o~w9NWmZuG7K@raaqiW+H5MXZr2!R{ z31L*o)0l7R2(czmM!<0u^eO98qNJ?l?3780rs`5!gu4IonYiIJpyRbK+FqH(0O!$u z31nfo$P_t9HE>>3M7R1I%r=O0ay;fctACbFat_e7nK`w8MOF58?@8Rdfn46iUL`U7 zuO!i4zI0V$gydg`EH^E3=O#8nVR^8LWCVUzpN2Lf@)s8Zi@F6B>-{s-N)kR=LI(}J zx)MJ9W!0@_p1k+R4|jFETK2zdV^?!A!Q#%A<GI=8`Qo8z#)5Q75j@j5bGV8bRm6Z_ zik<$GZNXiw>r35V|H~gf%7O&<8Yd_Mt=R2LtwcayEX6Rq=F{lHJg+c49zgikTe4~u z_8N+Kxcs>J5&+d}4HcqCD%APlJy8x%zGHlBw5mi6f+^`xyLK*fssJ3!+3?*=_j+9R z6&ZBkWE4yrWt47MFNsZ-5jIc6j^LItyO7wNADoV9#?69-maLr|{9rb9wS`<yUIAg~ z3f#=sP^}Tsc5ue`A405j(X4V{bYit#=N9BF%KcH0y<*0$;Gvm^Tj`<M)j^FG=sVt~ zQ>BB}2r5axX>|Nq?Xt)g%a#d|c5yT5H?R*+c4kXCiA#X96L2qAJ;e8$KCYNG>5r_k zm+LX1R2wa)Ga4HIE98YO&E3S3&afBVc;D>BMUzM%ySRI$U;AJE;+M>$;1=ox#rmcI z%go`l<E43(J_*4J9vZ^Au#U5F`grrUWa(H+z!3K-l{M=`@gj9*bHFovPp+hbZeDZj zFRHPP)D>-*lniaI6Xi;ms{rsZcqi;2G;OEw%I=t$=8bmz`YqPKTIViXcG{@SockeI zf`j;NAX$A^TezCWFd8lvjbNBOa-{eQ75W=kf5|;0CS=QtQ2&s2c^{G@Efya|_|s*6 z%q|xyxXA@Jg%S9uVA{#@N}rUL*C;N!-BAy3)v5ARasxpRUiJxbP3F4E8gEUyO^!`# z^aM)i1wJvYXsWtx$O(SrG}NEH(wh-rpz*^DItVfJ-`AEhpnK<_<JHB-q5!hNN7(3d z6wz1Bu(<!F^wlJf^?#0Ej)Gei|9n_bCAtPGfQfaV^{D0Q(iHDB4%Y9Kiv>A9ru8Zp zS?RHsf|v{iRhnkbXa&eOLiW8)KK?pu-!x>wR+E#M<$Bm&=q37W%0wn-{^*4oRxu+- zsN@(nn*^vvRYWNwzR#v<l_!%d9k$A27V%f&LMH((bPWkkN)f4I6eJ<d4^$2@f2lky zKKckO+H0-iLJyB8^0XADXhTauwNNKCWRIdYRl)E@j<ZM`ahE8MiBE(S4qnwJ@5vI} z2LH5EWl4(!XFh7@oh4(mYVUxe&~V$ko}I*Y@!*LX6%J)Y6lSv&UCOaqhm5Xw23=^9 z_rY}#NVT4NmD07~4#l22Z8CC6YM;%tg5hy{B5h8N--0aHfBD0o3X_8C|KcZiKG<b1 zml-OUiEYcMDE;HDHhOAl#5x~s4qc1L(P<XhPXBiw3R}a~$?+${vLLlJHQ2Rp6J^O| z=OR!>@Q3v*`focYy^QtTlK#m$9ZY#UGC>tOZc`aiiI0=Ux$T&WWrsy$`;lc{d%M3t z2bsA#i;2PBPZ>NY9|!czZSBK+IjbhG@q~8kxeZBhKdFpHM+n6zd-g>JPk0#Ilz806 zKI)tmlWw#N$i&}1dSs2$c+*lp7Q4B*lsFAJO1hL<WV0fbF6fUDm9rx9zP(IolQ2x& zi1e)TJ{x<8xso+>JxX?$w`BmMVg5x}po`S3NJIsyCShU|nY`KbZdWp&T~+^GB-?c# zF2-zvdvpI$;9a!PYy7N=fdr2#23AFGpl0eXfBK<R3BGNv9K?hMRuPi~KlioS-w!cl zo@<l|Az|tW{E1AZ5l5G=Dv#_;z9%hd(KSrA42~iX-JDKa@gVeWyx)uhZ>BIZb=1wN zDDQ0fQWpKvIB-VFD#>$I%UGmOlDh0u2gMGjM$cMJEY0@E@XlymUZ|8H1b<2`ve;p; zxGXe-OotVeG8Pj*c!}G^xot~C8>cc#=K7@8YZZmr4QApFa()#6+0Jc>Eqz>_;xeM< z&`Yv*Fx3Cbh`zh$44y63P=)c}ly8gCBZmSfRa4P*`svN1Y2Vo4m%3(VQ|Eu?AD7-P z2%@Z<>s2BvDuE4$tjK;VZOS4k<)1(Ud%A)#e~-IiTR8ZUi&VY(T&O<^E}+g+0OXj6 zNQ;tTqx39xRz)ccGlD+Yr5Zs&P~5OTT|%jdg@b84oPCT82tw9~Nmg#Os%c@I9%R5b z6<cbQM{YDCGm9js{Ziis(hiUy;Wvy-;xD?%kSzu&Od0zPk+ar~W9w(xN;|NUAldW= zzfEOXi_KH&i01IlLNYar<=19(wvon$=I~gW)!A#MY?F$L*~_0b`>C^!id^iJnWcQP z5puI^*f1F#xVUnc?ss7Al8N6meD9>?KSnJSHO&cDAv5B5Q|%Wu(>3Q7TaM%8<lVl> z{VBPsA`m&n!L33XmvGGDAYNtoy{f`poq3p7u31ZlxLR1PEz5Hu`MH({OP{{U(s-_e zr~H^|u7X|wD6GfS^y<^iXDoQ<I)M9FOjlciSwI^qP@7DpBWa=fB8QU6!d=ppMf6>V zQIORBT##p%Dp{5H^Y^`aSn3S}4b{iOSjXy#n*btzx(I~@+NjK94?$D<f%gx7o?JP= z)L<6@30LaVLoh}gSZll?QLT)Aq;GE6Gz-mdecZE~BBaZR@Y}Tyemplx_9sH_V?}Xn zUa*2Xr~GV{(jk@oZ8j;6Rd;U0O78TXRhxu|s=O<@cs)g5V7r|x(>jV>k?Xhmn5x{| zJR~p@CNJ1&;V$Si-u_<0){<%hGE?cOq*;;Lz~k??vhd+wn{#C%>@az#up>tZygiw* zIOOL7NoQB0m_Ff#3937b%6~lcXN&x^wG}y;Z1;AC{)Uc+DZKx`{QPgVaKQz@JOvf_ zXvD395^?nnW%ZTXFg39xl4$+2*eugNNkR)zg^&p$o+#ov>ty871$)B5_Y@6)af)=a z-#*Ex4D-mWkMK<YjtIdU+fyRMd_(XhYoP}&&*-=^nb~v<TI7(Fa(p~S5CIAIFoLT+ zUsxEWwP^LOvp6KU*Um31GE=<X&8(g|Dj_Q**EeT3hdt?SmV?N+AfTe&@Uf4kIJ-o~ z6~{s*LPFEYz@tV;A|50XlPxGo4qt}ExlTjD8Oe%{oF_gI%-#w1`RM=5im_SQVaaIE zOR@>L<a^>>U{>F96I9<~G8kXeWAA)6%LplxFY}|LLx?tYo(o0-pipd!B~0Z_6rGZP z=(Wc>CYUu=Fhga<<a&*t$#;Q*E5J?DM|qJHrZ&vz1|K=Z_^;X4E8SoWU1<#z>J$F) zh5`{XqVP)sfE6Qc?j9Y&6&l@e=3Qa~898d98UIi}cgnD90t9O~ia)c$FvfHE0~%Gh zKuxJ4BzKZD8sTytn%TtYII87`qTQt`zXS7!8Sw5~ayT4=_(%jP>?zo>WA?O3t{!)` z<`mX)=g;K*uS>!SGm3oQsoYAQB&+<wz3<H>I#W#`Ag~t`nngP$cKT_L$7M>Lfog%s zAJ^6ghl<sr>|adcxsu2=shsIv07rCX#o%~cj%z!d|Gf;2I-Eh#B#qOG*8C0AJ>rFN zuPcF}Z00=uJvEON=alZwW{p(?4Jtq(APgqSgf?a8JnlaTLQ=?n^)c6?6TGK390hQE z*+yjn-CRG<Rv<QHjOC-Oj+P|O_o<H$%$Cq}`a4t<8mN=<xe^wy%)YB)l!~*TP}P$8 zbybn$n{Rh?rXgpt41)@tPx1@0QkV8Xkb9_;MxZ4(Da2M5809JBoRMaN%KP^ebjM3c zZC<S7g;NW)5;{L>$s;%7T5Ux~G6pyu2URh5Pbu!>fVH{1lM_FnYBZ7N5sq#weZHE5 ztWq9$8mPAxv3qrT79YKt*k-*G^&*Kz%k9plwKO$NLhf=GjkNT8X3-tg5j(u;MYYK2 zb)dEDIbqDr+ap_Ve<eZ<h(Ez5=$H1B7qj~jfvB;A@NmgH-;^mZ;&tD^A!sBaL~q+# zF@YxI1rRaZNTz}q(O-QSr5yN7zUHj}B)8`4)8Hf<fZ0f>7&hq2zDRM2+1DOnVmN4! z8*4?Zb<RryMw+5II)>&*s@&6t-=6lv`xl*w!{6!Ph|%Hr6cBa#-xGF8lFf+dK)~Yl ztOo1<s;k$YkreP|)tl1Ks(d*Mdq7i9ls=b|(`#c|9$5rH8VC@&sL`%ho-pIQQTJV0 zfLdky*v9q6)3~*VSLX4@%Zu2{UH95DhvJC?cKJAwHD&&(rv8YQUY?(6`@MVW`!C%d zFV7PnMb&T|1%<KhGB-LwzXHUFa>q+Lq`aj#9^S8vkG#CFAkC)>R|EsK34Lqz4OLad z28;W~9mAQNapMOg-YJ_a55Wy1O)1lRF?}=JGq=O0dYgekk2ui#>cjdrS16cc%@;mI zrdZmHsu3to+v&(bl2+ci_w^kug<`~yiVGX;3}26Y$6rViG|5ATeSMC#6Xd9=ipawS zTrdy$C|1YQfF^e&(hKjd^N##4iP!jrhrZ?iO5ryyJ<Bx$m_{i>FtoZ(BcaVNWo#Yy zzoVCOEQ8{UM>XmP5_XCH+z(<qiZ31gRojw_K~Fxvs$C|Ex%+JUZEJ$|QKsq98D#W9 z<wMQ|o@VwSrb=1>nY;_hRR%xfiPo5SCpE?n6Q}x>)ti)qOO9`Cj+jTOB&<Z~9k6kb zKO9bMq)n(*FelwH#?T}&Kp7;iN{FYdgNVVzF?m55{|0SDs(=$_W`-ezl^Kp&R*1@A z{;wCp6}JXi5D!WdHuPWqM_S2|!WRB#<~`uXS4b&AV?gzBB?%FO7Zq-lib`Axh=cy7 zJM082>5WFO8SeLQrQuzrR8+3O-gzPIPFV~y8AMW^&!C1Q#AM0n*vM)NX`WpgOp}Ok zVz=rp^VKHMeuE?}YoAp!{$5ffh(xE*$o&JUcl<ov)DmUL*mUYL1s(>|+HJQA^Ovlw ztR*t*eNynR1EL0_2M_l5cSjSu2PN#M^g6#CYIcUIRe#QBwaa&Ai;N>4u|d=(m^{x- z$(a_*&)LM>aahk2mp-^ZpI?x@G^}zrb9Y`UV{s{)kA3ZSKNmP<$utg81rqCsEq^4{ z2?i`e%66tk3cjl+@C-}glJ!R>kVy!|Qx&}TS~HhHHO!*m7^{a1w|k8rW{F(%6y>$~ z8mj_eHBRANF)_Nq9`Y7$p}0F9f&fSrAb#n(L!HUw|3gPy90{+?6xE$!@W3juS_mg3 z3YdgOQy7*IL$Cr(Xuh?M)8Hywl7zsAsb~jdB4_!(Bg9|SYR(849t|UGtSFSGWUw@1 zs<V-<ZPW;^H86dDK{zJ%^7OCW;^q0h%~k%QIof*7tV!eIDVzRkMYM=|E3bpdOfC)v zt5cQ!x9@2ZLhK2NFlNSlbcIsew<Vdh3TcIJOyCY`@ya>`pv#uOewH78>A7gzcd7l= z`Jj48Zlf|cX_foeFL}`(V?kWfn_ar5H@1EdhSMbDtlES{z&k&0i)W@#kgr<xrij}F zF_^}^0Vy=&F0%02xSJKu=GBK+mIo3H|F8d<Ymotwp&T6-nv)Z`_YQO2Rv+9TFi#>a zfLbqk8Ws#gPyhxGle@ZcbJk4fn-3x2@S<z9YjHwP(k2kko0hcbps*n}tHn&7DlU`U z`c((0EgzJjzQOy}@7U2#Q%zb`=@2trd17#2?8Nk?M|I@*i?G<AtgP?1e-@KIzO_(s zhj<`Mm&$0+E#Ts0AxLoj5r>V*Xv)o27Y%WKT|CaUj_<0qLpyUDB{4SOeXs5HIgn9c zAddI%9f9DN<>Q2&X*QAAR;PD5Zz%}%iaA7L?0yTJE;X94omX{Kvv|dkIYl|Y-Fcuf z{|dlCQVhnuZF3~TrMK{Ka5l-{z0iC@VgA6ZBD%WSUN^BU295A8W!2bu_2G~nghT@d zR$~m|+(4L-(SyU}QCL?21?k)nAOR0+sC32PAPWX@8WZ*$>cl%*`N;lo*>tKl9N;qr z0En4RvqBjTIKk2Ub^O*i74ng4lpfmtGYCT(uHR^6rQtIJ%IJvjxD0W3j^M~cZG|)m z&)&VK3M&yu&-p>jWJ>Fwz8`y>1eaS42pBZN10xDn4>XQ-GNKCT&9)hzh|y>tJlr20 zsj;tG&_pG&I_OW)(3y5hf4!f7wA^jf`|h+WR<$^ChPzxawQbZjF;dGR{6715^m!~V z=ETfo#P+1CqQ3R{yIyTZaxyL<-X{VyQ7M)pj2Wq}+PugQLU<=POST^`tKoWFe0Zq_ zTbnX7Ti-45(EdpP*~!i?BkcX>BTyrh6&!^3FXz2rRr0KkcR5Y$Ejp?mQJ^Q~<*b;M z4Y^}SBBog8{x!Z;n*WLCtUKYWaRQ%gSH7Pk=(sP1(Vpt4OJC9?s>nZKHCoA8ij$RV zh1cB(U%aK5!8m^W?E9ci${Ejq`@MMMXzP^i>1X1hu>?E`ofaz<+++uW%0;Jm*+%5} zPGW9yox6Ie2U6+1XN70HEA!L0E2Ha{S~=I_^&1jd%fsANv4U|vLv{CsqV72ygN;Ap zoPhA2*zTMvnUc5`nhgzl^`!&G$jAwE`m-Eo7eAZ36#kXKuQ5`HNeSNAv*S*X?4!Hu z%doC^wfb$~Y?YS?8R6@xMurgCW4^yuO?N6%tv+6<l(w;BgU}G7TMEA^eD!%Jpv(@V z3c_@aX;bL_&d=EATRB)-+S$w*h#<K@(uwuHDQ?_`B37T~$=}g>-mvUG0@F~)>14<+ z)-bOLCkcKVcc(dJI%EttnMONP!JNIjOYjpjd81rLQDT~XX}rjTspagC-CeFbrO6to z_Q;g?x<kP2pU*a?OJct@s){$KiZe5(j|)zQ%zsyL*&-|$1Wzjs6l84g9=ZL-d7|i( zo*?BZp%h^qyJGE3;`_mpnX^P)ssA0u&JYffErt^HpH0r?>?K1>QDk{>HPa}dr(oqW zrCAd;$p|rf_O9$pkT|#xY&MZ48PGUl(CtvY-}+&hCt?taC5lOg)T%(2Dus<ui`dBg zppfY;r-qFjs*aD2P!`3Qh^_tVlPn+&jtLIJaVj*Am%@^@P+&53^W)J(xLk?U{EWq8 zgAvh?=r)4pWFDELzOy>1J4?)CuYM<B7?ALADD%tTw<jjNCoQc_skAL?tDM$3tPB=A zL|y2p-e82#3@MFf72pKTO%G}!>t<Uh25);1Gh$%52yFcL<Aqr(g}cshz`D>B@!+>^ zXT=Tr?c<E8<NuLm5Y)F`s{ZA%`|@d|l-(35a~9tum(Y*Gq-`gd#ykkFkBMGts{&z+ z)rJ+{w>%COR!c9K`&Bk8uH$*F-*4;vF?Gj<gpOJB<sFp{NTK>0F}QoxDal0o->E&i z)j_qB?kBF;4<u5h`T+qxysH_t4hGKiPJ%D$#Gfj;DaE2v=JJvgwC4+fe_wt2`HlHv zw$@NaeI+U$D%q-r{h%-K0+7GN`P7g%7rlF)mrCDSlqC>CA+<c~ljx#o+*#5V761XD znVEZ5p=2^k0ESBGVC;SXTcnTYqwluBFY{!PhqFq03PpQg3|SIM$xp4`7F3qt4eHw$ zJ3GL9l{m0*ISL{EEj4IH8taWXIYz;?3LI_W-~`=7ASdTw>luTDCl1@9BeoQZ7sKx7 zE>j~hju0w&Eoi3uyI=@X)WV82$(pG3r$5FHjBHM&W!_Ne5AG)3p7V3wPiwy?r{Cwk zBR0<0^Ua*)ynzn8$Q)T12ZlS+;o-%YZS11AA%Q+8hDTTJ?t6l4aG}nbN`9ddu5-?R zy{&n2=d2wu5B2*nC3dKX3|0Qy|J+Hk3ccSt&|jA$<_o;Mm|jsk)rUmR%J7^nb)S^L z;W*0^AhC;KOU;VwCU4c(xAzmG!Bwz~qrt_oWWHs;;EHTspLoG_nExFhNF3h+zP^Ws z-cGg_d$|}}I}yVTpb~4*$ur>;>0mjW)5hr#x?7Zv!-uDv_lDpL(Tj><+`&N$!Qx~P z_chJ5eJ$PS`Mi%vp*_}`^}=jbnP`Xj?(N=2awTo$T2=HOe9Tx1-|-nwzF-%-i2eP+ zKT`brVs{P!fld~QP8#|}<iqu;$LY%>cX@qb>7BLCzc%xC?D|?2YeQPZa$Jk8JCCAt ztx6wfJ4jLo-0+64t9hN(xc!ekXHf9^^q5LRC*)SIC<Xm@{b0%m=aq{}It8!s^CG7# z<Vd$^+9hHy&%98zL~4HP>#fC7__mq9DwB)FXT>j5okG(=2Nhn<x|CqAzk%*3*|zVS z0a83GeF70%eBc|UNHZoZn~fwy3o6ZngX7Ou1M|tOMB`|7stDprql+RFEKXKQB8JT0 z)k5}pjO{D$Qw9Npmi=MG20-b1^An4aRrC|1SSdIuV&f|H+JYiR+<uUvc6=?kd=`JA zLVX4Mo_nr6iI@LVWaP+r+Hv@w44=<is?V4BYEHBlDthQYY^T)R@~ZQO?b9hHA9SGR zlHCr*?+lZ2`Yf7ghm2z9=N}(wJ`p0_78QmhW+W-8$+F10L7|=ZNft8GKDmFa;|Otw z*IY`wdtTfRUv5ryhaZ_U(uu|5n_qoyWgLaf6o2LimO?$}oh01cvb?MGDz^lk8ou^( z{Tf$TWb8ovERr5eJB<FntqA`$iof=X16g?wXbdk|L93SA7I0~+<<x{M`r^Q7X*y}8 z26ahfq;a!c<Y0FAs4Crqnq(5=#uZU3#k>yVQs18+Gb+t}&YQ;h@rxKNAwEuhIG$|5 z#$F4xX#LXVDA5U?w}06GTH}vuXfSUOhiK}kB<@5|6|5j5Y>7M>xLGy~x|Rvd5y{jE z-tc`|9ZJi6_P4(OZdvPIM2<1$BnUs0DkeL#J?Nd~JmF=HZsTy)7{|4;-igs8OWJZ} zb?S4TFmeQ~&VOHP!Fry}@!^!4yR~_8a*Al$;>ab#1>M6z!+gK;)n`j&0ph59p#MJc z^MPR#?@Ai0u(qK-enGVaPqsakubC~bnXjTn)-?3Hyd|fnIs92*A;3BWXnq-Fo?K>0 zWoi!4#78&~mlz8Vx#IK;UNQ_BUHHNC>q+#J-bPR*4^C+@oWtcE)|;|?!wJoxw}gOw zp1m>>_UOq#DkV9&RMs&pq&#laR6}i6eR*lXJAXfo0J2!0!Zo)6xR2@sVHm7*^nY=} zILG775QxY&KXFH4gt((biYHry!S<y0X2Rc&MW7b~p%S#?7V5;VV++k++1=;aiZGey zI?ey}1EQ1<@|_Red{N*9eYQVy5xY1!qh>4FDgbEMLV-v&6_VF7X|hr|5R1C&{!%jZ zwRW!3&EXuV_fwPSbHsn!mqnV8F^kDJ0CR;n7Zo!sm<l)th=yZAj+GZEyI8xQT8B!< zfrIFNb;Pa|R{K>F0b+fS8@#cR3R29Jev)AV;)xSW!n#I(98GgSsiP>~keOeuC%vHG z-W?yGAJ&-sK3UJmXKEqSdEA7o_|qlFZ8DTjK{aQJ-3b%UN~YZhd69ZD%;b$4w`EG% zaE0Gq4Kr7QFN**OG4puMfLLzv(%xtfhO-%EO)g)nvnX0HEDn3|(fkS$jDj->d_%yR z0v`_n6w-o2?BSCuE5i`ix5K&gu$oRL_305ryx&zMt~w}M7jYt!Ibu_*&-RL^{A77d z8KtGcFe#}_Yg&To156>emFZv(BA|I_wh|;rDNtHn95@^alza7wGP)J=G|&A8;BinX zg5k@OZNZAgYb3w@>n*9D4oOl00w={QBeyD8a2oYZhi%e;9fe#(hn~vdma%|U?-^09 zGp%uk{g%HEFJ(4om@V1$=Y}78o=?r9$`1-i<Tv<VCc9Hm9~*C;f0{(_dp8b}LH7wd zmNO>&i2V%j0@xo>(^;usKF-at>*#v_*mp9|_9?6)M5^L$o4Asr^)GfcZb+Zl21+yR zb)56qTd-bryG|HKF%)}#a{?i7xizjunaAY7y!Y-a!|YA0ci}o4t2xhjG5BZovK~Iz zm1@lqdebA0XbTRwnw398V<WYA{mlTv6_@tCsBi#!ctjjvOcfex1V$N9n$xP1W-3&I zA(kl$!IOm;3HiVJGFWbp0QNp-6fTl@jxdnwz0~ZOf8X(VjVhG~e18kE@a5Csw(<_s z38W2k(+oG(U@<D|>)R!Y5hzI;AlcK5Q!P;7rIjx*4|{iva|?{>uvsF&+QDyN^)F)= z7G@-m=sY3WL@kFH983?3woB(j?%lQqmJes^aa6uw=#Tu{4?QPJ<Tw1gM184!?9p9} z?|CuqnKr79czIc>UhXtKH{#(t+9h5sPE#?6Tdw@YeBGqRRF-n`e<*v)t~i)rZFg|D z!6gKj!3GT$+!@^6U4jP*!QFjuclThy-Q9z`BxpjgL!Nh^cdfn7r_(>-TGiFvRdwHO z`nCDw>)TnTdVQfi>6KwUVX5XlgzI^lnf&VNrhXMs$?ntK=HvC*$fpTvhq<u%&9uaW z**E4B*2`YVr*T&Ou?!Xh0%b;T0Dw4C?=69%ZH=nh2O;Oj{2@dqMp6!@<1~Z8`WHX% zB+-T(jk0<Gx?ebS7^eNNREXWh{iWXH65$asbH{O}ehVrx)Vsh2BC%8sn1MjwfIF&H zK#ICv7@MkowKyU}D)`jkW+ooEL{Tu)9YHgGT#d0D%&Ghlu}R)A<6VC8wXI}_@K9nN zMNh36V1<=vyil2?^VigOuX*iJPG~i=)R14-cQ^Hor8dJVI7!2r!)^?}jc#!=&3$s! z)Jc-m*PU0Z{gxW3n$P@QZB?y*ZqC1f=j9x7%g<BcTgvqy2LeCUp0l$pQr2{J7nUB{ zly)9pUtYRj{i;ty%S_UiT3GR+wxoVZD;*Odru~ihAN*Kf9=e}jU%G#rGkD!D!obiV zYQ8_}y(%FEB1YDSG6sjWYL_e^S~3Ct@gWt}=inO6j)ys7MH-?6SRvrh;lmbC;IAUU z*%h1e9TrLCKvb!=(JI+e!;4Lf5zRB-Q*2UB(M9#!llM(i4!}_K4(v+cxgzB?2dayR z9mgVz(O5fWy@z>>&KHVsb4%KpKsV+I#?zigVjx9cB?d;^#avT}Ecu6GI@$K}1I_$D z_RSjLN+?5CtqYoTG)kmX*Bk@fzJJxFkHIUI#c!PBwrkYv;7cE!rxw#52}BR!l5Oza z^l3TaXJlx<H4^=Cf&*LFFh!wXmBgiUBUP?jA$e-4QT1$8>{z;}J9<mf=%N6}QLdjw zFF%0oL<7Nr8Dqp|(Hu*n9oTI@vrin0sD-EQcV*Fy4-t3a`8MLO%wi_wtr1F7KlhIh zgLE`#$_UyE=fDsAjt?Ugc{0oP&2?53pUo`gv9X~vqNDkN87o*yGWVlzhw`Exa1?ZA z7z^LRF`}H`xllo@Cx7=`QtG>1E|)5d(*~{Vo7P#V!Dw~V;~IeO`3e4#Ck(>-daAmX zk*eTJ*Xs9qE6b|jR}^<?c{WskKbEW!LAdg6*I%!uVe71`u)Jn|FSc6E=5E9GDe*Q@ z7)knlxc<JQ8pxR<qCI`uZ)KS5F_rAn`{4ZNYNby{x$xULXzQIyoR_L<6(kVFn+Q^5 z_UJ{9mVbYbf)~@rc(K%InI^#V`HJ{wNwk@?Y6f?w`}fapGBKvt&1OJ9UelOHcpF_f z2QCCod9p{fSG;(er(I+WGPf<}wg{y7$A?UGf;=2%Z3HG|jAq{W?Pj1k9s|;HPBmQ{ zR)aOH$<|Oai7yD$#x58Q#4|sRVC>rn2CSe`#?alQFQ8WiK+>@(B`5d+IwB2`_%4Zb zBArEd<m+r9RfZ>roTN?Z=fCoYGOSUVlmEh*mwBYN60)X}HCKI}biMA*W*aKC)0+1E zmwWzaJsk=-PTFbMG^RIJ>Q>&~vrgMyzW4L4&(WKH{jHb&cJgX;3t6SCJ(!p=rP`hG zJ&)CA7W`SlA=l~}e^Y$DoO$hZR-1Sn;{7qBpVH`PY&~Y#OWEKkDUMx;Q8XdR&WHXL zSRjH01+OD>S&9r`a_u-+*7Po_+pZCb<-1Aq1T>0R^b&ZHREf;oGzS0IKc(jzKy`y% z!clsL?UzhSU({t<)KG>Ulb6$tcS_8oro!$`W;dR^<BU$WW-?(O3vEL5&Zw09T9<qR z&ovhm6V%kETSyNUq%0q+hrZ4IIl?AP8RaZ`je;DAMr>3DWy9_A_{KJZ#vEF`ikgoe z);^bZ`)>VG!$S?xJ|=j>viIU>iUQVqF4wTNX^{47LSrZ^uL$#FB3v6Z!sJVjP?eMv zPtedQ$ymY!-DkX(K%XtiR+15ik*Qd_Z*1okW~Iuw-%$rzqsm6{nz%kh$RQxir^!QB zu|7Bjj}*fII>%t@_a7(BC{#Qa2dgr*2q+D!S@DZGeh4+`dVXR(bkQqXgT#YjmHg#+ z#WheCg+Sn6*A^3l>1YN2&9AC`@L3atKUCgCuxMV&rX_|E+c|)>T>2?hR$j8RYGFGy zvG`D>2I@5?D9I3&Q0AU1H3sbD@wzZ7uvCAYX0&A2iQkqZ0m=zC&4QNuq6t2j*cGel zM(UcH{@K<Yo>bI~yBe5PnPmn)(Ap)~Xq50);#aB!ZV5NO^K0dvTGSw^ophi0z}<ah zsQk;|Iw-^hsf0~EYQ?nw;ak3CDl+LshJ&?Ec0eDyNWT3jLO#IB+SbB9#iD9YWdS;P zlz*`5fr~{4-q(2!I4=71#8V}b0e24(A66h%-PtQ}r5^)?vT~;|0$8d7Q5z|HY@+t7 z;~(d?H2f>gEQ{?Dw#Jt1PZ1>WI^7oQTVg&Otl2bF$P2z+ei`rn?%Mose}(h>tzkgW z?}4#sE?(y74*OhHUcXuWTYt+B)1>+n%brV4t9w;rb?W;#?sfw+`IIU@PyHW)KiM`$ z1IMqxvi?-&PC9%12QAY=-FH>;zu8u+<tq<h%!H)g;u|8tL+iqXYg$j#G(v_FraypX z5NcS)HQKLBOEHN1aW7)4F+TW2<eKbah@3c<#Mr-DzQHVBW||q@3#rllvW=GcS&hW~ zFuCGgw8#j%s203KOruAL%wq!u0N}JUdt!mnJ<pVbCI^*sLqgZJs|$c>!n2wV*+@?e z&7-~oIulDYy9(Y^`vUXPFEwFBf-TblR@)RD6jE*^rZoQklwXK~^J}LMuhoRHXgRO& z(4u%~=O-3<;1PK4#LKZ^c>nR~)~n+8Hp-ZR4Ja+!i&x)AK$@4iX_b~|i`vO$o0CoO z97p8XSI~2z?FkNrn%cremBILpirrt725j58)mF8Cq=`8lB7OYQ>i0OcV^)T0P^g4m zHik@2U?NleOWl7i@GjjcPXrLpKud|jm>x<QVJ`giM9a;<`YG$}81{X6y~zy|eta2x zw|F^#2a92WdQ#Qs{(ThpT~&#=ZD!u~F90?icbZw6gNdw<j+((xO@;n>fr|GeK7jIK z(~P3G6t0&;=DGqpJ6v7vnW<|@jMvSOJR}T~Wfq<q&uV*~8Gaw_lx2lYw>R<3zwdgU z)pq&<8kY}^wU1R#Ij!m1hddWk?6dO*McFtC{^UP*B|{NkYa>Hq*C1{G$H$EK%p<{M ztp_GaVFkO7ryZw7HwEoNww0*SC`|9Kqu*PKxPB5=+VJ~5{nYXI=q+J99oX91pBZ*@ z+qTqQ))>?)R6ra&E`9ntx}$3^^qQ5jA2<7xjO>hZy0gX~x>l#U1@11imk2wmzmjde znXSD3cG%O?_&sy~`U=C<?IdSJVVb!VX)MtW5YQP(rHqB_iE9x{qGN;DX&5E~5RBvN z4xxX?^A^Ol+&q;_Ghd};@!9md>r@+?9-h*#ErnCu@?*Llv3d|;;x(pZM`?c8fnUV^ zbv-V}{exJ1)U=gbW#N^hP>RUh!B9tJISmcEo94wbG9Jx-vO<|C@JgDTb=oQup*}Z* z@8;u6CkSh3ra*EhxBuru?ms?>e2o<eB8R}uHxNg0RT?lU@-n;AUYb|xYPN~%l9Eos zu)7HG1nZ<8HT36O!WEpAJ$-)EZRlIN1>B2D<ch}yRn;cCYx1QSBU?%qO>35*#K77Y z^7lfrxFSF?3YZgPfHsd_Jt?d(lCU3X!YBV6g*+ZtfdmSbPm(YM#^G(5$}h9S@~8WH z9djb6v6D1BBb<wHy5e3IvZV~eK>_O>iIzlBs9mi3`NrkLpl>~a+ljk3Kt<zzdvAv7 za-g0xo}5H{s!a>VyOC6y(_n!q>Oq+fF^GrwgMU|kVg*6Lxcu0lM#;f^sX|B1bNB1Q zLN=Z(wnCKGrf%vp`fn1x6uBu6poJH-1Hxd=NfueeNQHn(=8B~3`d@$fJGb3+f%^P- zc*Zp`gS$zG@TH#eCh94TE<-X2NB$qY7&If1r6IsD0$xKA92AUd3@e*1;n4I*B|?O} z^g5^s|5oWT4&d(;0k|(#M3U(pJV-`{;h+b`JPDzbQ3j#^a<Q+1XF%L`AMzjU=P3l> z5h5VWqEFBejWS4yz=?2kTF(%kC|ju7jhHB!gxlibF|rM$!?5nAmI2w33t;^H5u_B1 zaS8`xb3#GsT;nG|AaPJh1w(Y`vexzIPL&Av3eYvdT@e@0oc`m4=AqH+4d?$X0p<>G zmlaHHXS=Z5Q=e7t$r@G)7<V(gKq6xeV$86?q~DD0l|N5%K`JJm7m0i<Ue*${>EV*% z?9UkJO~p;62tohi=TUn7G&t{2``IlDX?EL{y6FqEu*_Ku&*8M&TRnvF-ZYa*0J`ii z-*MV~Em-ld1XQ8Vq?6F7hLoU~0T1>hYuV2O=XDP3QaCAoEg*xS!@Ax?OOKCZUN0mo z;}U1roz@+bc7;z!GhDVZkcq+CD!(bG0qFqUn0PXL0xLA#P!x+AzE36?e<`2<T~yGw zm@hp5;4=}JWYj#KVw;%)Uz{;Oy<EFW9*T|u&%69dT*2mVSdBvAIKD^OAuJ4Qg)%_| zbzos|B8%4RYXy<^K!(dtbAsPKI@R7bT_2JNC_Ecr#h_)HZU+Qne}62k7<@7@xjS%P zOYL&D7DLVBziVnZO#I$#yTKPVrA@D(`E9*1q4{h2&(ubre|%1a$@qgsc2);WD_hSB zLvpNiADTIh9=G@z9B=VQH?%Af0aCOhrOPRZT#-ZAK_WePBvk1DB1Ttt7YHg!NW5nF zjecmnENOPOOmAMk3Z%#B!Dm~k-#H!g%OV<I{j#743<|1bdMM$nzdx1e5dC`#6?0ki z__?APrPZpu#MeMT^T_BfE06P|ikG>ifs(4RV!to2ncnRRc2F)dL;>$zY^rjewv0## zA$<v{b>(p)3t0;^gSZeKQGN*4<HB%@_@Xs>QV5D~-|YXRltUYH4=PSAnl4o2pg!`f zIk0K}eaUdZd!V0_@^87WON!vg0r{2DK+R!IV~6+AyZs5}?d~qx%0GldXIB?E_1Uy| zPX_Pz!;{_}5l{T%^C}h0Zw)v#Diefw+|y4o$R0ZtOgBX^uoU+s(#PJEi}aiyz)y{( zP|8Hhu>T$9OmSo9RRWv8V~1w~Bl`e_?HJlkpmHR*FiQn$2g(SM-9$nmm&%r17`zeY zj#^?8mBjknxC@kIS(+KNo^bhRq(KW_a_&h-`$bvM#_WQ`510)bv4RrNb)AB8apF{F z3)e*p`Y5L3xa3b1wpJH2dUbn(GP6mO=UB@~szuKnru40?>&^?>*jm+XcSc#KleN0_ z&}!{CQdLu*=TC0U?kM_33BxMh)(+}f<x5<4x+SsvA@)Ks)*7kO)7uT-)}*Xe(rpV& zEkoX=viMfMJyTUR2`+Tagt~Qqe8@h%VmIg4k}GESm1Uer`N!vvcqhMgxq&CZ0fSMa zHW%EZ!Y;t2hdrMXKNkaX50C%=82JzZFvt}CL=h)LTOmo4&dw+_)I&ZSM4=8q<NSog zAT(^u_uv9GOjL68An5=Il%W>}4q*bFW<b1G%*B1oe{mM1Du-$c6N-(EjcmN_0FzG` zf>|GyKUhx<>tB2S_id0a0CWv79ft`js)0K}DXB0JhlN!|NCICqlEPpD!9nb^YII0& z7|=P55zETArlVbYen93sVq+MfwFzG+EU^ks7D_2&y?hS7&cF<*AaQi?YWR#)VQX6_ zTo^uIbmW^RFFvr^3>Fg|TOEla$0Q#vVhHSQb1Dvz5(1hInONxh+^+PyUmA6<`i2=2 z>Bi&g`{^J4AAbPLCw@z{%oPAO@_vIWCMrP`TX|@LzjWfpSkNc{UE--TU&LJwm2xW7 zK%R2wM@Opfc=wDlwRM6;CENO^iF94FmdpJh?g%{W_yj~^ZLnxWR5U$`<peSXGh85h z{{+4zvi}?*)Uou{59{*k#qTH9lHRm_LO*WgC#=I|$}y$Zl`bFW2+JkBZrO<ouA*A& zk*sG)5;O~l`%>_o89E*qtZrH=ojPd>P}pY8oc8}EN6}XIZLqHqi^*4^h0lKbMF8QB zkaR%FvO;jkm_yp$ST)Kg!$jMoMcNO8&YTE&_wSl%T7n~oqASsNu2kgyTX}p+48ddg zD~M{8s*_`3h+B@L9eg=DM;LZSH3bGWL!Wqr%gQCIL+HQwapk(9N`#sAgdugou1w`C zsE)QybI1oB<Ls$maG6my4e1yQ2a*2ltzkh^`B8ToU+<ub_qT{d<K}4T@q5wc3$^2F ziLYIh5!ccO{UfifK-3|d(1uD%oiDpxWeb*PKH&P9ExLR9v>1m0j7dR^GkB^fs~m?! za%4NU$C?S6WRHBzBE(~mEVV_@n%kx<Io`}&8-$%SHz@lv%OCe2v;wJB92RNLnT&~J zj%;=YEnS--Ia^GFF6Hm<w(HnW8k%b98J0uWk!KMY$|1PPuBL0?bfT&%>t&ktVq9FH zWgMC-C%dVQN||o1xT&6grAkx8w^BLLuJZJhh_Om&N0|Iic2n=ik1zWD8?xtpJ0#@Q zk&lvhA-<zu{?qR?@JPT0i1ZI9RP9^qd^Y93zx>{(Ui7&nG_vCAD4|+F9#XLNJ?WM& z$;p~<kdzbS|28rHf2$`)l1GwVm5JIWPHLLSJ1{Yo(2Gw)1THKJASzba8BuCeV&vmV zIFWK%U%MX4)ryXJG!aXj6*-}bT%mGtaf*DkZT9iTUvP3z$Kp-2Pfm03mI)LhLx<>( zE<0ZyrN%4OZ(Y|qzf-6?uFVK~U5=`8_8naOUQ{>R$`mkjM?PXRl$-wjIv47WTZT+7 z0E^wwOEqoKoA!HYO*r+Qx6e-M%=GywJJ(k_<vpw4#Qo#!#^S&DX<%39NJSjSP@v>p zX?<k&;iM~+rzmAza%@g1`p+lpjYBV>Z{1%;jJ&po${*Vj13BRyTPGx^*di&lsG=Z* zMi;PNVJwc4*F{Xt)&1^s(Z#zpgJU<lcu^-ZOY(s2p4~p|wNcDb29-m512NAZMv8xs zuTjgvS-anjH5}z?X41kDjQ<c6_s}bftJfds3#P=S=g{NJamoE{vwk6@71NfGaPll- z<P{3(NH}|!h!tjubc9Tw5ck3JHiYzokg)0dFO5~ZLoEn9t4<{W9UT^}n6thKPJ}Kq zvy@>3KZ@2lpHM#I%}nijIvJbrV=1T8(}?}Ig@-a?Y3(1T!wR#^%f3{PM)qgU{B!Ou zp`Ok18NvHVtb#QE<!7;djsV5^Rrc?jr@N;-pUv)SxV~gpg+qI?MG!(rDL-IS9>AeV z(kOG(2U$mpm;_@I0ht9W094So{3{cH0D}}9(1(bPXR^I5stSWF8wQAii@@aEh7_26 zB<iCELNIH1{Ye-=$h>lh^t#0w42}W_;*OkLj_5+fPFWe6s57ZhY~Z0ZY{0UH>~kaR z4oQ6*D?6brYyuY@d?-2!>2PQxF6z5hHmYcT>o^3?KR0|DJS77-Kp0&oCL`0!eg+TA zHN7`A$Zr-4g|+F|SHIWAMs7VD3-bknuS}Y9;bLHYPmXw0W@thmNkq{A79}peK}o)r zkBY1$P6@=^m~5}WViYSxT$P$8Jk@!EQTr=xMvS-_ilpWL>7Pnfk-NeENPG@8S{m|P zQ?F?`K<x{@M#u^`2rT!|{k5(bruyHtQ2AIdpfcEe2p6G75nU0S=XPff^luA?^uz34 zL!Dp)mCLr3i7?=xBXYTNWMqg^^Y8$*_#P7&V{C0IpFD#j`ZVR=doM`Eus<Sc)Y}vn zqQ>Tm7ZR+;vWB2!oQSnn=}`V9VoYQg-MQg0rB2r$>dPlS@{ElV2VA9_z2<WZ@D&m+ zZVSz9+EheogU1s4H7d7*e%iQp*kO5AshvK8pRLmWJr-XdEEUr-O3msg(<Vdc5>C+R z$`X)6#1%f>)$&)7Ec|?W?L$>C@u5tPx#N(iVNjqV=8-FlRA_Xa)_VWqy>;gIx}=F} zeV%EfQ>OSYer`mIIPBpMfluLF_#kwc;`sa~t|*<TOZ8Mr6<c&3FswgSB$yvQUYR5V z4Y@Z2z=np~8wF)efHMh1CWi9DBz_`-_0RV=1`^@<=LcAmCBZb|=%jx5j6w)+44d9V zK>!p$)9g{6OBqWOsz;s(B(<({b(LKj7{?cJvS>xHCxd-qXm6_aFNYU_%%i@wuJ=5Z zeff`ouflQbZA@LlAvC5fp0)zA5~Yw7hd`xwnghSAKMfdc8OQXxl_dQJnkTXN@uH%P zzdpYH-Jhs4(f?#*l2}m0-gXt1<7-TxG;B2@=JhAA>73_1K-NlrJXJN>qYW~e!F6jO zzz3a-43kTHum7z2+wtrA?5-KtQN}oCb<WA#Fy!0%$LCf0iC-J`p6JPn?oEL6l{U&* zZCuET?(1#o_x9AtB=+1j&P0PYfh!`u|CN~&WL@Oa82!@@=?VO0LZkA@l#4`I87PU2 z$<K?AXT+2l@If$mLDosY2wP)S;o6`p3zFbr9=w<+n~@m1R+ouN7f!BhGsd+b_0Tw8 z6L`i@PXAYICon<}VbWn?G_856ePZkI4;xQ=^m*KXz&6afDNU4s3*wlMrJ;XI%&M22 ze{B7WmL|WSlYO`Sy65iQXIbYOE>Uq6^+6@jg89afC`_vKh!%lErrN;{ql2R(M7Vso zT43L>0V>^G1zTzgC6kx+X!CM)>XIe$C&l!fRvP(4k8Cb}J)M&c<7r#Iq~-g6d=7-j z_&r(oR-Wpg?K>SbQE4GG<fOBi{OK-u9b<qZTg{Cg0;R{dc4Bl$-swJOk3_%7pntSP zfGr_!lc>l{C7p2v@*(I%Z7oG{9L7HziYoOe&v?kRn5<pcI})1j8pBrw<~gmBH4TTE zBnqQzKoM2&N2j1HPhUlkg_q=<v(!^y^|N7KyZ)G-{bAIz+0}(tHEi9C_@nFx-jSE$ zI|;eS{#?{M8jFr)Kax%i_MK~Y46U-Ug-!`E@^H;E;3tly?aSIVd^t_ck`-tByLR{W zA$1<Ah&Yy0&nV{l555DPAcy@2|DF4LNwXw#hK?9Q=L_zm46ox$-DRUU#7Bh7ufw^D zK3{fZ(8IrC4>ZPN)86lp>3pdw_%D7W&H0`1ei&wDjeEVLu~qU&MV`^G#8P%VB8U5U zLT*b#+Xw#Wm8phHVyR42!I%E#4+@(`tjI`&j0EN2B043ZY_DJ-4iFisqsp4O<v8t^ zJ=Z(l5a50r{jgy;X)Z6DXx?SJe$iZnQC68l3^19n2OtA*Db$-dWMJTR)k{EBu8KrE zHNQYsp^+9<-MrTsOFDkEs;7>pnY*0zRj1;!%bzi<h>OAs>$Q_~{meKvIEP=aFL^aD zc@HWZ)|SsF<;&}<?cRMNhFEof|7uVftFt;p>|$j*ZhPaon0k8N`kC#tsx_rp(>+zg zNXz5sbWz7)#FqTLBtwIBocXHF?v2s^EI&>GJ(i`CX2wRndV+=n(SLlNr0w~&sSQQ| zwUh844gAY1k2?(X(#I^CE5~hz9Ob)D8iz76GBTOHVpw2B0HKPmxdmVo%Y^#cLvD34 zd?638Y;tHxj5?7$+1Jd~ihw{d>^cN4jp*J$T%;HZXtg*_=ma7<J2u6D87Fa13_MfH zuyY+YEZCn<3JGE84h@=xXT3wYJCcj3Q7d9$O8FJ?B!Lq%;erc}m^cv+`tWTEFOh3c z7jipRpEb}^fJ+>v9#51C3lJ}fk;qU9wpUFrSqzvGTl(4>ccUcmfEO;DI7nf3Et@V? z5kCfJhEEWE&cJT-;c<Fv?`3Cj&+YfA8CwL@4ah~v{%d7e!t5P9x(AYEN)|laZ!OC3 z{=8EwGq0!jm_o6ACHcw!_*_|W@pDyXZ3EVA?urwG0D;NeT%lrT+Jhp8@g@`|3DPB^ zm=S=90lC+o?pv?@Z~ky+1{UjX>D2m3Y6s$2?RR`Hrv7h}>Y<`25tt#OmSU*L;ta`T z0yr?z0ph(}H84o%hzLq-Z#K<muiriiy4So4Kg0+->`V>34^t)0O`DK^H_B`fF;X0j zW1@FF>(mdp5+_8ArlDaViNhjQ-vtA#2VngJVG`afDoFs*ia0<7LKXlhjMUe%QDp@m z<Sdu5!2i_CaUtoTx_0q}WwE>b&&2Dy)Flrc<|Gaz4K^+L3}{*BHM2^*_?Y>4O7mAK z=ofef-LaJ1CP_2Z13t4VG2%3FaO58k?JI>b3#YV)wGgh)w8do||MkDDlsfrCE>?N~ z$@L7^#o9!a{HyjGCcCBn+V<lpv8@b3V*3IPl>34DvgKb;S2naBt~6*pLoX$Kb6Dto zrPoOVa1z^<eQ2SP1G_O*q#uPXQ!cYJVgwToE1h?pw0y3glsd8tz4q<+O!9b!`dC@+ zWk#%*<q$hBaI}+tz&jNSMhcw@Uato(X>jVt8rnQ3w__gawaI2V`az%d=F<*TM#gu? zcGBi}P9U>S8#Wi&rIk4>v<bp7k!cQ#e&Sr%=wzciOXFFx4Sh_V_r}tSI9NScdRb2m zQy<Ugl<<cXhxWVVlGUed6))U~1PzzntwEBq6pI?Bo(RqTOnk?5!IWa|0+K;lezAR* zhDw^hHdzKoc?k1FrN;i7UlsU>&fyPOpUajhmG7m(&g<h3YsjnBOodsK4VlCs#7BN% z8NN<1;BTTdUT7g{syE+|G))YVERiP~_Z_rKj8DvuB`EQThzJjnNk@Axo~D;D_Qkl0 z{)4$q6&1b|hIw2*c6{MLfC)CDhNj3`0oc$Z#+VDa4idhzqivrfGbo0L7fd9kL7aba ztuw;4Q(&2ok&VNR5Ro&9M@M&CLFd6Nvp6uYpo?q+vuh%U?~Dx8;2~`nQ)G<*Cg)iT z5uYP20{oY{(DdO&Z&+2PGpN{=5jMo;(W4=UtvvqX!QUWtpyaha(=qJt=;Cl~+f$MR z86dn-B=~Sni<q^1D%Db(p9);t0an!l3Cd;IYe?Dt>xj;oL`{o-EdK2;9mR7V8=1p8 zCntvD3+;jcf#`?`E~p&Q###~2{<k`Ly+G=Ouxk~(Aq~qrnSIMQ6X*qNEUwS3<(xxP zWUrN>Mo*Q*<-(uPJ6=2A{0HBRE2r;l)x4|OgkK*u)jTFTF8docz{B^uT`Uo`Or{n% zj9wTfGtqR(IV6@dk9F~9vgm6*ZYKWw$gtIsM5%0NyqcdApu0at)*7wirogEW(%>N4 zI=C+<x}|=s3ggEP1GduTTXH_iT#Ie1Yu6;Y%btIEK#13|@L%mm**eryY3qOY8R0k^ zt(sfn!gM>`K)x8t$=Ggu*JIt}K&MvGYzjdn7R-2$Z1<}-!2kTqpbVTQh5{<H<yc)0 z<(nNLdLAEmh6^4`ra6fHzy1zb^b5Z=>UQpc4nMXesyRZLK6(ayWn4A>i3JTOiF5JF z;0d*c6=$MY9#M1y)r#S;7fA?!;d>%gPx1XTM|6czGfrfy=U)4z>SolABe$maZEB~* zOXz|7XYUrWbq}Fa<-Xv?(%0kH)|=m7d{1r9yr+D%+eylej0g(#zcez(9q?J|uVN(j z{a}=5ItrR8<&w2_C9*`}C9QI^OpKXa)9R1oq^5y14?Bl}LQ=I`%_p56ovXuUjJvAZ zGitN6({sh*6fPU<nI=+f1Nw-+#u20Ds<lAw1?NQ5l$`-+0M)T%w}v5%@Akomyx68c zt-kX6vcqyvIDNsUhG92}FO^?%>LtPW#TSAd6^O|%lRqg=An;0B>GJiz{~bQ^bkezz z{UFGWHEfJ&GjsthF&UjZeo&~mfsX5Yv?4abo)g7~m)Vk`EX}1vb*}uO$mAZ*Rm!l- z>Ng{xGEsc|^Lw*uA*K29+wQ;c`Kg}%W$Ws#cLQ~2{ul>WhD`$-HA*X0Ivo>Q88hH~ ziz~^Aa~lNThD(ehGmPzB)?2ud*r|uP_~w<u9QUU-?xlb&{H6Q=3A62rVb-SIL2$Un z_RGUz88*B=+{v8sSYmWEvN>R;5_27W79BFNFg??kuc({{<7BW%D8kPa1f-DX5>me| z1Y;<RQl?O_G^?v=TX1QPh{JiAgi$LFE`R-i8q@8#@sS~%G1$~Zepx(kZa_lKy~03x z^v;^&G#LlHh_8C}cOnL>%J<*?a_3#33l{u=oxL?YlGCuNL?lKeaaS~u{(&dyoeeVx zW56`#WBJd>9ibAMfdprBtN|xCF^nFucZncpY>K+=eHDFv#GoO6e<7zj^FN}#KTSm6 z4X%v5o;}PBZ9u4;dt>KVp9NSh8QjCnmSmuA38PNCztz3)E3g3ylQ0;>^*>Rfom^)~ za1i-m%CL=lagpp78?swoR_vC3i=|Zqx(3H|t8MHH!-F}<#&07~-1=84G1FSdynb8S zU(C1@X3#e_&s1SbgyhMav*|bJcQ39#Eg80ZzjsM9DsjNY;(9hfpT2+Di(+`K$NK?5 zhBW-jLJ8n-W*?GABS3<YT9K)gi_FH^e9J^H7jDnH<!c3_VK`Cv7>K>w1poLH@pSY> z^UmW$u;^E$WU6bn5J4Dt7`D~DAf+zaxAIjU1yAHc@baZe&r8t{Y2@)SjweI5X!)_c zLF^)tfc^LX>(%l9sR;p?F5olj;Gbd3YMhM~(}-jT@z*F1x}Ir@1QgO#HokvO-{JUJ zh-PO~u?cc+mZ;!UUICfvD@gL<Qdl6@QNUww@I>Y9e|S>}-AoMS3C|HZ(H#j4LR-z0 zbI~zkqWDxwe^k_>#B{X-?g)0Rhl=$LuvFP<W)4tnS3G$r^ZS-Cd-hS@KAlN*q4`M# z>k(Zg&MP^ze#7U+=t_*5_4#umJ=<1s{Oung^b`>ds~$c+L0aoqSk7uf*%kHx@E@P= zT<ZKaLSA9pv)Dwo$P5(67%-6&+q5XKQmDu&fUFKin*<(-s@UK5hKU;qhUs}Jsv1vL zHkC%3ANU;4&Je*Tf&?kmmJ|cIi^1gYAQtB--`Zo-eGQ7Ann?oud2!W~$?+USMHf+! zsUlMd#A&aC2fiV&rZd<qkSUsTNw)|U)b+zt0y4fhOexAY!N_q*fnWGhOi#dOg;bye zW&CL6eO0Xai91?M`3N}+TwS7pWR2Ql>iR{^j3CU3_?+S3=j&`e4v)y&M{|U5ocH8v z;O^jkZ_)M~&VXc2oVb)}GVJ-1;**qf0#m+n3k$=m>WVm()&ytbwnSBz$rzM?P<bFK z{+N9Lj2V-EKXQ#ipYDiBull=Cnc4sPgRfFm{2?Oa^9|x?L)PB46hG+jX7P|ADO?N= zHiX8}+#H&zR4yW7`I#B!3E6I`6sL3R0vDYj<%PmcUliWlplHdZ^)xHD3_>*XjGZi^ zsw`|xm-UOC4*D%FJrQFIE*hyqkxUU$Thh*LrC^b;{m)NmGUsktmNDbMq4V}2bpcaF z;%AIeoq8D1FqN8be}1TpC>hI+lnU&p#lq*h$FT7cR*}4jA#?o57&~O#wrWu)VaS)= zq9^gytp(O8iIpIBE;V@9W}LlD!GET#Oi_%9nsFrhM2Jv%bR0gRtii=VKV6?cbXUEW z*=$WeUsOBPdUyBumzEG=na)vVVht1VnYID*``4*vOi%|(RbfuR{O<`ZCC#`&;D7py z-{KJdP{3DWF7nA$ZwAw~3#j|%CHO9~c9QzI;gwM1ucU?yK9*ZTf+X&$RUYiTj<Wad zWq%uMQQaR|YHaR56Y$U6YdWqCzx3h_b7I<=Q<7jQ1aOd8Ojr#(y(+$6Sg@Gc(A9~B z6#6^aXMOK4!$2=l;>zl*xaT#bn@Yb}g{c39%T?oJH_;3H>`y0v(Hp2Bii(_AcJ?8* zSyIKO@b;cw*IYar(ON3evyBX)yj>2dtc_RPyTvU6Xn$=N9ATje9{Rd_A3+VR%RdTs z`MXE;s|{kkKS(=SR6bRDTC97`MRVQ4eBrYprQ?RMff*PN0DUCgCDC2b$n!iyf*py_ zV+Iu|;kAg^irKzAQ`uPPsLv<kf>dBM|Lw0QX>$%Y!1#PMAKpF|3qH&e5DPU$A0jlD zEJeElY?1J_2SPWw#)Kdh+{idMC#g32>2>g%Sd4h|nGME(k_gI8M>A-qhwb)itAI&t zL=vyaU<@EyqksZZ&n;HSyuxR(6*f_wD{o09(&K>vK&!If(}3e`sw&r$7R9J(X#*K! z-G#lYN<aqphy|B{X3N>?RPb~-X^<fvIK5VNJVLMSv5h>=hFY2~-susXY4;PlYGsyp zWQ(1<zki-s5GrEpI(<o8U@m49PO)5C(p+V)&nT;B%2IFJ$VauAS#R^Tg!CI?B*cT; z1r$=9xlr<TBr&gG%32Iak&+~vI;dz;m2~Kj^F%}4S6^?3<nS34*)N8keJ85j%Kq`W z5{2-40G5Co>v$5YNUmCSJd+#=x>Bh>_HYwVKu}OP)EExxdWdLgsZIonAg)JAP6xx% zC-Xu-rjLr}{;;VP3F=R0&kTjKf1^PLikOB<0RSilJnA>Ly2M00VvPd4>?JPL32C_% zg2UyqspjCi{-M9~{or+HcjK~~3j*J{;#!XVQ$##wQ9S9tu)th$87W*F`HVRe4lWAw z-F^{$?1=EPddd>Sxp72T?Zb!J-N<<O?O*WNBlP<jEKA<Vj6df;yk0!p9xiP$HOyzY z8VuWUW^8;2eHr2`b-GwYu?uDh-t`ht%=uX5KDK5#YQRL1ln@7t4GQPL(5^uBR8D3X zxRa<L$;wJglG=ZlS?kJNuT9c4z4hPw;zcSNWDWR1jPqAZC|Z*EVKSGGt^G=2@3g=m zZ9jF(*Wu8hh-EP+7&8R2s|p7v9@MiR4liOXLL3T1$AE@~842j`YeXi7^mPlWz>iid zbHMim!r`PqAjJGNQ=fz8lHok3i+JT_(TUxp+^~6p%Vr5Yi#3Dh_2DG8<k7QoW#-&g zW}3w+UMFThUYcu_%xM9lAnm=0p(6Qs5CVa`V&1{5jwJ;uCrvoKm|XGPn#Z=q5$)91 z{z1Y(_0gnKuKLq=#$=#^GwR`=ip^~fY?j`ZRswVmu8ohqOj|?5RTz7uxY`*cS)KEa z)y#*z2{}#c_MJJjDJ3~X<Yf96ck*$y{t-6F75;ydm{?7hMl>6C_d8!%<9_n}=Wl#3 zX^(0QID~Fk@g%3`R;P>kq71qjSjBHH>RCG7?&1dz;eEd~TQ`q{*`^Vv&J!b0BQgQI z=Bdu0Fq_j%z^NwCg{V!`;lq^h$hh4g-u|&ZpaW4^hEn$)#@Co$NsVD^g3P5XU@GCu zm+V}8c9kF*N{sFntp-dgo=uvG2Erj5;KlaaRu)B}g<lJf^UoMMV-Ak3Tr&hRGlPJ5 zS<Q`@0EGo4gq%yzw9Od|tF3}98}ev*BL?ROi`0%8^B*p&8NVCbgt>z^Hv}4Ae=L}g zshd~Q<6vcwOUjf4fPNF_qmCp}EAb?L=v%dXr36OmQ5l2Ald0(hL+CP?ArbYaKna(@ zYm`V}<z_MgHzFZdU%kdM;j8*{=zspJH)2hw+K?Z>=b6eKWYN#%{3Cr1RLozgi@+eU z@iK<X`@29iu7FZ>LC>+E=w>jwIr4iwOD<CibcRph{zj$X4~8?MSLuTpvc~C*$tTDI z3MZ686JUw%;$gW^-D}~PnDFkUy+0ahnw46Dr;`Six%bsHLz>n79N~95XggO`NAHkK z6mTC(A`oHmM5u((r~qv`aXsN=3@eMwZXUsd3tzPhV$OwTbp)pT2Q`)MxQK|fxnGtH zxXmK^%$?fW4D7PMHm0)LJPK5O{_<vKNP$^UhB!I5ej#$Mx~NXc$Ht;*Z5*6V9lyDR z9>x8BLD-I{nw}^L9~y^5I|AgQK|v$-Gi!^M((=%c?ZQ3TkULW<4|q-={eSDvkCJ*X z5Wo-Z=Zjpcy%-mK!j?MdPT=f?a=GkD1F-{E=)i@08&>+4gw{4vudBQ=VlUC8M51Hm z**t9C{z3G8>xl`B7pI@5(T-f}^$IT|e;qN0zLAj8a9Hh$Dw|75|3Q#0Kr4rIkj9L+ zvX-=ovbM>~XWKfA+5T=WH*3f;TE(C#K=riFT)vm<O=9RAvXL^gJfJ%$**j9RbhVtb zOrqnmEcY)=i(-^sOx4%0a8#u`Ef<A^N5l{;5D|A0JEiS**aJU5$M*kX#)s|y<LId7 zga0t3moo8#u8<~{)>UP|AgOE-G}iq5*Q@uU;;(yYtaM~b%4qv^uD}L?6fMWaQlTZj zo?(x346#djU~q2Pa(%Ai-MYk=|K?l%$gRH!0)7}i%aedy(9LeNK4lmT3^1O^`JKEt zS2*IAliW3iuH=i_Nm34<!zCw2tKEZ09Gm*m%PX43{_o}ae-vTp)k@I7oz&<c;(*D^ z5j@^rsR=Fs135AoxGqUSpF<`A7jQe9hYyV`Yc3zoCfXUz0}}JnlGKf1ifd;TVlaU# z)2q))bhMkMeeAf0tKN6Y6TmZkGTCg<#Q8=fqJ;w!C_$Sv65Qs8tM(0GT>|?pFfEM_ znkX~th)V$q9wE+3gs&PVnBCQjqxu`%);Td<bC<fhwc2uZ-#9O@W5A--`H8UM2EORB zwBj<#Xi4Q<xq{nYIygD?#!N>jMydL(-<hL$KTJw`^ZV03--5UEYnnInQi5WA;`O!l z8X3ZX536erq)T;Gq(Kdbyy;H=*FQtBc`yYJ(Jbk2{TaUOM6cWkVt-Gu87^5FL^x=f zvHopuuY$2LDylg^<dYf`Gtb-}-un-dGa(D}>a_)#B&CoM!po?{wshV6s<^D3@@Uuc zh+Cvz+=p5^3hooSgWhY^i|G>D=)>43AXOYm+O6D*S$u-S2A8o<<QfYRhg@esG81T+ zs)a9T5u;bb9c{c^gIVSA@DIB&yXCAMD(BL<WZ+$h9uk3%!f<!a%o)th(uwKE!i$d~ z(TjI=_e^e|tLHzn9O@on84ea3>$2PkZ`M2X|6teV`C*gs#c?&|=h9WQ?Vr}-LY_fi zjQd8fe|&z4EbvpP>3IU~yiBK&R$KMP*L_bPZ>PJ5{C?qcfte>6_Hd)3kEg&s!FSSX zwdwtMQo5*6Wmb~_Ph?a3lGRW$rerq)5G7a!LM#bRsOzV=A)<kJ5+ehHBulbz!u#N0 z2|-#^@PUqsL+BJ}jzET`g<>WX<tLUk$ncVY!Jg*w3&B(tvbTc=kv^p~?11Ikdf=~4 zo8N+YbZ9X7jDOj_9b@w%@mF8ZofWSbuk?!SK1Bf_Lb%|(_+B_TShi@O0vGt#zz7#? zqx`v7aeG-sI<h4y!pI`$uhpcTq>{isqN*&D1duEfjy7*!&pZisv?4VO3|i--V94MX zV~c8@CETvc4@CIu2^=S&Cq=|A(?r#ikiKn^_<wv}t>O7@VczP`$UIqxqzF)ZF%%SH zu)a0rY)F6c1QoJ8Vdlp8nt*E~6-#^5_qg%krSz-`B!go4OZ9NUNm}oPY>y)ff_4Kg zFc1N`htVVD5Ea1arH#;jL}3**L4o*_B}|GN$Cn~_vldHSsqzeo#}?t~d~V>wyMl1T zREdV-HU*|;Zk8zdJ>{Ecd5?{nE>`{&+h+{#=#SB?nyXWB@;c%;-sDimeN19ok)ZdK z&4jQRN=F~pEF1sK2-V9VvNIVg*evO=2HCG}(?MrQZa?^B_CHSj#_%;%66gFCUGAkM z(B9a@t0Y`6+gWA-OfPF{5loxxKvN4x5u1hLc`#GK;~`L{ujZ!*Bcn)2(pmHG5&f|? zMq~bOe%?jvmLHpObp?RH4w(6-9L*qs8O|L|Bp9v?;6Y?3hXEoY22$ne>uVz$E06U1 z_k<jyC8V$XN|JFNjZO-Tf+;RxMogOmMH70tORd;f*EFQe2b1UlZ-~kP!Qeiz>|b>@ zSs39nG3wh9-<C2ICc&dAQ^@B-ljss}Q~kVU5>$nG%Gk<wK@36f&z!~^u{<>3zqOWR zpLl4XZHz7;m>8<hc^$~d>}s*l+A#*#gEpL%`9k6sHP>lvQ1$C#)uPy4F+cgeJ`Q)k zcD-ec*PUcjmnS5E)m;wVX`7K`(ZKS}bZE!c^FeC2?)2%>_#=#-*AJ~lpf-XCb0V96 zf<AaYg<#X(x%KNZt4Bt&mwoZ7vst3_<CjCd|K>Mn#h&;96jSf_t60Cj{Ls;AwVcv6 zneu(&940qQVd-SD`_k9Uyh($gKrd#^tA|psv?cjeSCy}jkc`Jn)|A}NpFf0*a=XKr zSRx9to#Y{~B?QP&9hR8*TN5YaAoqS?Bq?g?N6h8PvM5%BR@uI2Gz>F!O{0F0ENKU) zG4`=Jr-;~aZ_CH(AcWL&95*t?^C$zfZ!<bewLc49Z3)l)ncP0}7}RllaB(Wgj@Jw- zCs52BSX5Tb?WFG`)3^L(aeFsC+&y&X@KYcp^HqNtEBB-3y(UpzraEYWhjY2!?#q+m zVo58Un&Zj@Jsois?{{)Sg|R68wDoF_jIZxPt!PsYnhU(fQ_2MULTmgmLii_s+Kx== zL})($_x@rQg6BI%?qAs$Pg}O|-ol7*B=u=x)ZDEXxVh7D_i>(5n?*ICO^J(J;5|~I z%A%zK;qx0zBuh(m&ci0`nN{RMFoRSG5M*Pd>*C>OyjUd!v-$OzlV<Y<9SjY8y*{c* zOBPYddMS+TFRuyE=j?<im^zJ33O1Xv?-a}GE9)u2=Knod04^{lQnQkp@tBZh^l0lH zlulKArwzf2&H2_+<TfLA9g4;U=J(hSN<us?9|juf?U3M?26w2IVxibyXL|Qz>VML- zT}%Uq4@TMkUPZje17U#4Ex0*LcEcjZnf1pc#Zd(NF=ZAh>?6hA)6Re#<7&E!QA!Eu z?sbwgEsyQ@=#`bf$FwLFn9u78FtZ@q>8Gg~u>4nFmF9uxyL0K&CZptywG>KJlTxS) zf(w}&+wDLj;9XZ_WiD*`s!0F6zM&`QExHTI*;dY5U2hrCvBwd%s<R_#X?B-X#Mj8a zZ@6tLNQSSjZLTpiW8FTtDAi;n*){k|WIRc)XPn(%M)5GKXkAXQyc5Z|?^Iah7(4h7 zuo;l;Zn}h>*;y4((QLhE<VOG4{9E^co)o^$Kh=oH{J5xqT0+<KkNe1|#I9M0dfAPY z#IP}UMaZG`G(pctHTCB9o$86H!?J3t)a)`7S9%(aTHXsZU<&H-GQ`E(y);$X`?&m2 z{{t{`u%WITf|NdZ)88&aPcTkF(P*|56cjk>;k+{uTh^5P6O!2mN8<F&PdZmjNevpG zp|9ihpFSm!FPQJVtWO(n!EmI!WOCPLkIpIEv;}`+I`R@6o(V*WDv^I?9Mlu`B9lH{ z_i9nh4`oO!xXc&aFKWMN8K<O7W*@3LV2_P>7e&CD%uht?5*bS__RhOf@ts5BsfCPu z+|H10w6vrq``1KrQ`iMm>R9}fnjanVd;-^zOCY&36Q)+$v8D!FqcZ1oV_vcT5nYWX zGS8|Tl`-x53-587DOB&1LiGA_?J{vZlDePurD^KMgIj_s4NKhXNyiOKAa}W%ObA4# z%$<ET>U<;=kHzI_g4d`zUo|ReWIT#gg@z)_e$*L(p`eP1ii&~fuadcq@m8iwA{D`> zUJv{cZk=eTc?&HM4o#<P*@FqE>)ZmBEG#z~k+I~z^Q)F;fbVv$f9~lwuEiq9+z?q! z-6v`kXt{J!RT$T9%*ev2nyPsO6_1omEXm$P-bf-nGZr<4B$Z(aSY{0tCqOcM%9uKf z950V0Czr-E?~9WT9lMGt3GH9mH5M}kdvv>!=5G-n@Wo0%0h9Ft1YV{MynGy@wh;CT zdSN1s>kmo-OZ#1;IVKEv^ylud2M&sVxsZ<REzC$!S%g}N_C0osG}%QA2HPV&mI;7D zu)c1c4{c5tiFmOX(r0Q}vg<lYr$znJk^<_vHVb?6qLF$%>J8=Uj&`UeT65(;NM#po z8?EJGJ~pa$#hlNWF}gF6y9R&ZVJ;w@dr&rcCYC<uR$H0YRCy2pKUfgHgXG%Gn?;Js zT!Om5Cp-W69EsWU?Z5f=lKHr3E-H|KY$STdHKk;(=EL;fEb^#_($KSGqE5pqh6dMJ zP+$`nPR9-&lqBg7TJt;N)<^M55@j!@*ZfTrt)8p8J0ed84c$I^wtMEzdy{XWgoyK9 zRyi6NG_p!(J_`hY(`aDPRA60DceAWWp0LNdj*;h-<-w0!reid&r;JqdV>u~7NVO}? zTmmOJ`8O=}ZL4>DJ@zu}^g)y^jnj4)6UsY>9+_b!r_86!o<dCK#Z|8s<A;|3$!ro| zzl6p&XgU|!jaO2^+A5k^(gyVUiaLC->2XsO6cG%r`K0Dr5AQ;khE1aTP+)BNSB+-4 zJZx!Di<+33H=i~}NJ>P`DOgh9X@{R^S`G6Ao^xrb`oH_3s6F3-$eaJXgd)wRDHj~= zRSp+^JYw_INy4>i1qpWiSMHwNcQL|=za151&PNVunvdT>ZJ8v2-p%6CI22RlxhcbR z@4s~ke&@ENxK&|FmY-t79a{tymb`CUM?I2e#_vmzOfk!1F3!MN2g|BOs*Gr<deKDC zl1Aw!U`erNj+^R`Y==WOL3~m970P6L$gGkwleF~xpl=SfgjZjT3D)0#%7;#q7s~!^ z)?KP)Fk@-vB*WG=6gO+H0C}M#KZZW==J;U8w=nGu>U>Rffrx#*-zSKlBK>Gnuo!mB zk#wSj9-A90K(L+2zcVvd%j4g7^Wr;`C-+gN?BoVsEcolrwbTZ;jXe@FT5=XoOC{#T zh@^|NE^e3bKR(YA`h2IrH~(IF@xn~>>?(`71b2jb@t9N~=TzJsb%BBc-4s2bx8ZN| z4}ndehQujJQW>0RU<{I29@vv8!X3SH!ImWAB$&XMPblOUk~01`C1G>T9@g-jy3U!I zTEzsMaGHUm$zrl)FkO})B~~bnT(tv{9}&BS%7}K#@trL&*+)VQ@5xw5XeCQUApvdA z#!W-QRRt&YKA<I?O|B6mCPd7Y2H!>lGlm<+d?GtCWULB8Ymk*`M-na9jBWo>t*j<p zj1r_5<68dF?JTkL!9<B&R96^<B~8)M;Sw67gV9<s^e*G{Zeq0m!GBSVCj>H;_1@~S z;Hj`7N$zUXqw!fW$r(a#k~<$~$DpZh!`mSgzdXG#_Fw(*Mf%IX|1Zc_LP0T)nYTE; zCTiKif~Oy#3l5PQeZ1+}p!?v!#DQKprpCCgKewvH5r<10{-*3@Ko5FdeAprVhCmZq z_z#Aj)MV|vl=$!ge`+{!7s??qGBR~GKp9$#`s@lUpjU#$)vr{o(kAV9@AQp?`ggC8 z+cDw@aI1pRQA;HYis&g-{7*lU5Xo88*36eAuA@lh*op``RJt^`3)r4$7n3U~rw(wq z={y3uD>u}X<qvkG%+;+g?PLg(`(oTm6J~3Eh%BRi`z^%J(%ZFn`!e-MAx!O-Pi6;p zzdfO_$3d&D09)4}UqyfKdj#N8jgzosA(EJwkF1rwl<sRiIc(FtZ&9Bta2fm2S)yWc zP>yJ1;9llGf6*JstpEPM*l;TgfEgX}V&d7ag-F`lYH|&!)9OMC$>?P(*8J)!>zaJX zxtSV1fVDwvY_VioP8V!3%hbDvDu-SipcN8)MDc@uj%?z0nh%MedBW_im*tgWK8L64 zYttukSyWJyS8iY;=pDAu&M&F~uYPz{6c9IXbV{#07<rBdbE70yJ3vZqwGia>$j7+q zUz(Ei1t3f%Y9OdQoZv~LLWiXwTnxM;ucm;`e5$6T*Y3u==C_H#uDNvBev$-_1j9KP zp?(jbi=MU-ErvF>4%XX$=nKkS{w2zt0te!hO3ZkXi%UB4XJ@#U;zL?IN5D+f(c(&q zuE;H%XR>9~Aa=v>FBQW0$6f<VJtK*V&5&Zq`!nhP^l?AMZ~x<it~Iua|79Vt5l44t zWoC$qJR|nfYaupeDB1KV#9NBvJwBsl>~H;vE7L^VJ<d=#Ny@&tOtWtsK=|mGLfpGR z@vKlLr&>?fc9Z{yv-b*z`wP2<L-bxouY<wpJx1@w7;Thj5xo;Vh)9UujTxP2ql{jo zr0I-agNVQ0B?w7~B%kDYpYOWf)A#6iFh{elwfDaFeXqS1WJWA&_-NmYcH7fJzLiR@ z%9o-pEi_x!WJo4BEaP(G>xIOnm0qbg|JTIbnxxp?$c6_%utM{_mX~DNHOyn4lHS9m zOCj%Nc`|z>giXkZh0uqDu3SL!9zhhG!u`YDBxjfqS&>DZy}g-8k%(@q{#_Be?W+bk z<eK6hpJ%f;B)`E?>9ouD9`zDvCLlz#6SG;y;J+ZASIj<S79<LLQdjF<XAX80=|Ww@ z8?x71s@=^r9ZiH&N8sMnx8BBG4#7mM8<a7#D}VQ&bFKfuhkbWFNpmyn8>xLoK69a) zuTq*hh;Gu0tW(!p&Wx=2fg>@cK)^$uc00CA?QI&1+IIkphi=y_n8zaTs{i97bzxA$ zZYT_^JCuzgmM)BVGj!)@OS9T?B#y1)r1`sbuQw&GMC#a4POH*UrArCRl$vhdIdC?9 zvsaEN(5Bj{nFDNT^?l|;v!*I|=g2bsBU}2Z3=}M^OXWpqzof3L@$PK+WR(R5nwX86 zE}kNO!~u&ARL-?2up&gmvH}o{cO!~{vx3WdM|dCIur9mIJ|w);bN+DP7>+opztr%F zbnR^hoI92Ykc@X|KnHK}2#@yYxNa0GypJYaK6{7vQQA|jJ`daqX>M=2@pFl;&NRpS zi_h8r;m@jHRZvD`Pjs$HtEhHb>?m#CjfHc>5Po|=Knwh;9s{1{(!?9HvAn*@z6Y*m zI0^YrP}EEI+vSxHn^oYIh@~5UtX+>um-QDnvpR8ple_)mJU{Zs(wr>eudR~ZyK*Pk zrVys=(Hz?6J8f+~ZuN{D2=r1nYe09v{Q^dxrTzyF$USp)4uVd%Nlj2xQCC%PSTD{^ zC@k9sZj|_=!18I$+P8ufPtuTv`;okL4kwR3b&_h{+R@Ma7wi}`D;wXjq*TuR6l3{z zl<_T_?%B(a-c5=EZb{+#6fUA=CCQ(FeA#r<JLJ@yZgVD;zsH`hJZV$~Rfhwc@`lW* z=}}m%LV>WG_fOP-+aHL3UWm#+T+;dH|8s!|g526_aVd@CF+&@&Q>AQ77DG^^JfFLi z4J`XJnuYXPAcBa5ui7XypKkN(Mpu84k+4u%Gl2q!rUFQ!*MVXC6Ma_DE}_Y|e#(Vx z)E!6Oc(%OHevDf;uhaS;kO&AUT<LuJF`&quipzIG4LBmRpOJev=dAcoSUnBh_y9JU zQph^}Nc+IZbmNY-yRq~yiM94U^`0?T8*+veVZP>e{|#XTdDubew{(a4x1R^Q9M`=X zZPiN~e|%LitWbJ}7HAF@3;M)3K`G*K8}Fj#75?;j$MJ?e_~G{s!x{T?Q0qdDxSQ9k zB~}U~6vz2Zx$M|4=PaUt@Ziu_XzmMjZ(vN|%f@7or1MiXU)y#G+OlS|hsRv}-7)|4 zy>Cn^AoC-OoHt(dAq<eozOp?96N?|Osckl?&6df`3I=E3MT&%774MB85OGy|L1&GL zts<08b(nYxpQqrFL04-7`IfwAj37$c?Rgw><h_BU@YD-D(nksjPwX=*NbrMhblW*e zU3lJ9?+38JD@43Nw`pb|OD6$#^XSDP{rfQbx)UG~H_D5hA6RUr$vJ9H#v|nW8zYP? zcIV^L4Oq1yZQ)E&FcaEJt$E(~TxqRVPe^@(L_>KTf6azA>1$@i>?325oC~Z-k`t3G z4{5tvKsiFauuj2t@dLGk3?l*eTXz0Cdm5uoW_N6mEilFDa~dyd!G5i%#6z{8Rw@Qt zAcYZA3Jjh#Pj77|4L+{$&)w|AC@tR@+P~{7(MSfeD>KG<XKB-nNg7nI*!olqM^Ziz zzCpH$st(Yg)X{PCir$PQmG692R|i_C9Zr&X$1*Ie{@D|`ksr*TVC5}K^Ys&ff0Bl} z%rllw2obiKs`Q!W{b;&K!G!QG)_@`r?fFr&rs1?$c2`e!gOo%v_mm~gpDj7!wp2m* zHCnT9UNL}Z9j@}8>w$zO)d-BS?rq@nuy?kDd3o<goV9q($}pQJc>}|V923uvA%^#P z!KewH=2s0uXUs$tGhn(KCcIubmkZ+y9h*pG>kkt?kELHzX_esNUQxY|T7C8*?E4p^ zNsptJWR^GeroU-Hcmt!O)qN<d%*Y5zW$HFMFKk?`1!8U*6j3`uMMX8*j{BRlscMxw zfAM*!69m}_xXr)NA76<pj`KvfJKx+Z78nn(Yp{{p^%DbaXe_De=|gXdTn1x;<+^?E zbH&RfI&QBHV2si#+r#O0*Loa7Gp2$rm3B)kLp5?9Eg*6*PkCk8MPjdJzA{gnV_CL7 zx#@zIy~ye;w8c7bx%ml~)^kbO+N(!rc_Nn^-wI79Cn(<xnmMQj1n3ClGn@~mZ6hOp zPwCI=Xr0@gPF9%w2%c>=jZfaul=3?_5NEIQ_jx{9chBr7J6Ipk_l!X}Dd5&z*2Cr( zM;xfFC6`e;((@t`x$4}IF>_jjTxqjcvX3ybtV1fvw7tD@i}EttFwP{($r_5Kofjn@ z68fCm4Kr~YtHc@c3YD^_<+|<xXt_E1?Em@OSZeS=wr}ghRntIdjx=6rGyF<k53ome z!#_gh=fvf2-zNPo()7Xc=)LBpm(-!60ryJD7QFJiMkf;A08lt(_+uHh+3JiQAAPAB z)^txTkM3-;>m7;09E<0Hu*UGm(hUw)+|9flZii&N<?YH2koiiXcI(S|U1^OJEcRZT zSce)Hys4sv7P!F>Ygg&uR<`~}M_`$m4o0~13ea!kEr+SrkN=Y1Wo1>~E?%A^z@uF6 zBs6@2#R!Z?Pa>`&(uz%u=rKuSZmk8wj|N9B=8>+&gxfpJ4DVl~IQc*V?&mR;YW`g; z-YHQm2YCWo%}`Rl#97*CeEV|_*-SAtAznHOSYpT%kw#(s1loq0yMnZQrP{_R9H9q) z`Qxbk8nS<<&+Z0HL@D2tI?<sE037Kbb)E(S4{Qcaq=%GQ9ko}j`5BU99np-3N4_{h z&1V7K8~H~bugieq?GFN`@O<9*^pdvZ&q{RYBBWK!M5GSP)>p=JU5$93u*+eDqL?}i zRLp16_FD5hWB}vDBr;E3T^Z|eJppC1Z)LC}pQn}==y%MeDuuc%CR`$H<leT34v~+2 zmU0N;XPEKKzytd%G8`;97c1SL({-vZb9o0ye0=6TNbO$|GOk*D!0uF|=i}t#hQNyD ztZvIjbZyPz63ptk>gs|1b9}YbFL(F?Op_!B^{L&aXpT=`W@1CbaCJ&9E$4F6Hs8{8 zT!|Y+%qbm(m(C0Qnzr<Zi+nT!m*;E0UHru-QnCW_3-yFjiSD=XX<^rsreW4AIa^`W zGL#I7^m071I_P%anq3b0E+n5>DW~BopK9*#Ww@!;(BRKKr*BTR80;%W!yzNHcopa+ zz<ZNMuv=iDd(+clet40Mw$DOc6r`#k2cnM6T>n-lC=7;c3sq@t?cwNhYc$Pf$u^Xl zrhl5JyWHNmQjLF=;Sd#q=&+|OV??Kd);3U0A51f+4^px*?Nfa1>Gt-aR7hHE-xhHv z>Uf7`4)lakdq?|GhHU&)D!h72%Un=y$a^d?S)j^n-Wgefx>bQ*;0yI`r&lJV0ZJQ{ zwO+3sp+dh0l-<j2R8GMrT0hPh86wJkNnTo1john=#V%@D`vbq;)sUxaq@iyyod0*e z<CNfk`D01Ki(ea31505Ov8Z|B!cSHm=GYtEI`1wpnpW;t4Oe9V)+bRTOnDa6Kz>zU z*a#h#<rZl596Uob^wWkqS_-d}a`n#XYz?kjRxO2I!|B|5h0JQ^#)Ss04-oEeqYZ6R z%JTpNP_oCPvFw7C5OImQ&v}FO3cnz2qMMzo_v;$w+^$9<-WjsqtrvVOElIJof4TMa z;Knvh*8N#JawGHG<_2YjXZDQSBU6Ti==H$74JC}B@QR*@X^o{%4MPYtcc416aCA3U zS2Ht65}0Lf!dz4lCjhlDt-5B#B4lVi%T60WIHcpl65uJ6jgI^EaRv-&;~Wr@8z`1| zKZ)O2zUqGE^jBH*Yg{@ZNLuG^%|Cz0BRD%`sd|o+g7H%6{Srns7E(8)D^W`mgJrJJ zhpLovcwY9^P%2omf3A=y2PS-*FPJ-RD#8r5Z`9QonaeG{*PrhXj<pUzYMs9p^*-bs zLX?celv@JI`0(FMC4)!pMj^m0ffcla{MGj)cirJ+Oqmqx3p@*P*_ft)JQsZ(38c}n zvxP^i3pNcf9NX3vh%R}m_wm?U8>y=)Kun|^mp)o+NRO_^Nj$yRZ(8m?Y4^cFz(UfW z{atz9P$g9Ub-N}RX(soOYq1Q^c`DZ;hq%Y9D9b$C7<f#mb6w$&K}Yjs^4zz>DNVp` z=j&@Z3k9!<l;h5u{p~AAD}U8Yfo-(`2YtDt_lzEidu*FNw~GT{a4rCEfKvJI`kGKY zzU5DEE=3?gRXvag%r$Lj8{+EcdVRq+Sia*cP!shPI6xrHaP-uZQvMU_Yo$5lT;emT z!xb)1owH=l!deq>1<5VBH&wOo6&?{>y35*?aA+Z9i5}ngT}+ZC)7p97C{Ob}&rzrF zqph;kWI>lkcy-9q^!&5hYN(+mOI<iY##=C;K^PrA>%Qg8f8gl-jDDM}-dg=`qJkQU z<FOvnEg)7)G+pb*(@F#RUAA`hx=m~#ywuQhcz_hGy2;@_2Q&x?O>N*mo1Go4_5gTH zsbveMn}q{v1NA@~m2-y!HNWGO#F_vp&c78ZBKOfi&#Mi=H3?3)@O!Mn9UlY3aI_h# z@u1v{k6AwwDPPs8$ZWhMrWYm~{fp13p3=X3D~jmjzNDpC`Qlm8x>p<`IP|v>DW2Rq zeJABSmHQ&#aP9)&zQ4{l%)-EL0nhoQC^F6D#N4(2e*pczmaP9TUm<n$vkDCc!*Wg6 zKoCz*-*`y5g<_nN$QGw_BO!lV3QX^b`(f7WswnJP&-c&;SxJT203KLSRJ>$O8VtO+ z_tUtJa(g<(n7D<Ej5vS{JH@S+&B)24NzH!XrzgSZNC5=68i#9kx}a1_e35~@Rilo5 zkIPmbRcyp*P^&Bz<S8QuE~aRygWX*xxTG?GUcvX>=B9C-|IV*GVJ!txlt;#D*`a8| z@btv=4=pY|`fP_9t*WL0UWH2uH3IqX`|4lImqH3E&ngS0SYn&HiikJ2*J`|#m;@bZ zU}Iymu66c^h8^G-J&%sm=~pphz25agc<GV$6}4@vB-UJ{4sJFh_0Ut9w2beKfHkXC z+V?WKN!L7~7c+tn;igR^{QOD&E3C6Ms)@i6U*xt-r5PbTX<<?vEDm2(zADZoN7gGr zIJtE3y&RL~^v#x<a9+;jSF>y;)7Vj1s<i&qsg0$%fq+N&Lj}4%X4_;kK&_PvzZdby z`f~~HmstEI#5?TH8<`*?5k#U0)<?Wb0aG~0*YMGr_?*?M1FRjD;#8fCzY)3($J0)| zKADxYy<gNv;*s{3KYxq>|MH3M?DUoy6_^O*`(bsn2$v@-9HmovE(}WLy7)?~@7~#z zp{}snv?Y!p2I|Ywlo{xL2U`tzY=}$I4STVuN_qY-k%Z_et@u-V2&N}UoQ*Y}U%^aB z`Eq0lOa(_WAu@h+asKs8ZN<Y<TQDl$aYNpHnf;Sa4ir>CCTD@`$|KldjHaIEQ}gn& zp2AEL%?)3-)s6_2m$7bNk$#A(C{uEIPm*9{81JZom82$HEQ2uZ+s#gyLNrD}(UYrA z8l8d$6fzo*5H|9IIZ)^?`vS8*ZVyyo@w)WQl%@4lpg<$|;#3fr16$q}tj}N<&$A<v zeZ00N@6e1;_Mm5OcY+nadI#f*{5ao+oKmq$`nUhcEB(*>tBcygIJL+!Ak&LvO}yO2 zWz8DW%F^xnrABWVTzy8^f}s1DI2aTyh=lOTG8Qx8VQ18)`{GRmmNF`4yAbp760H&v z{f=RP9`Gj#MJZ0+34qK`dx$j}PU=bsej8<xdsrvOrRSnKbSb>p62?NwpW;JBPSl^5 zyBw6KCcogk{X|qQCA#oET|7|M#TQ5xAh@dmbD^sQJ4Wv6R?~_RduA&Ro*%BQ7cae* z+0zQHY=gj%2AaUbz$--VJ<}Ixm00ZbcSBwzV9(Ya8}J!wWi*0g<OSP07kVrV@KrFh zA^}9QA$Bm$Ee_JFQZYEJ<<&tmDqCFmF)JWBnPucVgVT#b%Ojx``z3!14Ogt($v^%; zDIh1Peolb3k$g3C!Ngu|mp$+Z0|3n`%H3Cdn)QXCEMy)i^UbOVOR6tfHF7ghS@J8# zN;o%ORA4#JI<c(b2?s@ES@Uq6O!&bwk;5A6iv*kxYdp%Y8rN;mRTLm>#i-b|8SFXt zHSwV470SWJK(pLT;BM^ghVUz9cX7VPd==-fJ20>Pvvw{S)x7eQX|Kn{9~2M1qYNN@ z%y)EU@LKR?#eCLoJnyCV=wQ1fYZ0{~pT&^27PQ^$3#_Hoh!}+M<Og+8xv`b+`@+o{ z&3SN3{=xJbvexm^l1(4=pv~ji0&tQ`UY5m3P${PnY7=-K>!T}F8jurmpm0%Q+l2kN z-~%_ZINmO{P>t@3oV+5nGlEtA(;uE`9{<a)q|ktvo|&H+$r0jYRu%S4dG2r2J`_y8 z#?)tyw#3pckl|qox#!-0_o&E}M-5U(H;7aCk~TqkUZx6L@(4+b4e<(=$M%9{MXqc^ z>B!a!dxcm^WBWUpsi!JKWa-Y2i*rv+lfKhlzYi>~HlfazB#=&y-h{~WEjO3BRM)98 znv{=LguJtTnKh5j_nfsg7)_`;u-33$oXq4gSltYd)ip(p3q+M5W%8!zQRs$3U@KFf zXQ%N?5@&|`hC=o%5zNBze8|IXmvq4A<te<>QL(kAWDA&GLq^iCydYTdJ!uGbIn!-( zw#Kl*{9b)shdf{2+)P>fjFlYetv{!6g9f`v76W`5=%LC=_wfaJh1p5wU;bR^h(LA_ zW1P0dvTH>9uR9Gzoi~p#3cCRQu~yvx4OY&fw3cStgX_b?(X>G_jq;E^dut1bk|3{e zaS_^(E2JfmcZ?;)j)j!YOC(nyJ0%pnv`UI>hlk4b`a8-tIOXXcN*Y_5q@}s^{k$YE zvZc>l=(IHAF(C8|qXS{IfVE^qh+S{K(*_@xq@_w9KfCZiSutc2?eP1|$+#@XZ79{| z@nao+or9<0kuOPjb~hiRj=>?0c+%h8ccUG}Y$h#9Y@B#-+A6u)Ld-PV<ljIm=GK;s zn?mQdZv5giaO~YPYmA0%I^*{hjpY!<$$%OW|19-^+5jgDCDFQb-aKO|=~{<unZstY zWQ!#Qt5vi(yQQ@~IZtL3>0f+4LysY=xAQ@Yq#z$eETkxnh=_f$AYEHVrrpf5^~|tR zi5W#PNmQ2ER-s30G$oLlnM<3QBFedo6`KR@YP<7rO^HovzCq!{omdmcszCH+G@tm= z9(pR8x`?BCz8hqfQu;A3adovfwLlCnn-4LG+S+sUJU+#2-YgEw3+1vhpQ3qAMvvu* ze-db2IBPC`t4a(lKnRl7N*$jXzNSV00xI)_iBA`n*1mYCu*uthQCgf%(dHR$QVJoT zu}6jQ=auqWM1lIX%Tr0omedfudaI2(SAL&1&Qeu5C)-}(eQW@^)ASR1ay18ZTBeaS zn8e)Nyj@w6I3MiK)#U5{c$Ai6QOTMMl#LzSk?wDMOqdt*3z^kOR{;Bq&nFdI$o|N! zej~<AT}HI`*mz;H-z6#>U6d5};#REIXx&*C^7!!Y@pN0-D44Xo+kx?lPU_S5Sm)mt zOt%_lb{#0Q3UT}-8UY*!S2v6b{B$*^jjPuco%^g(DD=kCi?T-dOP*grN5+uQd*8A* zemyd2F%+S@ZHD8Rm~t6Lv)%JpXkVnHNDGVJVAcysRL!b%)_|-+X`m*{q&C^>^Mo4n z+FYY~y23q%n!5|tk2(dcR?~y+HrWYz=vfF>KTu+rZL^(9{istq^x5re?aR?4vDPdu z^TBjzK5)6HumKIsZB;_4o05hxYo^?swk3r>Kby9Klv&1EaJds)mu<<e#oGm{V^$hH zCxliPb%gOSptv7t8L{a6#pjFKYse+4e@+SNBbYD9SPd}b73g0Nbe685)ewhP{7|Dh zC^G~U6o?82FvqLtaUGNNV(V0xk_dZSo~u#^x5*Uf3N)g|0;>#n2?O3_>~o))9$3)V zXX?cBnx;6P)u03N3<Ryv*yWrj(1ziw$%p!Q%Ynu^RPjXBxPAjwx{eU6uZa7OLsFQU zHVfhUBuF%x9cSf|XPm?2tatNZhWxw+=OsE{SQ9^6suU7jIWNDZobqD(#Mp4<M8fY? z4)N3smOhQd1R}Mf!1)lMy9G|`3%Bp7Vv9R;Fl&3eJ4l=I!^xru8~FSt4jWVy`Aer; zbzPWtEP5spIw1^>5LuzxX7g+&_i|QbpRcmQ8O`iwBF8%)jI25T%V+!pOZm6HNQ!nn z=>}3CM((d^t{5zMWhI?(p8mOCG|Up|_@MA8k0m=zuJUUXU-|oQ_l1b$(CN{PW=-?S zyy#Ky{fJbh9FlC(Y?=cHNWQ@4M`1SDhCdIWR@Vx?HVC|q<eHg@Pt+Q~=ob>keN`>o z@O6b(pebA&yQq6+i#&=bG<bypf+mq@)IwyjcXY0gZmrGIRzv2!@LCt!bXg6Z;6U{s zi3uh^aLAShU3b7EiZca-QOTOC+`wzbIMEz!J{f}epupuj756?XF3gyY8vpUp8H&V~ zC%N|wq$Rep)(_wwhzav---wTF`JX@VkT=>CbTKJ1Fd6BL{|@COI6X4Kok^7Fr&kV` zL?o;<@@J)OB}slV|I42}U5eZJX_*~?KAhKD^OL=U;X^w?kQm(9L00BKr%}{;eu~yp z!`odM%I2{J9PDiXiYOUXVVQ1Qy`zPp=Inj~l{>jO;SYsX@3dpq5Zy}E7}R<73(#PW zzq;`t^L}BV^=3;um-EFh*1nakuXC{!h1prsmUJt_iGmjP8RnwJV8a)WjXz0+=J+Xu z%V$#oK<teH8hHMTy4v)@PtLy(65Mpl@lX$sB?FH{J$AxXcp|LlVX{_QyLCpd3PoYE zJ}9v{Ve!C^pO@))l9aautpq(~7I5b9+JeavKVx0!KunES?V>It-2_f=9fxad`7Asl z|NO+_KhiW_R??qAP{jP}u73AG`zV&C9}$BwwnPVJHip^=*yX?dM=b)fkL~AN-9Ff# zS>8?u{EpmCP32=jGo*XaeHKR>^K`TcPh<M$7kT^K!bu+>euP{&`q)RCY1y-Ia>i$- z8CXi|K-i<n;AA^b3PrT8qO+;T$@iUhMmMbx?)CH^?#s>U^2u&@_s4u@K=5rfHk*i( zxqXMAo2lsQ2N4B*n-?%3F)RP$PWKdtw=p$3ToqTOQ0NfW15uC+Zr3E$cw1)Hu9n%$ z!J&>fa%v*;UMc^h*h}D{dH7h;gY&RnzSh9Qy}qtY2e@6C*cn9*4_dg2=X$fCbY&?) zx1+TYY-vdz{rWA>n()G7ZE2~h`H`JVh4cE2BNn>aZXedGJFl(#;DczYV2lH6%pA-6 z9<ZeelEcg841WF>pU>Js|LPwl`nfZCOhB8Qo9A})n{7q_l6m%hj0E?pSpDKW4g-8? z(V_F8g%+hE_$g0KYwi`CN*_2=ic_=KbBgd`aN8PKNZi(A&Wy*6;Wg}<GReaana4Wi zQr08$4Lla$0qb3!N>uYLoNFNiF6LsZ`XA|n*(((8e?kqFd{&w}#5f3=i!Jd=admjQ zrM-SzY@w5B?JI^ZbB|@5_GCJL-^EY-g88kHJaRuNZvpm;)^Hx&Hz~Y3TsgL=?L*BJ z$J4IUcCltwM%SDa;uvATVuX6$+hYTpJ^P^s@OW`j_bZ_M1;#NEcA0Q)LZw)dxvLpV z<;*XVp0@~++)-lAo#M+qHE3y=@lMXz*D9N8XH^)!o2UL4pCjnN|J>gnd&xzlXt4_o zfH;k<1`VXPe<h?ELpaAKA5rb>w-%8}mwXFBG7o=v0LrzXx~I7r58lKfuzZ2s&*#ZZ z!Y_N~&c=}4Js~^yDut#NCc(Im2@byH-{eJZ#l8#vrU7r#=KEPi_Pdwkj(sJpvCW}2 zeq%aYo(k=~&1DLyI{l0u9LubibSH?-$1GTl>#M?_-fsFlR>V|x-9B2vABau8gMiQ1 z!{JnJO(Q$971OQ((Kf8xS7dj#26C}LWbA$YyRzc^hLOmsVCArN|JFZtzPU?h?S4!g z(z>-z1BdzH+$zou1g|{ZYxJpVbPgu#UI6MaRB4u=?hB(g=1qyiIx1+&;ZMyA@s*;h zXnhiL74TntJ}L)6j%WLqZd6JR&xbb!kE-I0#6;;nox|v8_6{cyZJnXvqy{Y>N|Q_* zuU0f2zEu=`&&ik+zp#+&(=xfEB^G0%WXLtZPMR;z&6dWmcFKJxX0`;HP|PX9pyaq> z9Q9I6&DUHY1E(GE98od2(D#+M<uu4rfaB9nI~TEpw*WP@I4_vcJf9otk?-D45={LA z2(x2PET=fkA{|KQ`l5fn{)^K%gFMzLtg_nDv?>$*915Ud)dr>rm~DHokbJ3OGauA7 zd(?F(OCHen2xEG%W4&ATlN}=i7@{)o6=B#7iY8TQt57vf{Mz*fUT3dbutXc@>@;K6 z2(K+pBP<IxMe=iMz+@8I8yXD3PJQB!QZDqybG*a;<?H>>t@y{E)t95g2{Dr6J%-it zMYRljuM^wnE6?glYrtgEgZdRtUFHc%@CWHw6~6ZQL=It#3|IxNd|OK$mKimnI1$f~ zqHD%y8*cc1KSnlqutPjcuyF!T{LSWJMd!fj*>mZtXo+=HHGB3v=?fYj8|x)*{%$i- z$^7Hb#CemL7~#DQpv(+r{)~y9!R4T=P$M=>%@Y{kH4G7-v)r0~ugc6i#uAV=`M%sy zAzhiPtR1b(K4~Pa=_m|$p<*5Ngck6YzkkH<X5YYT9BXQ7E#L84lfc35mny)wF5f4m zVisId@1Sfa$hN#}-?I9pBH3Vc@gm6vUQ2xV#x7emowd1^vt-#?;-eKg+09c4^VDv~ zsIM|DfBAE)%Lh3K>gOyJ;C+|czLy-IZvS3Un0f2q2hLs4GwMyWQ0M8dl3n>vkhh`h z-4;U07uBz@k3Z8+vHovMgS)cbb28D#QN_=YHV;2KDyva|-&mWN_k|{jGzDXxaL~&@ zWTDr*Xzpf2TLy-R$lV{4==-v$L5PiF)&^OdP?*p3pL)bb)+O)J7=76K&!ATo!bqJP z&`iwTo^Li7G=BIJ6+%5AcfwO=T@BW^u9Voyz`xgTC-tp53q-EDM55zXeuOXOI`6|I zf0Y$qn>NhlKEY$dXPv^Qz)7#*8caXNA#IDS2yFd65*p~Em%`x7_WmIxY#7l1WSUGV z6Jr&%qHQ1OpGYGdu4{mznujlikx6Il0N%g&tSEXz_DgQ{y{0=3%bs)sFNMTqgvue- z_Q58UE&`Q3b$<GiVmW%eCjDT*t5!9PMdA1f_FJIIm9w)ttW>XkMIG^2@~>;MmOpqh zhP#JTTBIS3u>QT9@T80kPPNh)i;nN+fGAiScW~?@itia?GsTc3trEXfS*XT&LY@h* z!8OC|N^}~*2;J_~y98${_8c@6uDlqno^V%eZaT6kkIp6Q3gzXsdyF0QJhQ@+FaLPy z`2baBX|4NSY?80H*A85p)1deD{h2^!GV(1%x#o{ei=?1kX==*O$vO0V;B&?hQ4GX9 zL~u916u&|{y>gyE#gL3ZlE;RnWU>LWRl^3w``u$Gn3JweGw5enIy7iu*v@m({7>Ke zM~~p2evCr?va9-=!j)8@_K3T#q0mY}G61(Y@Y8D`a8}0m<zC{*FbiE3T;14Dpw2B< z|9DST7yQoEMm)YE_>tu!7a9U;cGRa3UoH1t$8YPObZfA)2mY+)1bjEzPb#&)8~Wv1 z2+VE~v3b4<OIkYn$o~-gQlY!=O$UgDId4H|ht1|{VN3^-_L~Qjcfu)bg~axUU6(ev z%$fWG-!z)?Y)Gdo0!GV!nSGKQy(`o}y5zCcWc;i3!G-``ov#SRWl&?BnS_N7C_>OI ze8=f)5qBm9yg`>=Ta?dzX{qkCe*zgtM4OyJUn!ILJP|xFfju>lB1-rulov$@DF?i$ z7$9x>k)lD@3!kK0VdrP%q$20F)gk(~zEtP`)<1Jf#SG3RhU7FwDaKOtYP?dJ6}>xX zHc~>(_rZM^Bz$X}y3M7e5Hm%)AMGUKmWr9)+LO*2fc&5cC{jmDbxPj-hK?^B9wV5X zM+r-JrN8*Y7t0g6l{OvNa6Y5p{~T6|``RpHU5niL(IkOIxJXuHB5sqqLP4j$l`S@f z$Ch{N_=IZTn9)Ri2)Il6Rp?DN_h+qY+z+>Nc8?^XYMT#42y`#><?}8p69%FyAx_a$ zYvn3y3l3=5wUnFkn*F0o9OUZsZL$T=-00k|14O02d>)Y*eMn%gKP+BwnIn5knY+i= zT3c0fu>Q<ZHHr7kFWqfy;4DHnYxiy!v|NvOGIpd&n9;C*nQ_~h?i*b_3H{bzeE#Um zK$dRv-vPzsS7KqqqF*XZILDmFeVPk0Nt4u7+<9_RI|85orvu8;EY(nDwKT;nBwBmB zMA;%BBaiPpJUU5u-O5~kg12$1cu74u;%RHEhp@G57D`vu;G-Je%TcVkZUgPHdY06R zl`>M;rWYG&^k$q|fCcQ#QkyAWdp@lHkL7Aq6oLbCZ+l9(=Juu{ZXa7%Pu&m9>CdFn z<k9$UZ}HZ=)gj^`2;jM~y3%brJrdVwTnd}lFwnH=Hhm8@DOK)neUqrM!K);zfjQKn zRsssL&UARfJSKR@a2ch~J8jY~WA<7Lgr%dlH%MU^Blsx$P{IN!&L_(<Q{ocZ`RvZ5 zeaqx&t&9G!tqI_Rxff>t>3tbjr@#2@s2xML@o(qv*C(FDmxdi61_=kT<d2wk#3TT` zFyk1{&;fXi3-;tr&G%%~fogrVJYlnfn5OLUZbiJ25!HmViXL?^2;Zivo7C6D+Ti`( zF47jSDay#%J*<>c9;FB$nC-k@<y@BK%a#Z@(grX1bG69T6@1RiEomr~&#!*L(e(zc zmLD2XNk6hEYdyQ+Q<ONHS|eYLmStalq`C$alcb*U43uY?J`n}8X23IVpI22K24PLe zLo2)0x6P@g*^3=Un!!l_^=!VDjl4nTUs{V<p0>hUZ=781y%edo9NsE=N%!j|wlhWo z#TOT_-vBVq9(}RZ*-V<;82KBmfwY&=K~*W_I$E$#7zN3VVdk6Ak?E)68hU^6xzN7t zAC}<k$huUm0~~dQ7>-tqD;OHr41+j5lhm9wZ5TYWh@6iw5pFz{AgE>(ET;*Sp(mg< zl0so*U`$x$(oeG!O@`>)<^u_$wfP0!kLRZ6K~H-5@}&a)V<1<~1T(-mBZKKMF^3i} z8<daoOC>|@7EwJi+&_T46llnE%cOZ7+@UL)LRv+Vnn>scJHVuLqTvAV5XTf9(^yJZ z|Isx4GFFef7%Fj`>5WMzTC^%`OyR48B)sdLb~*EkfC;9`*MK9Q0PlB`QLdwoAH&C2 zuQXwr>=~0gYzo1ttGWv63EP2+%^jCYzZRLi=G_9rDw#1S%+u2`@9sgJEmNd@+L~1T z?{kWrR3~Q;EF<HjB(;s0vbDYc;`2qP4ziBB)z3RQ;2L?_YguS(tKmt|L#-D{Pg6IU zoL}s0_jDPKbx7xXn);7E9ZE{fm_KJIZ#OG_&<;sWkj>oLx~K%bp#MT|wm~;yR0#6k z1?asLIJC%5L1}9s9GIKpDn6E0Kjd92+V5|-OucV75+RhayjhF(BHd^5@$iGaGnC{R zhmIm#xQsst<<!j>v^zdDb}W?y7EgG?ulcnv6HasY4VYZB2dxFKLeMOS^@b6xtqtbd zh_?QqQYTj?mw<Zip|xTy5y8p$%4<SD4}KyG1NnnM1M6@Q^Tgv}=g}J=jM*S#Cv7qx z8$PvUM&BZ?<Kh>Co0EFVnGg`vw6fez@8u}ktM1%dsPiP<Q`jZx7aGe~_ZOc%%>l^C zZT+K!D9eATnNam8x*wlo!zU!N_6zBe664}vKXUF+|A_RqfAD6ms0yLWu4Z$0dp>z+ z`|QJKE_6SDzQxP1u6`?$L{|zjzu)BSYVKF#Ld_JbC7kG$Hlj;FDNe$U>V<&sanw^& z+%MwsS9gDV)@2q<L>fG^8e-<GL{*O7a7!z-06O;|f=35Z9$+vU%!r+~Uac6DY#4Mb z6IbN_DAX1`F07AC)|*KSDi)g{cFUJV6%E^4M)*p$fykrP53-jG*^LK&4;4t2wrZV7 zG?umHN^%|3+=mabYwr2$(>2%B+#L2DcR$cc=TI8z4L!9gc{`pgmPMUuXy|h?MVBXL zGN0pWe@GIGV6S!zrdqU_b@_|Wkv8gI|M@lQokr3%{f9c7JbCrlunlB~Fj(qF7ql;D z@Kzo`2dPD33oF@NLpPC%%tBzc#*foa%ty2a%ydQpBOxptTtLRr0YqoXc8Ptp47xET z-guE}7v$foeQ{W~<tV?!D(KmFS0Zn&Ad&GtqZa<PD|TQ@;SCvt)Y^O68Dfp1h)tFM zpr}AUoue0@0(l@@URCxsFvDub6JgsZfHLQb*8rbKY@2HbxFuDo{8!+qrD68@DP`t) z`(WK`tf63w0I&2ben#2sb{JL|lp#7SG@r02y<=UjVJ{%}HqlYmcHx9_OsFLtJn;2N zLzU9v0y)`Z(~|W!%A&Vcpcshwtzn{wjGgfip?5kFlD)uidOQ45`yYS)&;6a5ClDG> zXnUb&qpfV*u899O2sjF%w2XO~=I$k#)@|M_#AhtRo4#9NBlM;{Gqn%S``M;E`$7DX ze(i_nU&dj=X74Oy$1LHG_u<o~*@6e#wW{(T(Z)Mebb5=~PPB=M00!#)dNS7Rj}Ie4 zd2K)Bm=uaX4-C58k|s0PBu^SLETzI4emIJs!IW13h)H&d3ybv3m|{0Rkm-D*6BF9< z%VYnq8C=7#9f~X8c_97cvxqmXt(9+x@DeB15Nz|^+}5=+WY>~0<YSLyyUP(l3Tf;U z$1ayi9CXSwy=}*()5|!=NAF`TdI7<Y+)+&0iIZV5Z<-vQfG;?HWEZPV$oS#bk}rH& zO$+i4oPP{b;glHui_g|SeN;c^jq`&h$(GlT9>?t#^4n|)+CkR)Q~6%3M+hG!H3eZ^ z><>cgR8aeRvoDRodim@FDkh#?`N?f2p9^h7U@|6PDQcIyQp&c!(29T%$p&L;60LiO zXb*Uiir@H4$yD&t7F(5MH%65NM)E~KABSSj%F+lCJ>G}5G2;8#=gj}y%xsI~)Wh$y z9N#K@DqdVR9ZzE-ZI0}_(OV#!xye)2`{BWabR{XRgBzczYE|fcwC|OAMY2HqqHx<t z%y!a0aJq=`E=fTX;aO6rF8%X1^e&FF@ILMSi<Byc^UsF1qt>$Iic~RW+{Ymyparj3 ztLp=pFHhDOR8}EQ@>$UwUyo_31#!L{GG5+ZaMVVzUj-cU7oR^yB9LX+94CQMO7!mA z&|Ay@jL1PH2l^P<jIWWl(?dZ7OzBC4bTct$v9K7{?>5b=9QxI|eM~IRw-RL1=kwbc zn@Yq%3fhdl4@D-`0O2Wfc3GMx^6(2nwokU)Fr>yAUL4$#uU4o)V$_qUOl$*DYhuRr zecf-;2uixH31*fZoqP~ZpVfDXMcD>99ynaydEW^|^hy-2^sQ=@&2BUNpy1}L4p7_I z;t(Okcg_*!yT=DXolRc>4&|Jmcq~BT((U6`;RU%L-NUlCfgV4F-cXz55W)dlK`uN` zi`BWE10Ph7UaQkIxaulvTaabu1?;)(g{_!;b+i+)h>(;Uitd|IG+?;41?MtGe*45Y z`m;mKhw<c#Ud&&77SwO+=a$?4;bY|b7q5u_@D19vou6?uumj4a(0vmPWN6pYRt)CR z6FjQOfuWu!qPgfm$&m&pe`S_`Qw?b`NePL6stt&~bld6#hx-7hTp7QW!sY6L0d2oH zW2R#nPTU?hGz{v7IX~_zpUO6ENcG%`7CpRVoUPk3WR(vIH3f~_*s*4e>Ki8FW|+$r zpv~ubo-a}t_&6d@&<}~+oRWqp^@ywSEB)GO^(lse0xfhyEV#N;X%DQY1wRNPN4sne zw7wVEHf86t*)Y<~XJ20K#VpM5ZdK+5@R%d$KZZG2G_H;2`lY(lQj?F9KUS5A8cQ@m zvTFwHq1c(!`e^w-h&G7V39D0{UVN&+fRa|OsirFa;xh}eg&dRgS<-}Hb2ufn!ri~D z4OpoaD;qZVWwn#wa}vsH3lYVZ66i`PLd|(<p=|paCt|blCC2?{X9N&LN+Lf=|K*{} ztOiaAv{18s4Oj_oK!bI@0dUV3H93SL1Y7SF6w$0Xu<xCfB%%r2gp7ZT0IsO-GYfPZ zum(|UBs>KQgvr`LIrBsl7AqvaC{9VI8cNZ{N%`+z$JpRBovRHqT7T^;FOyg&^svYC zX;#Mxt}#<VkxSrxO|PJd&mc{qlFbM|V3;A+P)o40CST8s)Rc-9FU+z`#yXF*><>(` zg`3znc{5FNi)JpUNT;*CDvvzGtU6%ZPgin!#PqG7Gl!^XQz1>Pf{&j-qCBOisUk&U z(2C=~`uPv+81l(;jFTYjS7lJ4YmnVbVDtQ_yB3dBv)Tdv=0}Qdx?RTL0RMAyjdA7M zX<sv{rN|YAe<!n58gN2^JbnbubH0afsF~RrP-p%OSDH+DH=+H<XfP`^H4Y%of+X~a z6~;H^>myw{k7DtWF4M-BezEB}ICubcy;08-tOzk>VHV^E%)d>|I3Nv^i4iNSdLY6l zkWMnJ&2vDdoW8F^_xb}E2y%hFR?+_{&CAR4v)?yZ)3Iw1cdSci!dmr616gg|L6$Z% zpAc_EaXZsp1JaIEi?6GItNe}Fbxs&eCfoOi3i8Ds<Kl<QX-{KCtJlSt(UYQtv!C<r zu(>9W3@;Z<zN#6>(;TFI1KZcyjYC*w#@ayPpuhat)fRzlKyU9ChXVyZa2?oSOCHTH ztU~somxk&uln9CU5xp3Ve-5RW?jbO>y4>dedn;Qh>Y$VV?fyvoU!HzoZM%Ul&@NUG zcPQF?q~0toWi-~XFw-PEaW$x3Hr{Mn;mwXPRPW;A%0*!qQ$Cq2SWk~z7kf#~95L$Q zB2q-Kqa&BJ$0T8z!fW}JddLGm4wtaSC1Q0OCp3{G7tt4QgzYRlOCDUtPH-Yt($nIP zS9q_*V0N<JZLd)Yoo|{#5=|CfY?M{&?>n&uzcTLQZ0qj75zgV9v8DQ{e}CZY4L~jK zD}Q~>waF|n=|jPFQ-X|iZs9{Zq)>vd{m(H6JHOYb{n_kF2V^I`_<!-4l!rr(Z}pk% z?W5IwmDKRJ2~PY7+qYWK#I`R&U_4QCFx>C<p+DtIF770WPAM)P5xGZD8O9uYC9>>h z`6VoLLE3+E?}3WnN(k`$cN+EL4ESQp`-=m39ZNrM86}m<oR0br$ByNpP0P9j+p=vr zulJoORH(zlG?h?3v4hv^*>VCTrb(|onC7lxOD!812H6~Tx4lnDnP$pcslJ(txj<eg z>a_5YLQdhd$kHTJf@v`vteW2KTITlEi@JuTN&0uGwTm(_O3;C2ox9k<(r$#Q<TB1~ z3svhOa9~<QTG%5r+JcVvj#2KtisU2NnT;0WAX!zCupe&ey71IAV1$d1eZABuwDK=f zZtWUuu16$ltZ_<}{r4B2Ke`dJ2SI&y?5`j*uB0PxXwGHK>Z{5=LOS6$`n4J5lxMr6 z3>*k`CVEO6ozf92NrC+sF<^J8#u#O^#sDtI$4#d&IwOvJw|_k!2rf_gmU|qNsx*_d zwXE_2kxyft%LE-%fF$J5oALb-@FP*lwaOK%6{`wYwycxNq(+SfQdH{*p$_9V*Rr8` z@yMvcq+5MgX<XBIosYlw10hHDLyZFkQ>cvrzigq^%ksl#gIc6KR@5#Ig0mE9<cHAR zv)vMbUH=e-7^RHTHOO^TSihEsyqm$?=IgSUNe)UfNg9!<VMrAaG#<!83fOwyMenb~ zw0rc5FO_9sW^%N+qP$>4T}+dDP~(AAp?t~<6q>E*sY<<$1Jyr&$P0MI-~PrL0dWe= z^yUgW&2n*a4wb{9o8Md?2u!^5&dMSl=(C=#nt1ksht;0EB_or0l5iIv)78vcD=y@G z-GO)G|B~bqtYs%LwZg9FaHmt3{l!zeAg1%q4tZAmw~v8*T*RKr)t&s*bAM{727sg2 z>K*Kb+it03tVC308U|yTukOV-3p)~MxtmZXj<n%r2XI?Ld*$nBoKuv#w5Y9E4U@(? zkOD@QCV1*Z%-)8*%#3O<VIlk2-mN|)akVAdR5Kcnl-0px$eeqCcN}g3ZR(qFp@QCP zwuK$gi4rj^Za%0G7_mt#2{U4V4+sr$gRC6l<!Y&%?X_H3sbKxax(hFeNw@Pf*Eg%H zammbP)lu||*|e>H`7^I5a=X6zXO%|tIb8g~7F9fm{*C5&@{)4Ff!Q>SJ6iyGViT{t zASC;aJqf;XD)~LLTR&a!^rSFRk`zNymIfJNAIbAOWiKo-HC250>yHZX4f5~r7UB<D zZVtV8U<4#h*mS6qwbytK=E8fg&%HuOAXb8kAK9j1?Q8M~6`YGP^OL+oP|Q~v!qi;o zSajD0L9%Nb47TJZX`}S<B}w6?5cAI`g~FTJN~Zy26cUMocMg)n%kg!I9hps%1QpZ< z=1w@LNgSd}-{V2!HP`*QOVoH10c91M^>X};Sy2IXda(;66;Kea3CfNk=&L8<e!H=3 zqvA+4c0E$G#DWyitede*2*K?;p8i~FTyK}LqEHs%=rF|n^H;xi@rJB|tw-)`e0!2v z<iH*B3YO)C;Bqhm`;mu6Klbs?R8_`%=aFtC63W@nE-OqEhs%Na*U6VqfO2XI>Ry>l zIAmkrd5a4P$-Kf(v|piTH=5-}hd);ppO4Me;0Uh%<blF-urBC4ZL9PWeJTj@8RXld zz{Ja56W)nie!`d9-sz}@@z434H|^&4lV&1Usinp%dx}@wO!O#n`N^5mTPqIPXb0`v zHzo*tSMFYzi($QRnU2DpA#ydS4}PTUk3>;$O2T_t77DjJG4IETHbuiu>@q&h<*TFt zH6YQRqPI4?EPs61R;1nXv|ebb-6c+P_yPI#r1+ltO&sfgxvzEjm?XLFTe#Ps7(_mB zZ=+^<TKcN!=d*{VfWQ2CY@MP&gIBzMhmK+?N&573<tWg&3_dITyr6dGZhO_>r=hlw z@z9%kO(_6HjEZE2;8;Z~a~zzNSYlek#N+p8S&$ih;~K`&x}Bvwj)DN6goL?5hzPGz zA=^;XU+-xG2IO_VoP(6Wv6%DONz0!&IOYTXYj3Y*w@$K~{O?L%JTiIxI2C`K0^gq} zHL^H+?8m-H*nzEP`+<ni9#J@Fj{%-R{rB*Nuod426m;TR{pD}x*j?M=PS%KD+<%gm zOHTU3C3e;0$s@A%dY5?j#;5!oJ-W|?y!~zz8@AS-edW5I@PF|x;QMAEsjY9MH09tH z%|LVG(3a=MNVZTZ1&ib;k9!qwi~i$=(K1-Kz5SACHVesF3~x!E)!1KrxU>uTo8J}8 z+`W0Yb<adH=)NlK(EWp%n}H8-fs$@aeculdCB|?}F}D%)xQt4(<j?1~M&XBrNh>*u zrvA%8UlromNoP+5?-F3VJ+h#GI)jXkN?tkSpFI6J`X_blzhQ^{_zHSdw#lg);YNy@ z=)$zP)2SUXV#wx8+aO`a`L}m}8Va#%s(-#8)EO?+dzy`WNC6gmJzcxJeuD_lx%}_* z?>ak0h<FW8yny(_dRe2it50r)Q;Oew_B_RletZ2Py>1TwIVDIY%h>gE*laG~C%~em z-N?0pwa41Y;tS$;OX9<F+}cS%CG%eDNK8lDZ|i=uZ$0mRN51+~Z>S$Sj@B9t(V)~c zO|X=tNqj9v;wY@?<jTnH*r9{^mygD#%t!Z}_+u#E?48?qK%_$kqRFaR!`{Tk8iX%f z4s?t*o~c$Lmh26t6-j#5e-w|380GB<Z_y`$O0uE-76c$`&dj_)Wq1^ZS{ZOS+63y% z?sWWzE%w5|KthU2bF~gw?@r+G8)Zl@<?e`V7dGjRFQ3#po__cl)R=zjXY}WfO2LQW z?`yK~hjyH2n=@(BQY|`5G`}lq!y9DuKY!W7NLzo|dorZr^oaA_pO??N9o^2pSF={2 z8XFFbuus1H@=fOq4N0uI#GkMKUiHI5vFK+3?Z}2zd7u3Pr&b?^2E2e*{>sVHghX&h zkdJnTta4j`BFVW~0`wf?(NMakwO=7E11l%e13uVz4rJy)(7Z3p7;q%mnlmr@%O8lc ztwM&*+#DdU7`HPXI?*z64Jt>-ZMxnq{*cz@Um;pi^6uX4PZSx``K^LETDj`lB@m;# z?_&$UFwZ_>w#S;fv@=I^VN%kW3&^y8=KTB1w<!<{e!4(%Sc1VNZJpFik$^}GE$h&3 zwbqYQJwCoHOOcE-fE3B1h^<+3G(L|c!7_bmA@{DOaf{#^5K23NK1r^Mv^VQ%c+bKm zfz-Q-nt}m8iIf(+&s}#Cn*<XxM4Z_OX|%K)l3Xyd?J0&@NgvyOrFFeD4V598xjS)F zO%UP~He8bg!Us@GXW{df7{h=9=FQI~Xp*^Ywbv-{C4ke84m}klArNgz3OphzZpm!} zON~*r^y}<?V|~J7sqOJuO%t!b_*m$fDYz4?S>A|c)x%bc?}sTPbV8X01#|3K@u}V+ z<?wsGv>sT7WeS+#Uo(1iy;373g6MEN@e|ARn2_FoN4SVE^Eh<GUz5m=VtU<IAZ_g{ zg8m)iA-vX_ruyekcD+i7>r?PCZA+f1cs*xyIXdR3J_Z?Xr!FZ@GBtvF{cTvFQqb&) zFJDu{3c5{{4yIj>N}8!zP6!a4kr^9q{Ry@k9KaZeUG)omeXzv)zzH<7kYfmc+dix3 z;??@*x;Ja0u;+tvgNk0t&SFFRa2h;^g$^y_Tr1=c9ZQOG#e5cNVjz~{|N6v&fG9U3 zF9iv1+RqmtAn8mXMECJnuxcPM<oE*=G$JAr@9DUi%PE-3W?}^Z69yF7p?~qw6feiY ziEZ5PECVuH^biJs2bto6b=gWP$Z^bKw7yjhpOGv_nWHKo(KMEqK|!b<kXoD>R2as~ zH(}-T;Ye0e_R>Vp{lY|Bb|F0FNUUZ?9a<J8Dr*Btf`#nQpFGTMZ8uCJDN8DPV`1)H z;iYZ#*%3GYOW=`qk+-ux@p{RnwQvaT&K+(VJVLzNj~WR<^tg5AoEu?v9CKN~IXMMw zk$|yx9OtEd70<|k_Vh6y!E})&QH#u7I_ZQyd%WfttzAw$X1JIVy*ruj7+xYTh!(DM zM~389ydE{Mzim6TB{-IrOLNrV&3j5i<iIVFp<ToRP$J^Szq6?xyjbeJpx=)lu&@ZY zL(&WOj@iaIYxZstN3-Ae(Ep21&O-tPSJnLWJJ}5Xy<m0ju)Wb>3QVx4VWwdvqa-IH zh^~IjaYqyXxyw`jhjT{#qwvp$A;@k`2DM5&rdZwQ<x$V?IH_UOVueoW=p~H{2X~B1 z)ZW{JM78;{ax}B@HjGvwFI_fEob|NLhpD6<M}5cCc&-EPWF(?KN<1cWwc<LqmR&vh zymqvwn)+_)=a*mAtkS$y&$;4`7fM2_J{zdwjn@M1licW#co==nB33b+mu36?==-1l z{;WOw^Xs?U&mN;XmtVQ9Ml4=|Q!4B!+Te{Y?e1S0V@2H;)APQ+w<P8}pmUs-F0q<* zMAS4<Jt91Zc67QPo4GS(Hvb=EZyD59`-S}m2<|S$-CYB;xCV;5yBD_t#oaZyySux) zYoWy*TC~t2FFa@FKj)qE<?NY!$?VBw|K{3v?p%AVbs69nQ-+qwv0v+tw=65ONYyi| z47yt3hR=({0m;9Kzw=?`zXc^o&c*}m0K{-R<U2DMD9S^O{F5_M1dw>n1TFsrPViU4 z0<)zl2)n4FnL>`qPG+92FvzDvnVgn-zNn{ZMPC&9yd<JrCJIvpK{z!o!Q>>J8=Mxc z5SNi=J(Vajwv2sVv?#5bZ5mFYgGgf#Xr-^g$Km?9aRGcr8^m%)>D-i^zTgsiuyC^4 zUVv7jzn}nTZ~fQ0v0<j#f?gq7>(pMmK1Q}6aSAUI+!*nJAWd)A20Sz@MRU@w0qB!U zDc8-1_=_OGpw%3Phl!Dwia}}F{W36rr8ihHh6EMLqYvv6_ac^+s5DPfo3M47HQ^AF zNXIUyEU(Nkmldg4Z6_jEK@Zc9s&U{re2cDISh?KVzetJ3qkQM1&ocxHks8EQ;&h6a z<1$SxQ<R1)n&_wmBh~c!*Oj@G$)J&X^YrTAVP$YCFS!*^rCIKmtqk#_VXo(Wb5&7q z+KOj@v8xYEWw;_dkDrS&U7&9Yi^W#?%Gg2ZBYayIHl5BA*JkOP7Z9nGF^X++D#8L9 zRv_L*eUj8o!sC~LWKSS)r4cBnPJAFY$zaT*Mp*eFq7m&#K+2ADYLL`B_VAPOkp4v+ zYwX|olN8~3ez+JZlY)IpPqH1jf|sbWPXveZr$CmG|1LTE5bT5j&kHWW9M%&K_Zd7% zp`_=AL@{>Fz6#yv53O9juhlwdYT?|YI|39a1qrXAd$;!oSHylr@`7zQL|qd+aG$lZ zV^x<Xa`8=f{SH?x-}$5qqJV-6exN)te4{QdmZoe#x29kHoHztrqlHF+hf1K2IP4IC zGmZBk&v@luO0mTVgi43u`kX2Jv$f=3>-w+9$Jf`Nt*_cOTX|#M^p(#yA4a|i(<iQs zS)(1O=K%--AIMbJ5GCe4ZFS-cOF5+ct?-I!s91Vq(Qhs+Ya4mP%v~72Q+8`A6Y>aG z%uQ2oZsFJr+WnFsyQkotUl?XhH!WMyg^8WQIG<b$DQzD4LI&9qn(C;XIRBp7|AqVG zI$JmtC~{}8$UII$!;emZZSbTDO({VoG<=&}8n0Z^#`4=shyod_9&t9C?oI2<QIiEN z0>Bhw`2x?N)BLQT`;iPy#q(Zn4NFFs9A4GqYpw40S2^*Mmf3~QcRsy5RlGUe^V36v z5w-d_Ow`l=n(McSpYJkB=|J3s(Rd~Re&|@<G3Z@v<jDba3`{i~;~UImc^V*rGv4e8 z(`Mi{{3%?l4Ed4eh51{2^hXmW6Zg=V6ye3&%P!j7>Ro5TVnNwkLh^iny9`q;LtCU; zonWtT(ZRZSiOExQ{!T_pwA1VPMkO;t$l=-h6HzF}IJO7^MgqTMa79JO;7Z{UAaRAL zN#d^VR_YZkYYnok5!{u;b1XL(?>mW?-^-7dUG*r<DFQ=R#XUl`mC#Y(&k6B~WnKbe zDUt_@p;hEyOL!WQjKMEzW#e}lX<z0hrNuENHqsQS5;i6=H}V`aur6jlevt4~^!(Ay z6a9+B+p4eY<}3WCt^S?QKS?2wm;a%`X8rL$?<7Z-T>afdq5DnYg~v|ZyAIxIh)&%9 z*9rrG!m%Dz`Z9hTTK!fFhlaYJGggeiBZm&ZiQnYCkkn<|S9G%zXE+h|H2xcAqdaLg zpQ$}YTVmR9I_m>3o4GbgYQ;!dQt?cQNs%&<+p2xE_busZUw~&zgMQ6&3Xu4~Ad&>t z7Xg<L3IW7R&KL*c2$B*50dUcOxYdY#C^$yKn4(nYFR?+&*HkTXCYL`f{yQv7JZ!EW zX^O?>%V;`N1w(M>r5{qfth$7lnY&QY&6;>fa6U^lSDfk#Yx&9$(0~=<Y`nuP%gP?D zlCO0)Tq~p9H6$|s{ap?}-i<XAWd5&z`RHR8$Tx2hc`7dunHa{jt$R#ERN6R}XJ~>p z?0+R<A}l&_Fn~~Z<S<k)IR<Bu3`!IcR2(!608AosfUPLuP6d=V*$P<;D1=Cy&zMuh zE{eFF6tizx>wBTZPRXGrh|6bSB#Pus5JYe`V@6MPVjVd(N^1@b1u0)Wc|;G2Q;E!> zh|e(ibCAnb1`sH$4`#5{R|S-CB_|c9hAoh|_u|xh+5Y-ED?7YoMzL%t$;EZUB`9o{ zlbz{$u!Dsg5j95V2GfkiXw#XY->f*_akGo?f;yVAzf*-CV*5Etyevy5`f*VPi=@p# z$9VqYN_dzltZy0>*10h#PJ3Bay+*a*B-3u{XzF%fm5waghIiGr7xmY>e*TChbNU(` zYHe|S*0ebgFlQrEAFI_}kJ7jAZejeNa{m9el>(580LU&tSojQ#Aimbm^_a#MsB)0W zq<Aare6$~#DO!>8#q;e7Xu3t6Q_iiqwp(ciaR+67ng*is=w-XlqD^8@6sD|1`--6$ zK~eLFXjGhP&LJ(wJd!5XF$`4Y)_4t14t<NrfY;bihu;h$0zTCuc@JHPmX16X-zqWE zGYZGiD$Jk0y}phPUmoV{90aR~%W3$w=o)^tj^+|lvDlGV4h}hYFNC|j(Gk7MNuDK- z42Psvnw4P5=D6jB*VoM+yx!i9Wx$(A*-}Nk^O@l{V~nL<7@bDJN8s-l72|}8XA~jT zhIM^l%IWYUh)4(179ghIL%YJsmY`dB^Ap4RAOZ*;7{U0OvDxW1mm%$HJ`M^7I&0Yv z(GI77{qpdPO3s(dN@cidY*^{Jw+E-m!Rnz@2tEba->80yXYaXF`RqrsTfjS81{+b* z`xBW5Xon1#ofbd?+sXs$$^&nOf0JurD6yQzro$B0M_BiH#YZGk`E?i<UndButGnoM zzseo76xB{ueyBlaDbcF~qRMkwwEi;O_-Q!Ub6KF+ge5<o=jp7Z=;Nc;nxu|LG7x@D z6o3;h9G0lOEeG6}i;AG5_a@833fogD<c=u{2M&|han~Wm(2d%_Kgf34Sg*cbuQ3Q` zet75eDg}Pae?QcC5}-2QUY0KvnBgtAGdSDWNUFDwd$o6Zp0-;4pN#%5FWw(+b4>(5 zftc}#XPk_{WMWSq@fH;`N+Dun8W|c0H%s-LRPBtG8|v&@H4pmCT~s4byclYR&?6JS zOaE~k8kQvJp*n0F2APOD7@i0jGaR*S5>w14FxFbA{#pTh<QRX3goVyjO)-9Md{7gC zScD_bLL_n+D<6)n;9lo*MnS=8q=Z%ueH!@S0$Yc<=+~*ik;VyTVX|mMLHn-l$IHx) z#R4TZLRu3&Dw*;M3)*s!`69MzagdEo*|=p1eS+}}jZB2MFAUXf3}oy&{a0_b**G2> zbyA*V)ri!YEX>h=^Xqnn)u5I5^x~n&?aovbx2!T6<Xv$byKYYd+Sb)M$q=c&A^5Qu zDq}OxF|%8$H-7K{@p1}L;c%F9nhD?F<+X6cb0$%51K1@21LQOasCzK%l!Q<P->Zg_ zIQgj=iRq!zQ25Cq(dUkGrb>(eupb!(Ffg!RIUx*q;c3MvgG3F4gp-?-2SmZrD)48; zYZ(I)62+uQ(TNa2R^kM8n21Ju#_`~7V{M4^;oF!sXkBFrdMyGBWp?^ym{6GEEz=Sx zM1LqaXsGaksCvZ8rcL-kohX>tLnx#?0j<QoxvB$WN;F)c)n!z%(lKt1DHLb`9u^u} z-q$_tgm97;0f-K@kFgBT*o%axv*P*+%B&`i=WMNCkD2FJ`A<LA)F9xyKlZPxe3blc z5Pkev0<iN8Fa0mX>8SWiw23^C!X1*|&s7y^Y8%1Swcbyt_yHm#{)ogazhw1shiUx^ zb#uSHJ$ig(3L~)$=}vC?6OhVBhn1*tGm+JS;H@l6xhzN-wwBDOjnY;B73-hM1JM)c z-;^)x*`K#OgNx@0nm<<gvjfc;UmGf$8TM2K)<+rKn5U3oEjh9rredeZ<zt_>Y|nG( z_ge0a+yDRu0wi%L4F5VaDwIH|Iz%HSdO#jtByUE-9-OmP{{RIbyYWT>IWQC^-4UyT z%tU!CGAQJGFh88GC>05pau5-*7zD}~J3kv0NZaM>PKH^&3dz9S2}frgO@PSg-f`)p z35K5$gs*=ic7Eq$uT2cHFw9&7oN*HjAu2{<*(IHj(cu)YIR^WSib{z{OyFI|`|Y+6 zx06IXc<ou*cNM5=v#&$KF|aWFaW*kvwOi4LHr<?XN|i*~knJk7%@q?ryS7x2to8>4 zqhe9%rjS%$aAwy?>A3<vD*cNfNL9;No^+m6nrgpVLAQK;@o>xyQ5{Zsi{tvkmhHZK zJ>Kcmc~A1_YyEYaFgvziP04IAfgrl)_D3m_!5IfCFCK7Cmz(rOF8`q@&@emZy9Z6? z!MLemVb}*xiXn^3AZD-h!K;Z!V-7z*2a@9GUXCN!^^(kbmEn$6+Ur`*b<$B@w%`s? zQRllUw$%&WQ-9>XWFJBo5F(Mzc@QqGO#mSnKx|%Q+?Mj+{q;e@hco^2`W#dS#~9pj zMM%l*AOK&6joljd8IIKUASfUSidyKHgO<3L&WMh+TN?J?BtEHj2Qoxel8&|asw;=E zYcEQ8w4pvJf64ohz5lTHGkw7Eo+3)hT~zIuJd}J3?LvrX-ro$dIfNn-LX`o<26qZV zbH%{pP;0XB3Eh^vY`;B&(VbADa@pyl&96O2VXvQF@80%)y`#U67S)z!e=NtG+7$Eh zvRG<paxs=BjU4T*GPra<u8itkYJExL98I~a{&}?Y?0ex0z8QJ>#;19qZ0w$Rh_Uz` z+cOVjt=rC)l_FWS6n5dBs@YkO2z%o4=pqoVUpX&ysYTd_ATk}@TQp07Gl?LqxDT-K z-+u|2OagfIh0ea~hgblIF-2@;;&qN=dwPsm3st!-G7_#D5Mb^vyR%#B+B3ZRR#<Dd zD+QTo3loGMeC(;u*-V<Tq4D64x}y57omb?lT`X&-fTjP(O+nY&!}?EdnxKO7P1DNH z6<#!8nEV77Nx$_uH4Rt(Xin^zeF<1#T9G#SyjheJKly8F#;^Qenj?YQ1q1!-^6h-{ zl8UIYvMQ8XzE{987EZEQUf_tbNiv&~toXF}uWGZ2G1CG;f^rp7<y#^%c4t~<Vgfjd zpb2BVG7l!@TYVA?U32w7GXM0)5)qrbV_3KuMH;JcA4?^!$P0*PiG-GCzkeZ1P&6l; zSD$|)1<p!^8UZe`B?o4lR4836u|66GI$S_BB@!*ZxOqDO`JIoPEHTJ6?@*hFds7LE zAUfX#-}}~FJoS0?Et_{Qgz5n?&8CvU&2=T?+=2kf->@ROyjDQvtZv{>&w1+bD!7ng zi2$z6sfr4<nDZb{NK0=J>>rfM5bfzy)!r45qFr6HddT?7LF*QU<WazLx?}iNc#%su zrD|&G;;yaSqr_fk!+piVwL`lISTut!WRa}x9Le#vlu9P|B_iFV)TlH~3>_d9tDi;Z zDr$2tCRejxZFz?!dsS7NgjW4Mn7pmH2GSKDAq_PY($Pptnp<^mOrPGa+vmuw^m}}a z-B2~i*qaz4`_os4%H@1Jp2vX!AW-JtVDIZw58^`?j}%K$>myO*eHz7t;UtCb^QVe_ z(15@)JD9@Lb5*?a;Rk)4a78+VXro32fn>sI+EooKa@}W~3ffh|hT;q232Q<qHvSeH zPW@Rga`&r!n)=D+StzFCBMrb968nTEt+vo{92b*Rq;xTZhC)(5f{RojHz0TSRlxA# zJ)`9*LxFn#7xztT)-hXp<D;G4;{?xvZqwi>Qgmv>bk}2LRP0~a$}D#pXXoJf?MInS zKejkoyc##Ov~qJ~yw3)UpU>>LTQw}%3?@2t%^MaqCJ~}M^*ek7HZbIoqX)(Esc2-P zCx9Me1&gJ^<AGO3h(@ovqhJYyV^n$>r$w2FZzRV&<-9o9M0Y3v;;aC`?Lt~e$HYr^ zPZ&A>*?|qfsfYjxX8$Y+MRqa~KM>PdEL?WmRbmDqQl?J(osTg;n9~vZP(`2EBqW&L zRBN)Qb5Je2fX-0VsLh$0G6feafqGu_w8Z7($k(T%-tqaQ?I@#2j)}=;lLoK9#}=84 zxcceDT8)-2ZaiJu(cK1;4F+QRDd$SQ3W0BB;mA+jk2lX9e=?gJQ)E}$d1p8kmCR14 z_9)dB8(3sQ#z{&x82DNx7A)c`5C?Dp5Mo+~?s+aVM^+<8*)?luaw<4>qct2d=SAi= z^6e>5L}4CY!Mq_B1}6(oC@?;M#Q~B2xBFy}EbFtD3x=7~hHL7wY)7zEw6(ao_n=2p z4hr}Zk3wqnYxP3MzlYh`x5}U(?Y!<~tG_KB-l&U}9Mwpuxw_h0Y`QkT+|qNUg77i( zKRQ1o`iKR|_XoW5Nd$F)jPmt7q3Nb=;aH4Ci}VH(GW}pKE`OPPu9W=2^rcdFL1-lm z3mNA_x5l1EjLmoMma`YWOz+d4&P?~=vmFDLyi?qR$Ys(wnvpztThS;(#0$5WlNQ>7 z?!guMjp#uRb}G|7S~~U%twPSyZVn#iJ+`D*&KEx}B9VM9VUs<Q_zm7@F;(;cKK4ok zlQJX%HYn==3<L--K-52+DIF<29x8o;0~<3MCAtzqu`No`P#8-W1qhkOWTq#8SaIeZ zdWQp}J`pTttBY>uF@2WCCDAHB0sj*^$k8OX5+mOuKuBr$)9*Q0&wRKwxsaYDL2PbY zHwJ@2iGfaP_gm~l4Qwu5Ay$ksvD9OjfIwiBg_2N)?wW2N{?4bvFc?Gtm9qvQT#+~@ za{WS&+g+SGGwZHy98m9M)QrxyQAs!8RuJ5yjgfA^)|f9ZK<{6aygvMuvAz?s)f~ma zmX$3zKP4mV8wwSS*l*>agwE=->hko!r_;=%(|O0okQcVwS*xP{_0pVl*3Iif=;8s? z_D;PLMu3wMh}Izf6mD@Vjg@_x#wpxA_UJ1|G7qexF^9>yzNMTIqYa<UJyyA}-N_-t z&kY?CADCzl)CKePf6SNg34`v$o>{E$2P^3+q2Rvn5AuH)?TYT2+rV@*Dn)Zf><|@% zbr2$r^>s~rySkx@ZByfc=ocUkfn=ioARK}dp_0m@CW}ey7YHUCzg4*@<!&`4r0E$f z$XQsVw2+2C4~_B8=d*GVsKO932d#a-=T72hVR2B+3G;0RDo9TYni_}QDkykNH*ypb zDXTj?_Dj-3CQeL@I!G*D4nvRJr(#&hhEJHSQPyT!d*Vu#g56zbeJ}tO;5WH?Uv8p` zX;XKQRW)kGz_L`UBTDRpCzor17S$vJMNe*C>-_Wb2Va%yk!cF^O-A{LtqrSX4zHEJ z1N0j|wUGi=yH2_eZuCwK3BtZio>)6b9oihB`KdGOm8M8hX;|pDF37W{O;Bs*mko87 z&V9n0jE_r@Tjb<EbN|`FKf*n8h?_m}w<zthhPYjxhr_)^e^kpLVsjwPovXN3G}&fL zU%iqoSy9G$bNH?=!j20_q*)nZibM>;OoV!ZL!pNf#WRMO|L4zt;2C0#5Z&;E5@$`B z(i(<PQ>%la$Q|O;^OKURu9*K)G2?3lF`1crct>z3?C#dNR1j#UbCFu}{?nSu9A@K| zs`#PRllw*2_uJ#E*Nj2`*IN6H<{Gyfo$q^3t9Uile{`OGbhzzTYF!MYU&{@3YOO7& zW>e~1N~<p(%r%9TDoq9Wgqvc1vmi!{->%&ZKK@I@VYcAeRzN%_9vHp*O2q5deQ;?D z&t~!io2UFsZ`Z0@l#Ft&uL0M^tL1E=SO}3;2NH^$eKHYw6EPJK8D*G~8yckz4jU~! zSc-tb!G`*!4R(_Ps-eh^6o?EB;SVMfQpF5fG?;9fq%(SW`+vqqb3Oi80j_eN23PDj zXtuxfcohHYxX|K7de@I8$Q9&@u&W)vLMG$qaQ}NKn<0w`GY>gj87Gj(i~^w*%Fw}0 zsPok|!WA>y^6|Ad_qF%v^(c~0wRQ4a%-5|c6&EH8=;C-Jz%3BK&~KE%Pe>3>(|_9h zHrWB&(-iEw8$(i6f{ic*K_>Tw;+nKuz_br4!mF+XRr~5y{iG`DF13!6jsI!b*t2xz zqG73@lb-w7Y_DP9VHbg5oSaPHY7D4bPgEgD%#Z>gqo}hF4+e|DF~cn=qi7M<^Gag6 zke0^xiAG82b8N>u-caDt8JOS1G`9IEu98*AV5xrk(2Ta|Q^LZb!@$Spci_x-IU!d; z8wg?HERNhfxdeatc_JC)F*nH_^1&W05)C7ICNK<(|GcSw@@K(2AAb-t$cOPeCWI^X z#p6F0&fBuY!s2TJi}Bf){ig%ax8{TLWjY;RkSx;_yoxBKdtg2edwyW}BW`5H`pG|e zxx~{+?(hvBr3-`H<I-5fn1Z$M59_h1<7Wq)M}En9Zm?6kUa}BPMf{huv*5Flh61ZT z7$<f;5p)i_cx_&j`iR6SVE-T-03krm3uPrRvu1Z~lEQ&m;rEx~X&U$LGfA*9x~%zI z%-s<2{fBKc9O~m>rdi<0<#urHM3`lew@42~J~yz;s-DrHiL-88@a&;;N_K~1C;=%2 zmu#|ZO6rWgCO1CmuwX^!#(X@4Mgr-#S2I2rO6}|GQRXV7T0V7Cyk!-QL73to0w=B# zbt=e)12Zb@_ws-9pDP4WIDG(nt2j-8)>n5Wa9>E{wEjs1;bz$^7<(!GJ!HUJX+h2M z#uZungFW=!J`6H=UTCG}s}rZ=y27(EG{ZCL_R5p*eZ9(h`+xf&M&hTMwzSFw1mqY- z0Oea45ha1tfbF~vO+e&XUtgcux_H|*qIKDaBNe6BEsqNEM$WTZ)C_HPTQ1ewD!@vC zJgI#<&zSG<gfO%OURPNeAk0|u%3aR`TcCr9x7RyY)*Z*%_RNd^r6@%iBe<MNmE7k} z(1J%|BC#xq+DAogM*hv7)_%%_>^8x|!lmw7VAGPtF=s?#R!ozHolv%~P;Mq#t2*iK zw^owomQ&gaso1Afe<=T3kk+|1e_dotfwT9npFy4?R!8n#`IOB96FUTciGQf*VWVh< zcM?+UIg<Y~ekejkF^G-)nawG3Ou`Q>!)HDsgagDSMWOQA26yuM>&W}a$H>vBp`$6` zW&G!iu;Tt|@~#xP;nyeh$1|Pv=uzf<vZ}JC%qL24=7zh*LZRCJk|bK-7vzd9mO};L zfI>J>%xC?{*<anT|AOva6#f!_fln|<6=hME7yF8?O~ECZvj4|R?^B3b(!*6wUkv(J zQ_a!8Q+&S;yTD8ovP@B}b>xGQUPZ;G@=W;g7|RtcXM-N{3Ry?rusvFu&!`c}2WOlA zSgo2W7j6EtHwz&&`1!44(SZ2Ay52_P_l3R+T>XpV+R{4gFk_n_cHQD^8Y=GBw0A!D zVm=@v7JW}Zo$i6(dfQZH5zDt5I4$SVZ=`=h0U7`R6*ShMd@VMXY<{N9p!4m_fy8Xl z_UI1zw#5DfCC(4$<SFtY2`mCe90Dc$fKXK|QK&gw7DOqk>tY86Bn%)cA^SkMe-NS+ zX9yKOG>ig`+&)iB>?R&m29-aCtvyt<KOm6EWHTiIOLV}01|=U(tN0qhhL}@y<v{4; zgeo=gQB>8F6`>>FC^}FY6~ay_xuwR$n9eEEN8=9*9x|P<lZYR#fw$*B5Yd{U4U!@M z0pmoj%iV0aW}ZC94BD~1xg_Cxk)j4KGODyP9KAlOUa<JNgzo%N$NhOI^XI@;+Z^;m zkY#{<yYuz+$u(W)YuhXpnad{pPO{KDpI75sRyFJFIjDrDDj)R?NA6aIu#S5o-+L~y zZyfR~)a3BZuvl1G;U4aD<7IWwx74~L+pJCB<J&O?#ey4XObMXe813tMd*N%^)%cfI zIBb%ds;CrHWq~?12ZHIbrCdG#IFHIji}V#J=OL=$pT$!C_J7&4R3=2@kZOh>ExMF| z*I-XL19u~Vj+@6~K~1T^WNIjKQy3^g{w8rcI<D{J2qG8{?A-K(vY2U%Nvy6v&v-AC z4blwjb!-kcnwC<(+6ua}=hmv2bf2{k6~`h&vvr1{^y;UsrF~D;s23%x<=HUcoxKj~ zE{HcRJ1w44&{D!%#B|EY=jNP7q(C<NF07EDj$F$T^ZjwTJjKr@)lkso)0=<rcD(V4 z6ok2U?f>2f{ofnUmd<y4G~COy=Y|8d_piS`9^Ko|8MJP;RxY2aU8)NTd)FSh2uNwv zcU$;nJb!NGo5Z=V{L@_b;-@?K@9%-W%g3FkQK7~s_FsE3x{G{tjq#efz4D(F<P9=E zWFs<T0pr*Mtp@V?-MRkqv6JWFG0_o;VnA`3jY_4ABl{j79vO#w^JyrTOBK6W@E8(C zHKnUwwv>O3X={#HBephw_Ajwj6lvbwj9v+I3agy7;hX#0u62@u29>;vl`&S*k4lTk zr4&H0+Ck66u0*pgeYleSFIuBCxkyqSix88#YENOf>%R+qef`&}su|4zOQ^vfoLVia zKpn_Z8Zn`2n5TiUvV~h$HJL59_|B(ZAQ;+C<OkL!_2BVCJwlEowc<=w%S^s=%C4F_ z4SfklSA<qEY>^@PLYdex%+R*k@AO7&ti)W}lo8qS|BXEi9F^=JqNTtxF0_D|a2h>K zJCwsRlAu475PGdxF=q3;Sh)YHERXfiQ-7v8>eG+wt%ymL4($pg-J}+?>-)NxfB>dY zV6-J*_?V8?>{iWjlk=~L8VhbVTQU?4#2-UShM19?Gt0=RMI-k7S)VqqPSh<$kjbpW z>!+s)jUpzwME|iXIto_${p+i*B0Xza_#isSSYr>joL4nc7MB9u&BDkr8^>S?l{xVu ztZIgUfR~I|d)te$GO5k!LO`Zm#@?DXdnR$7Xy*u<okqJE%<Z6R@n62f3dr>DTXBo( z&{sNYN(nN%?RuuwecmRvU*7<aHgNFN)DQr`(t;wrZzP9+z~8}c%J|zz$_N@t1biA~ z4(JcKdP}CQFb?1UHE8dBjtq6(6sl~aI{}i(D&9EH5YDRaGRNR@>(OQ(C@}-$=@B@6 z`FP`a)3!j^7{Yxmgc5QiQDiP7F-a*Uk3&z;H_1j(N=ytCv4$C}jm1<Pw@$^NAWw!e ziU$d>Xn{s)<_}tGNpjo}Z?_A><ivAw{~sxA;AOIQj_57h)gHuQ0c}0`0)M}lsiksB z46q^|$TI7fF3-43B2NEJ+)y-1_+Tk3vs>VLT+cx7SC*cxI{H_#nvZJr&Pq>XB2@|z z^SnB;g;o~5X_7MbKmX@dx(Q1!?=a^lj@5--y7%KfPFe9Hv-=%%dJEUaD&AwGDK|e# zw8JrNJ`he>Yl*QC2#a#<!66oYGJQjgh$4o-UKR~hB|+?({1WxTLt&>fGEQj@5o*G5 z`o7Scw49T?3<*7AnUO5lV>r(0fUj5_0DwKb%sA*;)UHRUYX;BHT5E4wZiF1<z82Vs zMIp@d5Yri{(N%R=hQv2l5u{E-B@n@2ikUZKo0pe7iE}r;(joT1EcA)_>whY=Brz!d zclh6~&BDTi+<%^CqNIGPRy#dg>#&p1j*lr$TFmoU(QDrjIU@2ni<yiCJLqbfywBNy z-j1FipDgY?Vo)2?iR6}*wZT)5Py1`WNSmAiHQUDzh*Ead%<uQtjU*1G9^jB*lZ34H zV<^otX{yBGSSU1%csb&C+V6XrvpwBTOSW;VHKr8-(J1_Q$V7;jl=OkH{jwNsdY-Pv zbx>iUFcJ;>G&&tBEq{xe|AkThg7u^#_*$=9cm9LBbx$xwxlhSVPS^mY$lsKU9l;fo z+Vg~j#^J&~bu4=VZ+<XUZ~?CvmCKWXVolUGwBR>&19!xhAqJX1PU#ivVms=ptFa45 zxYM06O8vjEvJ3Y1uL#8yCf})Vnu+%6#J{`;0jE#69;rmAb{Oi(?o%_&W|<g<w>eoT zXGQ_a!VvXM>y@5ea!;PMGZL*Rn?xBYlpN6mB`C)q;Yxx1OVe`0(St`Lhz0@k6#eHN zsZ?(Jrqq->O09#ycRp8=W+1J1K3pGofY3nLJ4#vZNKiR#7NYuR$t%~9wqXh$tid!* z<1MOeZEvn6BTQ$d^b|ECV+0Vn2_puwhLOq#PIj&0Qaae9ZfBXF_6Bv&!q`ikV{hX( zICNcq-Ie31FsN0$wMN_b*G-298jHq8*pYfhBjXjte3mYBX}t7l5i4+%pX!41zdkgg zhK7eysaFu`rw)w`=>zY6G+b@Lnu6T?qBowS>ZEZ#sliK~few(FAA!XCs1Cku0avxr z6bP6|n0w<gPSP8f9Um#hx}+<`DYg6Zy#y=W5{R>gr%6Dc63&}6Qm6{F!hO_8)nk&e zS6KAcpXuF4^-9Wvhg+9eyl!ygx?R++r0Re2GO-9;3&~5@z4N&gWv0|4-z6M6ZqpIv zmub1xj-w6L3CJc>T1(9d=1>n}VCN^s!Qt8E9)Jc+kRRqTg4o#~7?H6g37AP^8SF+r z=+Ld8#P`N8vIUtdGmLeWBHWtXt33MQTA%E>+&0Dodx*+rU;#)(H5$Q4i@6pHg<Tgz zJ0<$QX=S#B;4r1_<7k9bE8!1fDZl^X^Ppm*h@gsCi-<JE3=11@-<a9E^?Pr6Tz--@ zCDWRMv%te)p&gsJ{$*TwKiT#C?QH>iVUf)fHFclaxC+(&eWUcp?4RO+R53P!^*;;< zJEL0a%=2hMsZs+>5Vz&=5YP4;h?zKD!cm(QQKI1#YcHs#{;z7|nbAv4X(%#j%A`KO z|Eq!H4|=5Fk#weaK2Op>PA%xY^>P0Olide0^dHP<z_B|E_7i=G5z7y*KWfB8e$1+P z%+NJJHr9m0D1s<VJki4G4|(hXK10x8BO`zmbEu*AA?JngZxrr>;S8U12QCM)@Wth@ z@}tTL9^{g5JF73M$q2tY&dv3y&RE65NrvpL^Q2<Xc;WE?rYxsX43+MhoW;90Y|W9s z>1~JlmdX)L*nzdkaAlEkc}<dXc^X>H<_g|i)J|3ftqd1CIU41pWka8Qt5l~%?o3?^ z2IEz~z4*s#NNemycxRV1^r(`|l~~z{QKh2Eo+*ZZuF};IC`=A98@0?Is9!l*U3+NK z0!w55<f0V;ktHp~%~Me?-E~0j3zxMqWknJ0xiXgl+YL^}5C5${5?vrYz*_qDlb*9W z+_0<ZxKD$QimajjNV~7SRiW^RhPRilqSJj%uuLC3a*p$a6kyEspd?DeX;2Jy5QTti zyPiNhEC?Qn(~Z*{qorYIp}&?Z0j5_4T+o@S)T?CSa<!3&sAsdcc`F_R)zLlX$aWPP z)aZI8iX5J7&rrkYL8ycMEUe6uNj;D}$g(%c7)pT}a$?1VwAUC`CPRQhkxC)H78a-c z5LTNd&6&;@s$Ki-q>@kalloys-hpuxx8TQ`#y257{d$eE(CU~bzs#{)5q4h3J@<@W zAlr&S8#ntAENWHpc;$>;Vv=7Dd8vQIf|@VKK$7E+s}B-ePZZ3l2#z^cnErCF_Z+{i zi;P74HUh-=v&?_~{EZ|KqzC<dy=pQok_!go{NNX7{Vn`(5LYnYEXoS819+zO7rnvm zYap9HGeH?tFfM|&NRpHZmd8Yim7@!=1P{gkx?kA=!enlrN9*6Ylae<}EbZ*T#Tr1V z-Ot&6p`s{nJg$2Aw+{omk<21)68keRmbP?WHVlBR-;yF=E<}a8o}$IV7Nvpq<U^pD zav5HtI7WiFy+)&qehsB#8sS->fi+3k?mbcIg`7OHY2<Kcxt0HvnLjyy(kh#Try#n> z5?UID$z%Eurvtc8P8jSn<7+F>T+_c?Y)SSpaE^%qO|rg9XVQT`tFGQ6!jc|#xL9np z{K?De$$*KKEx$?@>i7%`dwy2M`Yf%X9tmrAuQ3@xHBp{v>s>$B5<?&@;17ZyLLEY; z(NR3SR9e5gIzD(WF5lpOe*5KgTO3?Urrvx!GC`s|hM4gC``;|ZNU#IOh+}PwN{Vod z0uZr?F-qbZH7xASt;RJgwJpknJJVESgkGjTEqwt?-WApg$XrP6vrP*&Ef&UrQ1!Nf zWg@|pa{?JyLnv)*Yf%A<@NT<_bMwP>87=#BI??mor8C$J0fdfydT#zwm9Cdln)JyQ z2RxP68^1o*ooJC@A%h8lqef#MEaU|$+fOJ+@D^Vdl8b*mE^_(Jj6Y_d7n3OES6qs; zRXF<XBzUP<cGb8%Oc0AVx^t-1+s+T%>FC;)-_BR7!nWpl_1fE)+eII055CodgbbG1 z!aJ~DxSi+oSAeem<0B3PfyI6hZ2B_rU)<5}Xs9asc_jEbix2D`zV#OCrP%&4m&35W zEa7hZLLJ)G;5=%PFNV<pf&B@_EeI@6);a2NhBB^1imbR3fp7~Q_zBEOHO3jHNf<(s zBPn0n*$2|;k!>wdos3iO5XYVtqFe*Vs?0^^lVLL^O>3C6BS-YpQMsD=Uq7w1U(qpU zDOnWe@hlf7j~K35Wc+09t(yu7Uoh8b8=Gc|@FSDh*Hg|{Bao^Ogq?GM7#$#jVak8_ zF;Sfjuk>74X-pBV@@sNQwU`?lYucY}%!gXg*ffhd$H@w~mIx%Xa<CA2e%3bj(c$?N zW$SCvtdUj9#a+dv_!yCaYShp+e}ARUQKXlwQM<)=W@CWV^v>r|+784H_(8gPD3C0R z*&(LRUd^2v6E+uXf}$uN#VqfDI3X%K?+i*WHm;&^!-RngkcyNg!N{?Z;E3<HFxw|o zLNRkRR>NXOKtNJtY?3`no?3BO@RE-lGX=}no)6gw=yV}7gqqe)g`$Kg_OMSX*-cj( zhyGBRq-l&kZ$BptO)b)fQe|j_4X|IBQ7_aeO;NZi_RFzXp!>Y&xXv1MqFC(`cnabk z507_Q9Kn`UE4ET=?zJ?hI~{yDk|+chK4~JwGvM9d{v|L>!SSCq+szjR*KFn#<aPJI z94>6vYL3KFnTqx$s`m|!04Y9`B0q#@F(qEEGHTfUM4y9H7JWTL$D=+<jQAKP?fG(v z#u7QE`caimY4yM7%a?K}AOyf6Y1Yr)(r<8@cyKJ0=T%FU>^a2^?QdhoQ<`5VO4#es zs9kH_lPa2b>!TFu)M!#=YA{<-6ZG1>JJnaQ-}<VHIQ||lo-J}g&v=V)O$Dz7@9_r1 ziyGxoc;~a-l`dt>w=$VxFWW}bFAQ(-fxKDrqzkdZ!UE#Kdg8aylwtAeSQzn%N;@Fc zYRveR65FBRlr**U8AF;Usqu->#PM5&UgYWiHs@ROtdW_0M}p5S0-Fd?N;kI4zg z0Qo73B3fb<ZIX!&GG<hu@?**+1+T0V*Q6}<;(|<~P?a@GqWAb!{I0)(&#@d0Sx!Ic zx=xeN^eO{Wm5QgLIp*Wes3h@llVZ6l6PXQ)R_xhCl&LMH@w<N3M1ny;z}{NcS05`U z+;EMUXzMbq&#u<8mQw>~wbtjX=0*^V^&txSv4<Wv=zTlxduCA#qhx5Ky8h^Ip@Mu_ z=3^f4A5xjgm4K%5>Rtv@HYT62V|(WjiM`pMrn|XZc?!Yz$VKipe8gs~nWL5n-~t6k zXaK$)rHecadt>qS@b=|2eJBXNy&^&di9lH<I0kk!9%TJyWw5~OV-Qt}v4|_k^5aTv zH29Y7R?Bm~@G2<{^szWMHS*jJX?OB?t1t4(g)C@r`G*)ta*beddxa7QR*^_?Qgc3l z0REN1CpL=UkG=6qb-cj|W<YatjA%$c1)1p3FdqWzlD86c<q$b6Mskr9mJX3@N(?Xx zZZ2xAPfhGJy6~OPi9XI-e)plHk9<$({6M-(<yTi#J}mI<6D`pIVP<G{O0eJB8KYO0 z`wn9`AFa)HjT9=59gFpo*Vg?sNuYi+uTH&JTFdwCHGH-1Jn5|o51qFmt=Kj4jLuA4 zu!oyQV@Y9vt~tR;3X?=@?vLI1WvZ6-3=zZT4TsC69}`-u-Sy+Tl+O261c+lA@BF`M ze)aD^JmqYvql=y#<Oq;1f<YC%5EJgqu9uI!niwI=j`gi#aBR%++GNv=-y3e4fshGX zThMDGV_q&Kd76XFO$LUfuNE&7be2-gJr^wq4c5mSC?~O3K|&6ZwVk$lw3<m0YV}O& zmV|}CDTj`bp;ILqW<7koC=#z6A>R8IiJLo^WDnfAhlHKScRpeK=bS+D#YGa6c(b|? zf}Ntj<j)ZUFb50Pay~_8(b&E^kcLt&gu;+?sqqbaqK6WC{o}M>lDO-1(P|nQBMBtT z4|!DgnsPM@9SxuGFUw&`_hY#eNn)GwHFhX@g~y+#Owr|7)O&(||1K7-mUO_x@pUC+ z76;pbL%K`C&)el0two`6#1*#-u%NF5ig`VEl7f19sQJ~~9<m8{o()(NBbLE59~J3K zl{V?>+5i5LxGSRxOU{rLI#P<Nq5?`QpAbe$gN;~W3y;I8$S_l>@X%-R^utq|Rj`5k zJh1%Z<MBh-;`3{d&Xfg1Jk*qhu;Rbe`0+>zuAmy4xS2;<tLl9d@pPyu7pbrc4uTKi zC0k@}+=U14*Z=FkXb~&|0RZz5t(c+6bYafQS@jU^rUGF%BD1$W_UT9uc8QF4_ufs& zku5&q@<CJ@ek_MmXDE+!L~5%Z_`CBajrspf>i<`DB5*z(z-u-~Bt?QnJFpO%HPP<f zGQ2h+isC83#9}P>(gnJ2LdQ~4Lo5{KmX<S9;eb0b3mr$o$P?f)>WBIYkI9}GQi`4# zL5<GBg+$0hrhzw|R(CoY$*ULUzBafk4{UhWPW@b^u8BplHtZQ+0!vpz$^S_&oid|5 zgS^CYW_F?Y(*)(}^WA0+-AaAw^@-XF_T!etWo<JWIk;{2()%VXOE60P&-#%IZ|ms= zRY=EWr(xyur1iwI#3D{1HtU8qlc~(RenxnGI5E^L2%zLxe%s!wqOJ~0Gs&?pMxD%V zJxS4`(CYZ7gcFzA#5}tdvQ14*Iiyw#?N_7;E}S_|nWb>mWtX!4;NxY``;0$}Q?VpQ z>K7HXBtS1G+gFoJDJFwxlocYALTfr_iiNvAr@LSawn=GxJncSflASWE7_aXVl^jM; zDM~55{!XdTt~f~TW<tsF(Bb3Po_NGnP3c&XL7Hz{SSO|^;mtfBM7Uc+QZS8s-Ggm< z#OsoBA`to1HL6;tucw=Gn20gYNm^401G>JBbXSvW=I!uJ<36=9N7A9HEX~g^o!{^D z4jvSWFHw0&k}OY#SShv8smZ_!!8LD6Y4&(LWSkz-U~_Rfx0gt%{cQ&&>5Z5e>%H@N zmBA5sfYR~=m`7?H%Sou`o#-`{h$n-m>h~-_&1c12-Um8&&8SKZWi|jYauH+#)r!16 zpK02kQ!;>nLpSpD)To}205MURvDg{^p}`MGcv?_kLMU)7Aqp;B3?;a8+6azxiU>a3 zAHacObMo7wkKBYk<I7jmSa-fdjF25D$eaTX)D#170B%u1ii;64NR_GTFo^H8l;Cj* zM3O>+E@2EQK?<-@)bb+s|El!Xu;6V?pl7ArWJDj7qy8`F85=v992o^=E&t2B-N4+^ zFsE8ShsXrgasRGb|J+00?$^hs(N{f{5%-_`2_0Wkv4e+2fq6R2?cuxO+ZaM0s<B~3 zlQ4&WKK*-seO;~D_Qzl+_!d9k$ahKo&gaVN79^mYbqsLO$FWDz;ET3H2Ky68$2j@> z3>b3{D*>!<j82V=tQZ%+{du?Rs(N3V_@^lz%?^CFp6VvdM-6`M!a+=MLNN>Spyh*t zl#QbM@-W^Wa1FjSZU7+6D3A<Xh!GKi9}p^l?EM2?-d<;~FTZT$=}L|7e<XGpy8k}( zSq^J3l)->a3^WtH3IYJ4NjQM`?0K{#@UZ?GB7Mc2!FzB3k!^Bfsw6-OV!$DE&_20Z zP9A`U8V0ohs%sQ8gF%H3Uz^fsUrZ*SI(LXE?576nOfFvzX)1y^o6Utd{xV(~XCJOi zvDktW0awGo&lQ~*W)TbkV}SWb8h$(BvW&!nC{~+6r8?*5Cf*`^WJwrU-*-MnN==;h zJL`Q=pC;T0&Ejpta3ZF8R%ub=>Jhy3D=hB5^^UO)t$xQ<NhQjWB?FDAH9tr3SqkLU z;K6)QKG=<QmI?NzFv)&(-*Y;yA_2_T(Yn^>@_NCxp0yWB5QiPy`Wl84W?L7}`m(^! z@Qm)}y62o=zXHK)9{OptREP<d@WW&$pUvy2Q|YivvXW=VD_3$dW9H>{>&{CNJ9M<i z^-vBZ8Yiq@&bJ)CziyU@BBrjn6{zQV{3VEb`^M>MlO@Foe+l+_eu?8(PqNOgr%aw4 z#J!YMn@bW-Fvw1omlvxlXVKZndwFtVeA0Fk`a@?Nw&yq*$Awmfl-=bp#R3sxvET4Z zgssMJms&kN@YR_-PB?`_GW~o0&WByn2jo*QIPJ;jy`bURuIWt|pUh4VWvt?+*k{;s z)fWR5nj?-Ah9HA39r1!mJO7c2W-5^|vM^vuY6D^JidmYXE=~qcbXzU3Y7^U80yWSK zs!uVy&@mT!(-ZACL_bN4ygTP8GhvnqsfDtedb_!CY)pOLT`$~}paDN*U!|_IvgIRY z6wOJ0B5XOXVle&)t~L{?-O>7<``Sz#FZvJdPL`H9!7~%!4^1Af)pNU8FFWhFP$may z14!_PXFR#FL3m}3F=Dx~^C9$ak|dn_BL(Ey;}{76CQ+T>KpBA=nadOgdc=W!uS3WI zNg+e3cm7lfk?#*}_4ak}gj`eNV^`cio4jk})Kn2Blxan66y-Y313YT}i-mVSEMmc& zK6!(S3C+T}Qmn_V@aC&vbpdKZZSp0JDDfI9`p+T$arg)F%KUguT+;kvENnwP&Z}1r zA>E+9Rslg$1S2<;VvZrIK)EJbGY+Xw@F&bz{m2-L%p*{1;9v-V$!bqfol(qNR%bcH zj8|(G2i9hfC5eKjIeOnpY;~VyYKl?gfu;0};$hDwb%{eNT|I5MtTwj_w?uBSL@G;t zr2EdvR>utE1QV+@l26kOAz&+z$~Y}kq+m3G{HG)CFI5z?+E1^~iAv+*Y&G2jF@OIt z{B&i3S_jE{RlHKUU2_lT%cUWI?g3({F_<QoQ#{<D>s}6tkviv5%t1*Uwb?iFS^m1R z`a`NYQx#hzYG0{SS+(W_aNuSydFR6`ddui5GKhLIp%k3rs70|o>6ONe^D()aL(U&j z=NO6l%1eHNc??%*EwQ43I@ox+P&mwFZmPYp<08=eGo{B0Zf@Ek@7BhRk&;DMg9<|% zXQU@$WIfFzqXBx8@c37U)4|pfWA;s(RP*&>HAt2Z#T>O5)TjvYDhlodAwfrqz&iah zIXGU{|N9UGUn2RUvPfxJ-)HZLAS8s9{o9An;9AoX9oGKoEx%6ltC4mGzh&LyM1g-V zf5IwD>ZL(e#niJtdewMci#*cV8pWrWA>=mv9)h}9sKsz-$7;sKI<d|}fS}m7S^!2- zaRfje3cC;t1B;Q-Jtv5eUbj&|>Htv{uWN-wKA9836UobmnODf*xYS-cg8ZL;L<Oc8 zLqrx7MmbfK5mjj$>Qf7f^d*1VhELtV!@)x7Ff9l7MyvN)&TcmFub()MI<KQlXU5;0 zVGM5-hHYd?JCErZBAho6MIbezQCO_wG+sBam*=N$$fIKHxJ8#FNfXhk=Q2=;T+=st z_Qn{bMMX)u&@jrk@E$#HcE61O9MU}sHo1IXxL?aT;S0geC7-Fi7c6ZKQe>zP3k1-P z;Q}ur$R`K9N-UqFC!{K{qL`&w>98M0hlgf+55X0|Z5FWFqOf5UA6>Ml-b!q!xhDui z{qs@e1NBBmp*s<HiE&dI)eP*B+k#luJHs=+_y?D}+nLmplQ&1wUIGDXtcASL7yuqE zJLurFa#E}-*V?<47x{wtQRDG<{n!Z$f&BAUur`In5_6PUyht^1Lkc?eG;6!Vpwa~E z1z@&Ne<n_imSs@WXt-Sd8VkXT6)rMUdEu&ldHs`f<~#Z?=F5Lq_Xlrd?R7^laTYpw zrvXO?HYuJ~@zbDDx;S$Ioq6ipDNo&nLXuRam|+`w^G6j@4)|8H#A2^|WUhyn&E*_q zkz6Zjw@q#uWRrLp<~b7#B8}o1gXjn=EIf&G3cKW_DRnInG~6$h42IWo^JeN0+{&6W z#&mzIQyS1zLL{QO1uB(?H;%9tU9vz#i*F!IZ?*m;rCc2fy(5aBPG0p#sJNz0+5-2p zKK6*Qg@a=E{51RaCJ4v|N=pk)n>wIcb#G@*Q$P%v2u<Xn4giG3gaN$si3ItO1_S1` z$4w~mluYTT)n5h1UkYCfeSmmq2=Hu(|8Qt(B8+z$Jo{P2<*0j-(W>lHT!`k2;W+9R zm*2|qEG%?Ctt!oVUve6KQyXQ<B-=DAuH!KLA`o7X-Z5IPMd#EtdcrZbYL*9}P_E`M z&2?&W>?2T%d}2l?r@mZLDb-1rm_U<h(_2BLiK1EhMa0d7oO+W&!7DoEmUt>xjQsrI zC>^2lIr0?V8rXt+h!x^59zXQAO={IXy$SDeTRa!DGn!vRZ7n`lMtN!ph+v&+O>H?u ztLb!tc}~Y4sV0|w!~UpQgudU7@WE#9M8%YG^7ZxVZM4P9tKn1W!8cuM`b^bnt;`k+ z0oqkFMn^qNoE-0IS(z_UDDQk4`GH8jyNioTz8FPzrq)25Z%;dDqp1?#WMws#sZmZa z598KSt&A_3*_)hYGK)>WI~^BAlp$iX#sdg;h@?Z+gp5#ke8B)cph_||i%461Y7XmK zf<n=Xg!aVJ*yGcwxTwgpyBQIE(?|X>xE&?qLOG4W`w4$9WnxK~LalpkN)*5`&qsZ4 z(rnQ9)$Z_w;*5GUVoV2^k7(E&x}R%B85>{7poi$Ny8iguz3z%2Hky`&1UI$I$fyy# zx_ftau~xVJ`*P{nbW&hl=2s{(Z!EK4Pk*o~>iM!smn}(Xi?uENI6NkspxslEwH1~| zQA5-`TL2Z#>eXn4J4M2_8|W@Y@C!yc&QP^JkqOpC?!*>i@-_tBYzFV0&*8@>kZa%f zHP6+QpX^S?dl6IPwhM^J(I_~HL!K)FF`r5aVKKloW%%+q-*Po{L(?ZO1gI<;V=FrW zpH#mLbyRj=m4hv`E(E}*;;=GVY0D+$iP%AJ5ksoMbM=*3$WYQ`e^S|S%NZ?}vlt%T zGO)#aVvCZID&$nKCdSedbD40ZRRCaZCY(Sjgc)S&!qD(>1bQ5+heRVgPgn1q-8LLN z>1CM5MJ8D>)$giQm23MMmm;3^CX=AHqcYEYFQu&DA-ighs2LM}l0xcaMR_nMymt7t z3OxMIn5PeEY{6~>tOPm{-siqu-*nz9xHuu=C8=a_Wr?3mP&qDK<cZ9up0aya66s7y z6U72UeOdf-D`xyfW>TDE@$Y<|rGr6sfI+fknNP04SmI@&aiSh_#5J@Gv2NuW3q!1o zSYJ6l99)efTL36EthU9w=Qiz>98HvDHy?X0YWi7`ojl&ATmEw~zI7hU!bb0OYSZUl z{<W1(EaQojjy{`NEQob4pp++%pG$&C`iWYRnI{sq;O;ELpliC3TOI{vHaOtQBCA-+ zj~^S+6vD?TE4KFHqsz<#RJeg<s$$8l(9+d-JiSv$L}xZ~B&QS-qez)$K)PQ0=8>i$ zv^0W@5;ciV3^EMVxw|C8pWPAj?i5rSKHvs7loP8WGZf!sXYUoAy0M^1aE`ls9P0*B zg31Z{hL*O&1>-`GV1;O=lV(3C{-L?k<yxxzz_`B!X26si;S`JXGjO(h=W{I~!D((Z zNG9B^$(I(*N3~=FJ6|y{WPxQ_)GqT@>v_wKkKnu=hKtM9q3>usDjtA@5^9}+#^T+= zxs{i#)gJ_i2LjQ!7Z`E^<@r`w63Rrw{cliXBvGJJ%wk;9Sjt=QHb{N(q1gA%@6=m~ z@if_cqGMIE?U4RZ-^kkkAG*#es;%z%`vfcQ?k>SXaA}JKg1bAxwRq7MclS`-2~gaj zIK>@`7bz|+)>2B#3(xQN{omy3tgLlDXZFnAGkd-xPuwip$)sU3)&4DfkkaQoW_ZBc zb>XCZ_|DN&h`^NywFnh8xD_*66-+04?B2P=IH0BEd7!bQinh*EW;TDP$c(H>>sY?r zdBIpC+_dPIRLu>F^6@)eU(#|*L#^dc$Y`+{g^4$u>~&>j2UcTRm)DHzr6(rA687cC zjVyv>cS~&9`6<QE9Yyo;CKcyt2=e%VjUMY<Dteva{?B}Fp6XxD3=^v$L%FQfxy!36 z0lHOjF?VkW@pl{CxU%YLge8*|tG5Xn0bIjiarJ;k=Nt_8e96E+%fP^xg&ZOk_su9+ z5S~k%L&Vr1nY}XxC(2eZT$MpLr3|=}#F+?I0fldpp;}uGl&j=g^)oOs-~eXHjM{Jm zM@Pm*$&%A}9`f;B!JTIBorTfWX4g~0B8;#o3=n}5&dv-R?Grj;i$w{dM90Qc3Kt5+ zuYtC2x;~lv;H4^f;<cL`+alFVD{Y$=G7#~v8zq=V36lqHoAPuQCGaYzwy>-glDk@q z&;v+bvCv^2Vq!Ddd^u|R+1eXs_x|C_y3^@n7DZMnhSmT@x;mQD41S>nAxd8%1}f4| zl$$RMiU$|}@lkr>Q@lk^QLK%_%4}!;_{aFy0h<dQ)c5C=jMa}D4J|$K&xR~-@b?DK zLV@CLiH_I%i5`=JU<DsJ^Y`;h!NIZZqZ)3*`xf!-w8)Y#sR@X=I+Wtz6imBHVHh3& zS`MahaVt&6;1b4pU=$kj3cAW*gd1p=;mD3<P=2ymvq~08)4fP{KBo`%#ao3;1sjPA zwcyd$B&~fS1zQ_FgB4yh#*$4HpLL^yYJi946kp8w163kYVDf+&P075uE4Ev~O+6cP zP5I|)DMc_52YC<xaY3ai>I47!=37=S^y)RRJj1v=e?*5JZElB!9G&|GT2qud3tRrr z$KJ;q+kc*E_?IF6wy1?AnUHnl`mT9buUji6;Rc2G&wMtd9)x{M*V4Z2xcxiCFW~g# z<dAKZdcPs4#$<{~O_h+u)Wt35_gC$mY`sm}`^R&zzLJ&&QOdXuiyT8C!~Bmu3iOo8 zL3<QNbb>+*M*eMI15qY?%JWoo_RvUl_1K(3EK35hl%ME9#M$3)%oY?r<HQPpRtRQ) z$dW`N1&2}dPcwo>qH!49bZ3{a;w?s(xYQGf9i7MK!Nr?bePQU<y{;0Oee*O#jH%!s zjwt9l!yyC(=#4`1ICCi88)(Th$V{nD!<<DvlRst^NPTdSFNIGeCuh+)bg~EL2*WVM zt>n{Cz&ol%41`FsNQUm)QX}X~V&N({IIulR4>p|W(`1EaZNdy5RI=sZqNO1u1N47d zt<Z~g6RWsqKBqP~Pxa-4TO_lI!V?aLLXLavLK+f>EXhReBFfCueM66?&%l{1i_w6L zKmY9mpMQJ=GTm(oxU!mz>_Z2+M$>rZA}P{BMrWs_X0z8-$7nizc`{`QAb8i>LM{Ca zCb5sAx%o@-si*3q8q>R2)jq=YlVsN?AIm&+F4Dy?F!j%0en=yRoz-3ite2Rpf8aik zROS<C3%Lt5dU%KD^xadyA&J}cYf;wY>2HEe-D)aA?Ce_P;R5uLB+;%N_Yf;x*Qh6j zouOzp6_~Tt{v#Ml>`TGF<EBJ7It2j4bu$p-R#6~VQWs$?YkLa(cpKcmZADE?vdwz1 zziSLvK+XiXniBM-oKVI=Ar`evJ7QR{Xn8Evp=Ul08l_M1Ys>5*dgpNYIMJ|t`-i0` zAUwOjHB25<cGj>UC(7(JY+3yY<E9ux#0{Bb)jL^imhX2Wzcbuz$2Qo}?bDh+Tn~ek z+f1IMx6nDSOEc>}N1w<e-@}4{$!pPu95BGnsd*P3IT7#yEVi`DC^_-#@zC&Tf7bRN z@B1OWI&pm-sW)QE11=6eNiQ|R0{<|pKtr<+BA;f$(pOgmpk<Lwy&vvzic^TSSwu<U zBTVswM6<=}%5v^dOeuUw#PqCc`FJ)lZdE7OqMa#Ztqi0wr65wLbx3977~>`l^;uys zSY%jDW<VOkRYErjk+v*4#a(ukqYKYkmDOVCaWtT1BFW%|#%e)CU(2-E+((dXTps7j zJo6EQ2m*Zz*H_m2yjh|^EK)`kqY9Qhq8wUK%@sd}s8Q^ZPNa88={2vh+czyaFo_D! z$VnA_UvS)XRI6uR?+Gu8iYK*;maC{S$35la;s|SJ8>Xf8e0<cwESQ5=MH~+S#1W;S zg!&&;q|_Lni)EO>ehQg@3F`r?hEyD!p_U+z->=yY-{du0PSd901ASOi=W0sPlH40( z?J&?<uyDv?Y@G%}{d(D@-5&a3Bj;@{@2AM#=5p87Nk&lLgw6=o|75LM99}2S5br35 z6;r$LCo}h0)H;w`{aIUAfnbw9ZA%bhj9UZV+;*-U%^$+-1{D=$c>%7dJDC<tj#I}b zM>@KbRthaHabQ2fMKN<*tYUK+jbqw&CfG9{38;>Ekj1!JpsdC~X+`D+4y&7yZp=c2 zjid?b`5nsy>r(t!?6^if4h-%A%`2tk3hW9THC`=OKxmFoI?xJAky`2jG&mJFvKU6C zKN%0E^T@ZqLk;(g;(tvzkLpC^zDymDvdE!~iLR4+D_v=phHKa9XEyLO%pt{YY&s$L z{uOj^f$~)<i$hpVa9PTzLwo>#Zj_jxCyaIY9OTb9kq#J%Wc=*sDPB;-Yp=Ayff%mj z-z<u`y|ubwx4*hu-u}(j+xjIolD+O~Hwy7I8MaTq(FP$N(kpJKWi%)Jb3M8wbcvq( z(&?oxYL&6Z4>Vt+`fs}JJJ(smB}OEUGE>IM2l7XACa}lWzownS7Pjoad$zvseoDJ& zf93-cvj&G$2CUL<?)VFx64-39mYOR5bIkSm<$crT5rBn3;-fyLQyjea<~4cE==O$R z{^b(4nxlu3tm4=^p@=b!aS?+36#+${IAWM{#i1={+Lb^H2qKF2rY1@*t>LpiVPO&` z*Uru7DoNZ_JoiuB-hKBLxal}sA6!d6`!NvKT|C(1H0PUUU8T=zPK}*f)Sh)UUE5Hb z#%=MpMWmY(XPH+`&O0Na*T5UE*2!SuOY~!5<l@m(`rOAYKbsI`!J3&|?3cyP_>iCG zbC%<`VJmmx4NhZL9d3<osty5yCF{RI58uxwRsX)e|I!kNZlH-E_~+Q9IamWn9n zu-vMll|D8r)~kx2C3fJfPzFyB@5ez=^r!8OXFjIlG-RPtR;#sJqt;^C_F+2p6yY7X zTp0xL<X;t?s&03`to5>;+>$(ebcR_D(rr#?EB8&%!Z~@<oFixJyG%rc+9osK6j2PF z<(3|e1$-06Co@8x)A&UHs-?8y<zC6@G5aCZDx7=w0O3)#x^9Yln$O~1Hr@XIhtNx8 z8)0}xTC28A;z7};Pe1Egc=sj^YG8dzgx(kqA0R1Sv|-9}O5RzUQ9skSAU$U#hVm5U zVt={$$_`&w2nE^q_|rhz!@V!vyRwbVp2YA1K+fljq`wriYojh#!nxFhwB(6n23|$4 zvWq8q31R5-5>D1LN#)}}UD;uC_Z=|0L%?f2KDitL|5{KuB2z_(|DujJw)F1eiiPOU z*aF2fpD6Kg-cSas)ha28m|cLv-NVOsSJwkItQ@$qp^P2BeOXzE7w9snqRWK{$EfaM zmeO7_OxoFcd1*1K^gM#!Do$UmOl`Y-jJ@_gu5P@YZfPN8_VGvnEqxyjXfuVXn{B=` zi+DH??YDU|JL0LcQ!u!bv49!_9k0L~v&J9~SKZ=Dv85Q1Ew{Y=m=s>byI1RfpXSZN zVyERqZo0=9B-$ZBM$%2eFM!)KwE(6#-Vx`>m{8~>Akb|sMUl#R$$lc|tiY>YVALqm z{Vq@cCB1Suz<>o72y>5P^e(!&qgEVsB^Dv-k7qm9sOi}d5I6gBU#56+bkeS#sQ^q& znxmlo@WIpxs_K4s5CXIK_bNNf|D}L?dr--6%YXAP>V+SuA{mrdV`x8A4NmT<^O$_% zLn!I7C<YiiR^N;D&b<d(9*Bq(7%%YlV)Gb1EZoKqwmg>vb*1!nmF2_-kch$pJ4#c6 zxiKRNU7R$`_@RqIS}!lAPl7JcF4+QIPK85E=l$INo#7<Y)`+%Ijhvyi9B0#%yIH!( z^{`iJ8$K8qG5}51ZglTGX$KD5oy)8zUm_Jn#n{GS^64_oSZX}T+;s9Jd6t$XNC{cj z_q>j2+>tB)nrqjQUsYAexoYONv3`wg$0UwC#8l*>G@K>PH;B<XMY6fbhJ!yOYhC^( zi<XCu9vH*;ZTli`VL|5Pw#)<R{=5XpjY%pM>D$^+AL;(t)vEW^@Om@Q<I4x>&W0l< z%L{q-XZzWG`D{P$D68`MM&9w<_mVkXciyfoi+h-@W*t0naI3fBXDTG=(=5?q?vsTY z$l&qEI8P#NYv|s-HSf}_=#QFGJ{jaYgGCxbJ#t>>zlUu;xE1u?8C0~n&lUCN^26{4 zkQ|&`yah7K&?Imzx07Kjln*^aNu8LuguaaBC?Y&bO~>k1ZFKCbxS5^7Qe2fZcy3=< zna7P;f!dV6Vl&nTeiBkzhYIKpJB{beHN=3LUG*IHNKJO>aM5I?Yg*}&5HAQv$2{8l z>oepNH@TR&#G;{d*_EdT%dG4#<kcu}Um)~~YOS}Ay;hDMhWuDsBLmAK#;3GmW#u5k zgs_<?&Lj9qtk05W+DG{#%?DDFBTYmBVWFbHGoJ_9NxGozJ>p}I9C25JVh15EW)E#G z(N0xM9Y-J%qf)s!<cm0*5!Ly)lY)TGsZb_RFTQA4=odewa?C`-;*!$8pPGsu`OG{e zIki%|s&<4?wD*p7j*7rDd5mD21;=a)9CPfa1g0A|4yr{u7B4BF;SiHA?!Io5OlFjr zwCmG%>>s(`m174IAwq$<kpdz3&J?!rfhsw4M_J6}79*krSJ2chQlBKG?^=BJ%dE%7 z<l2=W#iwcL89N(0f~={b@Jv@!R$2gIsb0%(=n&@>SZrGY2F_L?%cJl>l4K|1J+SHo z6s^7$?(ix_1bUAbGr?`J^g#4U+4nsff2W+|b4Sl`x!Fx4Fqt_qh{}B*QatZ2apnq@ z{>#rl%9DWIE%p}bo2?Mpes$+xRB71Mp#;M-x&kTLk(~Dv{*LHW!E>_^V2(E${2fVX z2<B|Nnnm`N9&&Hf5CaE=zV^i)Hh?Zld~xKC6T791@7p1;lSFcFs9`TKgNNm_!*zDO z@S6@Qc98GCqBIm8K$+|>4dr;6_(J<mWns~`iPXI0kh>9a^~DpSkHtq}<R#@m8c#~C zk=Tvjrs&ijJXsi8?}*$KBRv|#uxL`}9bNDKObNNnlNnHNb<f{djhRw@xIjDj^ebxq zzJB=YKucB?9MkhIzT#Sdniq3yzJ;KPPh{#^cfd5<#6WMkuoWt4&PC3-s-L|dqd)Do zv;HW79dU`4N#fiSLOH-gvFd~lFVXSs+Tw9~wx8b+RIq*F1zmyBtmE$#y$)#mprxLe zs6)+f3y&x}Ob#?L4IEUYG~`)(ei<n%a*!GTWwf4<0t=uRxq}i~Xy9w1GE9ULWlR;1 zg4QRUqOezt!S{k!&ehNt&bro|aBHgPKdS}nI@`e|?r~e<FhCV85#_|R53qpdF!Ch< zbhcE9mG;MxLRMoU=E>^fBJDeCS!)ZMfVZxB)^>JYUU0{#hD72ROkURJWTGZBto^VV zbOl$!+n$r2^IzM2X9&{0+lZFogDSQeuYA^O@k!sspqhaRiDd))+`{_FcQhcFG`ZD7 zYoQepnf<-L_kl@unlneKSVpXXQiNn#oIxx{r|-(-jLNQoGlKoJ-Nog8$%3;`8WzU= zUw!ZaIw@{Y_;bq)_q(B;zVL+q?yTV|8TNI`-Sl*XfEL!V&fk2lrLwPGxid_<%-*WI zg=q08mQnsC6hF}FgV7-t+<gN`M>G^zJ-g}}7{(z5y)#^j*racF7mu|(!+U6&h%{DS z{d8aP%-N#~wW}SHiqQC!`f;G&_IxKOS+SBQYmO3X&bpFW{dG*+k7FlY@yE59!nx7s zo?9;L#O=*Xkqv>;^jMjMDI<@17>au?X>3ex3+>OX>O|kEB(8W{^5~zG_nj8%l&t;2 zWBQJwTy3LTZ@?YkW_fd?8iOh0&|$8K{rJh&kCBt_nRn!Ue@-cPG3%%xhRYlkrzDf- z8Bh2-<lXz^grQy5TwM{8^we$8i8LG)#t90~XZ!gsKPj%m@H0OrQ+>Alo6&f4oda*v zF-DB(rryx|`nH@($Cw1ACazIzuCzHE74DB;lzz5(0^ffC*MTg+=vRR1u~4btK4c3H zB$xvjS%i|(X&lKD7kSpbf*@9TsU=C-rJ6C+a#uxXlc`F3|C7ph_!PI06Ml&z4zljx zs%CK&qATs-jstt+UY@!*g9}~kGo=n~6hVl7N0jLZbBMR?{lYbI1pAy|I!IP+<r@)A zW0cj6Ry!KNQ{Y$Z0YQn8wv6CgcfTPLTXbeHW~?)_Ns7}crA~$?!DWcI_{a>kL36PR zFvdB;76T@UG^^ZU%0NR}jU0=7V|zxGWW9%+Gsn<wZ|5mJ<;Jlb^S#LFOg!_l#;AkY z{Ui99&jFO=ssG}XK3mkRJ=xz_$VkPg3Q3G;OI%mBMZP3<+Mf=eLE<HyE~HVxaI1W6 z`U~seGV^b(+#ImqDIF7+qY6yTKAOcwE~Lw3L^u>K$!l+UoHd88App--G9<+WMbD0x z4bntV+q+2Rzh#TPrP8U!FSdtJ8R#H}BY1)0WTe7{zE|H$xg%7HnMi^)Ic7Vhi!d<- zQ|RM$Q59j+h?#UfyCuacqiDl)EnKXam{K4yF`<Eul8j0MuA%`kuSzzP-Jz~ZMp11% zLVL`2vE-exSL~}*V{aT?TmD!U{pC#=CKTm*DmYJ{&ZP>QQVB<Xgk8~CsC2DCj>kqk z>-RMhaC-8-+=OkB4Uu&VmyE9tTQT#F#)8a${&`&@7pMa;Q2_K4K*Gl?AX$CULSUUC z87J1l$`&LHq(!NaaI3lQA4mjz{L$PfXb1RI=+Jm1DeLjV<jEp~zNo;{(r<lIH42;{ z6<Ux28NVMdzw>uHPeC?16gKwYk(U@FzgcZv-$ZV(w;(J7KU);`{oo;e5rSft>Pe-{ zOcuO=64~vdxsQ`o8_lEO%4?A=YH1I%DCDrzpjqau9rx>|IVu%C9A$z@6P3viq5y0{ zKm_Rd<=ItPHg12%tV5d{%B%fVi?ZvP8dGxn>di5x%{j_8*VUT1iIAypXWM9eEz1d! z&;{ZSFwn|rFd}gxtpFSZL?9Dd5Gw9plg)q@{^@^#F>{ZH?{h9%hJB;YyYNHlhpilv zzdhT}qt(;>7Wcd(q)%Nb56hz8|I{>0H0I>Zh}o?Erd{d!%AGKJrk}N_Dwi1VU<4oP z*D@<7Fu{_>44@8in6THti9<HubSilK`fvUh{NL+o^H`;Wf~eGYsi+yl=PI2SUhuEx zm(cTyd0RX*<UWjK!B`dsg=hlhRV6nBGqMyja`2o*bbmca3OyQtgh7LgI*E#g_S6&H zPd;YHb%a5kOgAzm8i`4Jz#T0c+z`X2IHOr_eGjXogIT;1#mkVIw=C`JGwHMB9tMc& z5MYOlpoFl=J!M8RY=z?cSV;P%otaW*sGMX7P?2-6N&J_HLZqE|eEU=)iSer9g=g~| zMQMAwz~5zS&|<nKhUM~g3T6Bq#oUMnTF*URKJ$4>L5b@CHfNEbXjVr2F!8%_10`A% zEEZ`8^6Fde@~nwkQ9fpO+u9OD?a5!%x)Mg!#r@QdmTyxT7w?OwmTM%W`U>o8n4pXo z<aKKU<+Q!;%wJv${&KJZnJk@>-28k5rcMRE`|D>?Oz=ZAiK`tUx8jtn=&(j2p7DL- zTS{0Ld0y5<Qd+8LI`Z`0Sr0*wp^A%VQwLQxKk~2OkWM^E!*;AJ@548j!8m9>M~Oo4 zVbPM7?^Y!*rQ7{eIUcdx_B7^FozxwsTH`20uQW94sdrUbNP|qn4X~@*CssZv;0Yqz zJqxm)jdJMhCKNZIGeWU+B40CbS!N(0h!~QR1uabxVsjyJefx%rnl<dg0Z6Qg3Xek0 zHt2Zf!zulwFClZV<p!o~6~<OXLh6^|{>IBqfy>w3$GNs{tco%&QGuwLZ_a;tRlFr| z%w4<xJ4cd~b1GZIVAbk#i4e1NZho|VT^s|pgB~rdo9V}8hCUts)SGJm;;2|s%9PWi zjK{zF{yzi$xsYY|wB!nNw@JB2l%uz+)9`MWlwQ)#qZ*`s-Xc-s+z0r0B!1K+HzfDz zS}hsY8H8!7#?be>87aTYlWC9qplL%^4Jjs(S-E0Sa-6MYUVro}F$`s4ujj9Q%Cy~g z$L)v*kr~uwW$Eh+4{Z2)?rE2>24y)1c;n_LXo`=Z;JyG*+q|$x!T@<u3dzI>)J*bo zV`&q#a|G*h5riBNC!$Z{GJS<)AQR?>BFasV1fTf`E2fKEBVFWoCu5d?DA1*D>p8tY zLT!GrYWvh0WlD*e?#+KGbo2$I=6-1Jp#wTNwE?=tAcvdw0d8k(x>}TlQ4Ra8G9>6| z+r4}tbmT)8+nPkk)<}bWcHm&LD6*Y*WT`Z{pf6AeYC0i~`!Mh0Ys$xi&Og&I*{>7K z37a7B?&mXa*QE+$S%H-CfqTC9L^B5JUt)Et(+HR7>I_}?&?R4-PxA@hqZL6tA&m_q ztBb{sny!_ym|lcCL#o+^6`GP^=eO5r*VtZ9o&Wl(y?>k6evh60W~wZ*bQr^w(dQfl zN*Xov4bsTpmy@q+axw>hcJ%cw1~PXpqzny~pj`HRj+BF=M#mdA0%M*;9wMY_{gzQt zWzhfg&rGkM^fMU_DE`?{WIs6oqVVYcnNp)100xrq+e{luhqJ(^Q(61f&NU{d9d`9( zw8u#s#dN^5;AhW@Pz&s<INTicfma(JRDe(p&&RJ4M$3k-3WJ9)cTjLc$4x3Kvu{>A z&rZ`Xh2M%v^IGG+pkA+iqXT+-=;<@TWzE9E`=Q#!J>@t@%MoIu^Re#6{l?VEoX1Zt z)5hB=YyFcJWE$)j<j0fgF#3r!>{t_f0o4^jm%&u#<MHuJl^+~l>M8L)w?d^M39|Z^ z4&+I8G3D65OO2CV=j~y)W_|a%u~8v4a70&u{@^5vF>E!8dDPu$(rd0rfH|Dp*|(DL z-CGdcmW)D((q@rRJQY!u%#F)cFo4No-(kMx&G~FUA|jLGZc=;m59+<Bx>IUIKXNv+ zY&tfnWoos#Kf-<q((IVhvtd~(3sZazlJ@MiuzY(po&UzoS42Aj*mnvps~XHXD-m9S z@#UoYanbc2uBpFLEf~Qtj6;@Oia;-N{Gi6oFXF|Pv7%>NC`~Uq!i7cfZ67_kmMZ;J zc#NRZ8X_8jlT2y*!SVY0)y&f>>D7PT`{?o24l+|jDd3i`XZlwcHRoDNj&1mbYeHp= zwF3}Wf?MZ+fSC?phKD2MPJuHrK>%XIt6)Uk=yCoONrAZl*|%aac<VI~JnHRLP25Ln zXzuXv-U|+!d=b2qz{r497zI&}y+`ig`?izlhk7m)StO>f>b+{Y%!l%t=&!LINyc%- z-GF7q;4>db5fgE*XFhKPOUA;Kf+YEB3?u7gT7o|2?LH~Zysu=&p>Vk+;;15MQtXj0 zX^v|UDs4SSJUFs#=MbUpI~<Y}y*lnl(zJ7T^dzIiA*|8qQlEJe`ni5`33b=^;cY)H z8>C`q2^Y(Y=hX-iXJUDlre5<l$T#6<c`aea4Gbif^>5u3!k>aIJVVCWvyX#lBiTj3 zq7|JHHQS5C$idoSiN}j#oQRO(9Je1t`qA;1>!&Y1Nwfyl;wGkPCDGob5Ry#MEa4DK z>Kb%~6&Bg$2o&cOw6!X=JOlrJ%!_TgBuJ{prWW;*#6GK2Nbo^GJx%;n;!ovUC=Nb( z$Q<|WYy5ZS_Z>zwa@5kX;EiPV^Ai!r%6EGLz-K-M;wn$|i@o_f8mTEeWrxIDie|1) zT;%>vpqh_=wc;HNGqG`tQ1J=4sRdhP-ao#*Sd17PAiVzbZ{oo8{xWt-nfI^1(&4&h z9WY+uWR;INfm0cwg3Qpx$W0?>?_DmB_0e0>);Agx4Yb2d^@55-1x>;Z3ysNTb>S~f zp?}UT9oZKBf#odwwlNSVHBe;kb4TXnh~H>g$*pifWiF+y5PUY>s7zxdo_OG_M($O9 z$4J{NPkhng>PoQyWQjmB4uT@Z>;~A$pKhO@^!jaKb&tP#AO9?`)zoxszNuaiD)+f( zt9csx@zrVX{ryyuFX;x)8n=tF{|Mds-biyDN4up<9?T~i$K%hN_vX4w?&tr69v}UG z_Kr9_ec&fPpM{&ny=0Ajk^VmB9{vej?)Ce38-kE)ahGhULq|qJ(m<*Ywz=MAM2<H| zQo%(@(BK-2XJ$s)6Hwd(^bN9Vl%S!5`dhpx!m(aUM`0!f`-?S1P=PE6BWj!YaZ>^0 zdNe>>9LhGR9FGc%xL^lKUub{IWxm`COf^<3(vWo<m1srqpIVXJK`VEp#Xbh4{`PR9 zX<4Kqqztt9>1+xNOACv7R^kb@WasvNh6MnM3JoJK$OKIr6K#p!nP17E866!7ocRq_ z|0l6e0m)*UG6DsipPWMQMF7WNVxoq+(S8QKj}{D!;mCbE=*PCJ79bN6q6`IDSt|^5 zn9|x>yX5l8PJ-bIto(rKgFC0)=A`hSlC_4$U!?Z`>CZpfkcivw<!vKrW`si1k@~UL z#sCaQPkj(_CmTH*C;K!xNvCAJ`0X*#(a|U+OR6STxG<t`6_WBu`Z3{BD5L=3jj4>5 zB8pUqlwoivgN2fLs8kkE*<*q;=j&TT{ml(dP-NvHkjGnLh!z5<>Y9<Ghy_1o(7y&9 z?!x6O3w&4IGSQmUUb~Ty^`#t}imHTkHpgTk45iV5y8cTi5JnzJ6;{a90UiafVrN>_ zO*6cyNV1nm#|Vo`2)ZM{%mt>jPRV32qiMXvr$<S}EqoypDDYzim6RcDOwfdBsK3f4 zUS|xfn58mBCQlu=>+1o@b|MsDMwsgIhfbhdW5i)_2u2ic<ghDrFv)X-;B0@Tj}%Oz zB;ruYdA6S*p@sQu_LXEn8kg0H_wv8GlSU=g#5v=}rAJ51(p0=sZAshE6eBLJP57#F zV(KdGMX9FMxM+%Rx4pG0dE5wePL7o&t&45U@m&`JAuo3wF1oTZnZc2zC5~MfPyZMr zT8xOWsS6}^n{-+Mk(x*ie3rt><ksJ5v5(j%<z^U1b5U=ln%Lu`{xN3|AftEn7X>FI z;fl2RQ?1x;d4jyIp3%T?)3yIZ>v;oIU+rV=9sk1IVMsRVGTX+wyJ=AwkyJ;C_S3X5 zM6~K(qZBUuW!dB(A~4aekapvb4!w_2y^p;|5+IYJ0f&Q4_Fo+oq74Ib6cN;@(O04^ zeD9SN4_NPe{wVU4WysFkOKG3GurcH=i#$I5woQOM^Jy121IOb{%n1={7Ms6-6qm(| z8g$a}Xwp1B&S{fc<tF_9+E4!1iSo4iQjmfWHrqq-bEV1xg`s_k1ed`Mm0(P>mb*b* z%^w571PW+;)pS1kRDDusSb`ZbR0NXp2D|{f_>@0DHfk<x#?)##4oYn<tV|KKN%Ynw z<zEzF8#o+?y>jlUpil=JNjNuZFA|i{L8O1sL6jYo?%YQ>N2Qgr^edLHW_;qVXHDvo z+p1HdR=G!8*V+n6s<!Pr(el|yt2Xjom&Fc~>VKL{YJ<ER{4sI9LIv6&{#D$kODpH? ziUaGge7f@*GsZ0M2j_wHDyP>^?&k5n)>5G@Zac$NCp{t^pY)l}&Py6_NcX}5C6ge6 zDJ^T6pt8FNgEx)h-t^2AE4)vzzWc?iAFv_IqDW00SEfQV0X8K5Yrzf!JwfY2xd*U? z2QtGha-XPW3d5y;)Te0V26QrKmLR3q{W-_UqMYf!wNy@Ak?p3sC$ZBz{e^JlocF0} zX~!kx7!uU?b0_!xoO5cXc*}J6K}(&DD^qqSd0SPul0f8bsTm{QQi5<W44nmy8&$|a zy7?8H2v7YMIyw8s#7T3#h|`iYApP1UlI*zFN+*imtL^dM#9LNDR;)ZwgaTs0NY$XP z0$R1a%|s6L8BI0}oU`mVOciipNu~O8BjXXpwC?XvdAoMX-V79m3&1|~JmIN3PWYl( zW)oaI2+U}2?bd$g^9aoan_z62ZM|~Qs%N1|9I+Fv#2DrH$b)G(EmFFSyiQf6H#@p7 zxfBcyJ|IHLLH-Hj2kGQk61ksGloOM{k<>W&*ytnV$&!tNwPXtsJEb^3&@)ZY%7P}k zn`Ba{Fz5;yyWIS2zW^7m>=GQ8MQ6=48x)E8_eN41b|qV?^jb~65FYvw&wSWQ3#xBi zI^#SQv~4iu>O+v0PTsIjRmq>ow>l*i><UBO&27QUBp+3NSnvxsj=1%<cvc%fefDPV zfz1MbwLBM_*J+Lm7+03QMfwu4o3xoI0>r0{pI}h}DAF|@hJr=3Y|=12e!eOz;Cj;F z!<C&5<J=rCe^x-L)8`%h@2yw}yErOal;iT0zH;eG0V_4jKCL|g;uz|1W!mfz%q zt8L_MN36F|tnX*z9J%(h*zoEFEz|EK)^CSUIy6Awfg1j9H4x^5AV@;M<%U0*OrJSQ znhgU3vk#j=w(k@WHBVWSEp_yu2kFD*2@A9VP%>?oPF!e{hc3lmfONY=6K=s|d9Rpp z8|dj}-dPK95X5GlR0!6+Hm}k>95+muQLT=0o-<=1!mG>vvGi*#c~*~^1Ref|7K0dI zK_8DU4Un0=d))iCNz%#q)YZUW^bT4<kbx7+@nap{_)p^R&i?n<b^TAb@rMDMu=8&! zk|C7FK4o63ze1`=d_v0%qPO$43{CF-SqA|gs%*)0C%Ef$HSj9gsZ%4wDm&x;(aZm? z`}y_h?gS4yH>>!w{oF#%z>uf&mu^*rifb;Yabv3PZ=G%wv!bKyWOxr`AilZE_BIF| znTe`f<pu2|50*-iBQ6PQya3TO9bJ4S-4qpCyatL)`y8e^5|(CP6sjSC1-NvEl59Wo z$d}2Y)iUJl8xiJ&-UPpr(8Sqs42G*pL3rY~$qhjxh}YMbt9E*1e?QBjCLaYzzhwke zYBAA6LfX(aYz5}IScL~Z)8boc@_t^%cK76~#=N%g1!JeShvzv!|5R*n@WXauVssP| ztZ;FSi*ssHcH3W-f6wjzbjlmlQ2RRn*ZuU{I^VZ<UB6yP1b#8+D{6h|b)Io8z{8b2 zudrT`%odoEG6SU&Q@7`Lim$J(cv@hO`cG`G7r*aZeE+&G*!|4sM&_yi9p@rnN38d8 zxq-&&goLb8jh}fd(=8@@`Pnxk$P$>baTy3j`-=&_7+7pFu4M3neN#YZI{cM<{3_%a z=wYGA$HZuTMQ%vwIN^*^3{ohqB1L%nUl{>(-!_z<wy5dC%0x%vIkhygKFC?(xPHHP z&&<c|P{dVz(89<9JXF4<9BPeeSm2{a2WQjX#u{}?j?Ul?*mkPogE)>o;#8SlD2~5l zEy`ROUZD@Kf3d1CfRYmg87)+EoVf*r*8Vt@O|Eh>6%lJETQ1=qvkk7XuGW(7+h9Z+ zo{c1jaDVVfFMCYyRK}x}dgDkS71Eol;q-UGQNaCb{wPs2?+Bz9tdAV~z4HANpoga> zu7CbT9IL@;{r|<Uds!8*2FvL}f=q*V+0z62f);Uv72CRBS=N+w|L@rK9KleK(dcJp z3&#p|L)0c6e*A5k%*|fAkM|OSN9$adYm%&tMjnJnN=fSx6BmoQ@Q?in!50)u5?rj` z*3`w6<4P%x6Tuz}(&MR0R<2;hVOI1iW(R7WY^kA?6-cBC{k3}4%5Z{3T=NBo&k&at z+|$b9BJ*aPdVQ?wTXBHhQbcQo_gxKz($~C&r>-=fwV8_WsUC#>sd)HF7-`JUmZx`# zYOvsf=hQ09?NnuGHcvLy-0=(4J`8kWO;(oq6V)~mJL?GTOnOVGRle=5`}g`s!JEf_ zFB{7uH=DIcSSplg{ujTv#ZC0K=oAhj>!1Ge@#^;6za?bG{Jy=%`~R4qLI@BRvo4GY z<Lj?JF)(x!ON<w)`3+ta14DMner(>+!ry(y1ShBy?5JZHf<TQ-(o7VEc!?N1m<cqb z3WoDlsgv`SmTI-J@3|_tFtz)^YH&7BxVuzEI)~$S51lGzx-Wk6u4w*Vm5J~@4`T(a zPhG%He0W>d(pq227G~S!2#TOCLKjdU;#if{vyfOg;^t@RrqJ0c9DaU*$J03f;gpDu z+6(p;Nnp_XOM^FMadxvSW^E#`YMrVbXRKib=D*Efmv4iHV=v~~KD@vERr44%nX=zB z_2&B?K4LUSTZCWQe|+OVeSqWFg5sjQ7iJnnwTb5DPZ#fdADi~Q-$%5ODr<HAx4sY{ zDGe6)FanC~)H2H!$1_T_G{%ySHOkDyxzNh*6zLI|kra{?_y8j!Rc$=&6jTgsH{mlp zhGBqe-p||b{M4d(7&>QAxfrUIA(vzLbvl1|B7Y(~3S8&zpU-$dQ+6A0Y%~QB!;7;G zC^D-0uHC>`Z7!dZV6?p{@Njb5eEw1Ri)9SOU-@EbrD~cH(Z4K;w~j)Rw-FgJF($vS zXl{3DC2!)CmtVKK8&Hqf4r;;w42c=NYy6Zu=Z=7<7tm<X%BH+@ZZYE)J>zG3>Fs;J z@g}2GDI&w@@+RVLu6kvB53U>I`S;&r`M*yM*?cfal#ojuHoiOHzy0%XS*~du!JlR> zyp&~P)o<@peT8Cue+3)P+(K)n9D)t7q6cS3D5K)T<|PMNRHpoOjs_%wlX^JTLlm%h ziV*HXLLA&a{)(!_p^1|ekffG2t&c;Q;g_zSR<&Q6Es~d>gk%ZkyomX&<@vs?HabJZ zS6nDQwl!_7PC#dDB$uJ2??(aAkMKQC(y23-hP--79mY4ySxScr5d%^KbS$%URwUke zST`@j=XI)<o15cA*usA*8M)nE8hs#BQ^lHOR))F_YrZTwxDqD!8=)&%R@Xhrc5V3N z&H_$vey3}4Jddq1=TY&of&1QznrDCb+`kGj(d>J6r8!$s&4VneqKXGSc1vgH1D5c1 zI_q(53SqrCO&l2tdCYVDZ~p^X<o|(jz4ABf7ctOoLBb|}pn@P@6mvjk7sW;wM23`x z790*<G7QsVE&;!M<;fyjE>0`DWRXV6I;n*QdeVzU7gdgR*GuzojX*Z28dU=JcOA97 z@l%+Zd;|ZvPdjy`IYpUXOi*H&IJ;ZXc>a#ayS6q|-ew{!P_))%*2u7+wL-_0u-p#X zR87jxoM5<xAkQE(p~T#;O2_hs`N2&_CvP)i`mR`~;j<$?zo_|nKE1o5x{vU>ylQBX z#S);px+XrFp3@ISc$j{+SE<lF^f-6v==JIhf3ywl8ldT+G^Vss2FSo8|0WpXasTPO zkzWCeO4phXcA;K=JvRA+Uq9+fSW=`{9fhW`cG>qUdZJBfOt3m5=K7h>KdAH5eCl6x zEK32#PBbqBPbF@3Ua-<YSu>1JgxSmsuGr^svJ_8<i9qQbAp04?zkh`qB|)JHFMnhU zne4d>%Ue(C>YXX!C(dMEHPcxtS|1RTrCc7z&rY?e=SD4YV=lr8#zGg0Eo1AQ*E!@7 z=`pCyU~9W?U!De?Ej`$OlTWUcNH0g_wY=OVM01kMa@05bopJ&QvJ{_0DEv9koS7pT za9{D(a%2)d2^GV1A5R}C<Kk)%oC97Rv<4opbE>Lg74w&rruZ?)zRAc<WM?hHI>3Pb za%-)%9AioB-d>FWMmJw~5;2MEaIn%8#{Bj(N$^yrmod=?XC-Hn#|>Q>`efU`1#qk+ z&-M3(rugaNNV39TRCF>OKl8bk#SuF~x>#W$2!EZ&CiaDtk9ZD)GZYm6(C^SCT@I0; z)WwWKMaNhBPz7`25Q`j>{}!K9B_9rVCyRSZcqESElSQ?#kF5BJzjBO0lH>g-!`H2& zXo)5oZO+fjn5H1h<akYmZ?8BNFRvi0f*V!3H@IKsKIAS*ti>y?T37Cz>7N?O9<|?} zysPhaRH;7*_6K9?NK5)E5r%Z!dlE0S?4}{%kXI1ar$%9a=8lc2h%@3X`jq{Udhd=} z|C+Z;o;A<J$WM2O0B+#Hl%}Q^BjX!LOrM<*Me;&vVBug1Ly(JDIf>bogSQmo;5itB zj5a>0$BdQL*_2>vk|GEi(0&=8uG{gmx<T*;F_zlTy}jA!Fj%EWPgC>E=eHb=*fz#N zHjB8PqS#Bxh#&@+$awr1K^l!QRz<#S0p(Q1It02oV;>;iz6cP(*SIMxRJbofHHC-i zljqx1#+@~4B5{HRw3aMk*BDE_JFkhYoSec^V22znviaPc_)%GR%4`_oHGU>KJ;V-h zgcIv2=4P6eucnL-r-vE+TaNQ6{BHX8yaLv*W53!EK03?Y8Xghu{^J{3B`=jDA%nqd z+;H;xADzE=<i(10&XW|xMqrF45EBpEKXyluvOFSiVX`<yta|jn!J|TV45<Ie{V>w8 zmphd3frW7m@K9qQ!R0wPPbaD%0cJt~DP?jr5kTUv7#I#~Vs0I>MJMPz6gT1?0F#`a zL6NDb7CRKp(M&f3k=INa&wPGCO2xLvCssBnUbA+p^2BY^A%j!4lTGOsk`9OB8!mSS z7PRj%q!C?;*P7h9dRU2QGFqXGp*y6Cm+<kB(Rg=k6hNpQab|%$f}$8m%K3J(Rxv9k zD~Y&LqoFF1?4TpPbO%qSg>p6DSES%1OkP*K%>KKcbXeQx(7^5*eh}9Z_kn+UFVnQ1 z)g?uir39SWmRZI<X4malkBi!XGN&?<cV)~-Y}{`q8_!3bsj%9L(atDBCs&4So%dZq zRBNJe*%S%(Nh2bb-SG=>G~6M4t48QHy4Hy+S}hm8ELa>EDK?<V$84=zOunJcjP%!r zKbK)3;}Z{U6N&S(_Q<=bQYFS0fNDp*P<JwKNc_09tW05Nu`&4Ikn%r1ay4Sxl@oL{ zW*NH4`k%!wfN*@@42jqPg`wHx+H8WiM)nSxEYX9w?7Dq0E$G^yWI)*)lwbJc2Vd7d z(hMothWvR+Y6EFOfuj!@bL+X)PgZ!cRxv2NARh2FCBlidf`?QE7U$kJ_9#QfSe+uQ z?g-nBs4!)uHTwMrMlVzi-x9QY;-84t5@piFjV}%~B)>1868dR%8A{_RZLa70V!)e8 zajOj0tKa8h#_O!?F!b^zZfX-_i;2KMaFv}_Hm{m;v{lgkK+Fjb4##}5C6~Te?9J&0 z8Jqs$H=?7;K@(wJqED#8l<T|TCtZNh#YjHc45JQpL2hE!wG(0I(wOlwFxf5)^vQ(i z*C`wy!rFJC-#V<TRwd=|pZ|e0i)|xK&~0ckelsp87pEHJA078$UN>Xc9aA)+p(LEh zU`~~h3A|<3KD*LIH@M?(%TTs}x_rrv9VsI{I*i9E%NPK_QwOpeEDDMN*G$j~4skWM z_~aMvOVUz9+rcXQ#F-gxsXi80ivo!<vTR0YA+kl=E8WogE4X-CDCdHr#!#a|6@a<s z#F+7f)X-$H)KV~)N=v0m(D8t$r15F=b(k7qaC5R=v9?CqsgR(xz#0>qR{dV?jB$Sb z5O`?{+j{<Fk>jT-l*++IhJU`HL#BB&IKxR{3B|ARcT6JI#36c%Q=HcMxOHIFb=OeC z&q%2^3F#@R1>H~~lmPL|ABDd49*wmo>s2Wf(~V_PDT2hFO-;3+|N0BAAu9jlV}#Iy zk}>^ZIrNu05IjTpFLpS}T9O~ek~%(Vvj0V}+X~s5Gq)IVu5xSf;i<?bFCs9~d#?`t zr2CpT+(9C{n$~PU97V%}y$}^e4YPPo5n!h-?lKWEK3YIQr~Qtm=z;!A`*%DNTJ7DJ zsk=9&QV)0iY%%C;c2NfrVdM%t?GQ(G(^K(;0m>N`y^gt5rZ-@M@$I_T6KiFx-r!=n zHbF5%w7nRAttw4ETg@sGmNBD7ulj5iJoHj+g&hRRvI}CDxZLwt@Zd?sNKK#vXbenp z*5bjFE31CxJ6{|CR2;v6N>DYoek5VnDsXC@Cq}LFy1fpx5l{aIqAskhj>o9r+#zfP z?gG)CN(bd1KIlE$&yAeP|M-}ss424Ck1X<;*>^WUc;(BeQjV;3ByY63^xJEB1V8Wy zaoP&0Dvv9kP+?)ExiSCdZCK}0%SA8imszyl-!ru6Hs@zptZ`LUX>RX6!{cNn{hZ1W z`HD-eJ{3LHpl;uNNQ21T9ay#o)7Sfv7s)wR%+BZ`f}rOBmO%`aKGk#`#;?gU<sq=f z)T@__Db>Dp(67EngIv}uYm+ahz4NgGYOP12IqRS-P3^I#e244f@Kj!B&&vxit;UkX z1*G}&M}KnZxH&xGY%v>m1r=L1aqW~X9x{$RVioHG;4diiY?&}wX3Jxnr(rx?bKGwT z2j>7jlk>$sO<?$xMx>-P(Al?<*>LJ}rn6pMweHv`#xtKw$dmthT3_ATH`nbitiWVn z2@~aoCiYyRB92}WQ!P!qF*ginlBB>ze+J0eP^k!?n8rWvoi8%36i=BJiZqYWJ8PBj z*=~CemN558A23@+{p3UJ%9$L9q5zM}I9iD(BmT3rawTf`cX`w3Vhb~V++5Ak_rhN* ziGagFI0og6t4yGIxermtLz(Qen+?qjMrVg)Cy+n88<lIDnHr%XYgIkJPoluMZ_iZZ zmhw_;q9-+{%9#sTwGV#9EH7I%@-PbJ*(n2Kl0(2QE+0_<ufUY)n%d)g-U&t>vbvgv zYHcVd$bMsMY$)0h)NEQ1eIb`ulcl!r^qM5pXyUKaZuZ$t1=PkfUs8SHYL-0rt%Ub) zX5kkuy;}Q^kNi`7fAY_{yRE@meOmQ2<JV<oPYS7Z3YCUh==_WMWw7cmFZv~-Cqt45 zn?H#zaUs~b&Werh`;)m-T{#&me=5-HpD+Sup+|n=KLrI<Wt3^PNrAJm@0i>5XmJuY zP%(#=*dvpK1ydAC9WmK;+3B%Ahweu3!c#NxKckPR`d0Pd&G~Ctq3*V2)SF`LR_3$C zuoq`RQFeG5J1jijMx}*t)EnUUY}3A3IH+NjF*($0WLeu9(kM#gR>Bx(m5JrFew!C_ zyN;K+)S&vShU1TvG4Ii!7K*1auqP$>Um!wJQ)N0ppn^K-R;xvR@ycfZjkWYHatxlC zmS&z>Y04nlm~-Fv*)RK-8n&;Lstp)wg|kn>@_*`AJo7n~WBJd19u6G!id4o86yz6G zTx^@;rs6<psQujy)NT#CP&d|i*6j_0SGh1|Oq4SAR&$#8_66LCh8poBJFYF_pf9n) z_Lg5=9UNZ8Bp^1Y`jUj9x4td#N{6}fHk`h*+J`z~=U-m~W(37h<;|n`RR8><HcwOM z;?8zIlP@od>f3QhufPYO!utCV5~O%h$@adI#q21`cu}aH;luTgYB-;nLG}XVm{2U` z;ILLvy3biEiK8@;zVAw<-6_XYt9HQ-N0Ex`nTp-^!Oa`NR&Q}lF~hGsiY@Q279t%7 zGZR107X{}-Uy8BilR{wqYULehN?b2c^oQoqPc<;v4Yp5a+9e3Gb!?-*AwkO^srMd7 z7um1Op7|U@!^Jj`Cg`)Lsb6()?As@WlL!*Q=VNK);c-M&*OKm)jU$U+XfyP_6)SU6 zY9k`#NHNm6@9g>P_5|Z}i3hY55^xZ<aDc;;?l-!KP~k;mM^=?3rBb1}0}V0~>z<LE zeA7@CpP%RAW>KNxS392~MmV|#Z==<AiyVP9xIQ}C5!{m^IR9AG!)P}g18DRT3ys8) zvZ9zEcSWet*j7d65S&h52(<pAdQTU130(8{5MBW{{(Q~)RbBWeIO{u!;QsvQApP)d zJl1f#T+QH2UkLry)23aB#G(qBpwn)dL8clzm*uS)HXV^$C2WO;kvM^|)xs@${Djv& zxQZ5igJK_(6@oq*9*RP#Fzg8SD6XG%?!f3kwteQarSNpV01guOjm{8&<f~E)k!vi= zupw908WJ^$%}gYfhbx5}xIau(q~aCSh;c9?U}eMB2iYJC_dyn!m|PqToOx?3jIH)? za$*D%|A*i*0YzWaK@xG{E&+);;d*4ttVIF_<1ji5{*;ut1u_P(g#bSQeA}e@6{RbC z2cuB~m1dYv<l7Vhpg$x;*%|5JEsRc>oiZg6!yk(>9m)iVKz%oxcPQ^nd0L4ABgG>_ zHBQ4D8ptQ$!47A$eG`>K05w#+6t$5ikrGl(;FpyTpDaQ%HVys;Nb6Ui-MwePnBqWw zngGxzw<shX&xAsdya|6nEr3M~k<U#h<Um2UM#GuI0F@!F6BqVH$p~fce{#Ca{MZc0 zN|<=&GiwZaI)80tKfs*Su&jTmC?-R_R_KEnUUHH#B&i@cGbd$3(~2!|b{dP53uX5j zUbgCxL~RmWQocJpWZ4r4LoVL^?Otm8`If0odQnY+CaE4y=o=)ca=bcN2HF3$NMTZA zsy~6NcsRm5QQE@JOOufs{e9X(z-DCVBY%v8Nq*$Kb<IYue-zPfNdi$CUyLL2W*0X3 zCvSN|!O&3Oa=lE|nD|l-v_+fxryH|Hq4-q5-#xTp)i~j%l#>{41cHBq0b^1M$bG?V zWa2i=+Y46ZBpA*wXi<@fLZ7aHWyWFj>D@GspeTk;7FPDj$0xU=QrQpK!bm<BSaINs z2oO7m;3RSrQiuZT7%Y+~Ld($=woAnltOX7MhtGU`v{l6Jkhan^Ld(@itVVLj>jkU& z0m(Xr$(Nzb@}dIGg|Zqr8UtHoDU6|L!EhuJR3304HV{`0jmC~7BqKN?hJ}{KCO9!0 z%qSN_z$Hf*Jf*A8Rr~-}2(A?3!4PCZ$_1gwN9COP)3`Y*GaK1UNwMH%+7KLH7k5wt zi}pX@zc?rX_>fqsGfGnePt*0JOi2l^VCdN-#~BW2s@l~8neV9_RXv&3LOjHHKX}^c zjZwH{F`1roq;a3kK*@}hwdU)f<jjp<2LiNtqb%lyvsJ4)@DU<7y3y=3y4g$Uoyj|O zsm<liFgLz!t|9}}y%jR8b3!z_vWCz<<t*LQOlxdUezbQp<5g2?X_68tTNlxZ)&V!Q zHpw#|2}n8{O_@O-fEt&gQ!Y{uDW<7yr-4<Qp3WnvN~$WoA8FE^qnrr0z8i0qK!+M} zP--*v@d&81;=~cyIGV)eIC(*BKWjAXgoCUS$;Tq|B4haE)nyycQLME3q^-WWM6-X{ z_|Uh`nol&jNorO+P0D=P@s5#Snfw+TfMr<=W^JlBx2LLs|EgetAMP{ue~+K9lvaGG zCFyu4K%LXHHEA@SlQ~5*EELy0y_w+HD*CoJQev5^I`PwIr>{Mkzivg$-sepOO>}tn zY?)_v<hMN5{4wuXpZWe4zxt!=L|+Cn5(+-IKT#C4p@0?}06=YrxF?>Fa%t#HB#h-M zP%Z#;M7AiDz8hTQc15AEdJbI4(fp1zaOjXa!u^kr;)Xay1H2E3d%g$@o`+LAp&C80 z+hHNfT8azeUTPzAyijI>xenq+7Aem$Z`s6P_bV+*180xsG=e_J>3vX$N+0LlTnf;m zWAb|qGMd}d3tE<7@Nya{3|DuZ9D_9$wBSP^QCcjQqRA4Uv5HDl>~dQ@hEJ(3_Vm4t zHd!UhduscV9`HrKcM^3NKjIoj!dU}WM_F!vz<WKW?tkm{I8t>*QaVZKdVHw$FP%Uw z*28#b=#n?iy1=)o*pZ`@Z573<_iy3q@Xgn@8j*nCd7XhxZPhO)aJ;L1oOWiuzIsTa z6zL<+Gc+su)00=F?l|)ApOUSOcEp>v>&mO8FrZlOQsiOS%MpXes!nTcTHjA2$)qv= zkFv9Vi|YHM{m|VtAl=Q-3?VQy4BaW+Al)FH1Jd2y(hbsG(y26(3J3xsT>N~W=iZ<1 z`4iT&_Bm(o{a&y2UA;x~zy2LYIZ@i;m&|?y#*$77r+01C`#H!a*}f0QfdSk^)N3ok zcRB|dX&aip4_2KO{F7nE?c#!iYR}B(=Yd*y<<f@XvgBc=j~xaTOB#`~1B3%zO@8E^ zU?r2}<kA^G*5;LW-WO~;EEq|7USB1Hwzy&Sf*<srKPk~XU2jR==R{1=&yvTl#SUFR z?^A!udyvU#y)_>>>)3nJblWN(=^!#&+u{;@X1^47*^X{8Cr{F=5o67(tM-4;6eniB zx-c;_saPs&uQp;;F{^RExlYhZHBR3sT|$u)I{dOSrt!qvUIRS+<e=X=)!P_lcbo5f zoLBQyQBm5S@ZpZ-q_>SfOZG7F_(F~xyP=D8E16^rrWkOi#6Qx=hcWmcUzGeUpKuV} zs1Km-<U_5Ez^rHXu}in-4S|ZPWq~?trLCrj@G=WYHDO9#g)Tv<XM(yd=1nR&Z7C$$ zcO_oo)Ul7J+KsoU+;+<nIpi7~*YzHG579HTMWOngGum3Q4l%u8RqhsEHJpbdpMH$J zCv~ljKMzAI1>7I>A`v(YlPnRuUQF?E(&?b;LY|I28NbHhziv?VEOp+T*BkPX7*u_o zYNGERQ(T#u<mMNR;%JQrLi1N0MDb)?LCewgdggWd`t8b`On&`&Ui%`J$TD>P77myh zfx`Ki-V{>tls5MXw|}YaBonf-a+r<%@^do{ybfkAMwjSCKa@}L=+)~ou9o3zC;Wlx z`s}vO^vqUsaKIf|G#cfCql5mfF9BIcVJ8V|s^WL_VHV2(BPFJ!^27Os{+4w7{23U8 zIt)8WNG>8i(;odK`i{51PaK_l4>At+K#^$2*!j#*^vpt&2p_z}heapZg;DZkl2i&D zME}B|f}riejE@xEo8TVaf%EIS=L&+Gj__;OKnQP4BY0jUz9Z_iZrJIgUOObxgJ(O0 zftJ`vZjfxxvS@U7$%hHWbx%Ke`I?YB9!c#sAEVq}Wj4uN44dIrcpsxu-S7fyksj4! zVOQg$Mq%ZcY`a<t(iQK@n`Pvx%(VhFaS;Q&X!Y--L9JufVzoQdW1dI%*DN_M+dLts z@r%4XiAn~n+`h@3u3hAw8|q9Setu2h+<0e)@!{M1Hj|qK{X2cnUJ+sZ>hOR1aETcS z`vI&;YkNNwQ0k3OcYz#Rz90Ena!0p~=btDpEoxY?x0+yQcAfPiF3ow;S4-J5C?R%C zA9f7IqHa+LxiL<VYnPr4O<gNcxg2n#U*6pEL#7Nw`d1F8`vBf_^{E);?Gy<E#MTdK zIqzF*JejyYCrtzrTp6%_VKSFI6|RPey?$(#KIsLXI!|cdj+LdpR;+r>4?&iRD_utC zM`zqt9XcOH_w+D4yEhTh_Qbr7<#OE`v6jzB-%*u{)nd`hpkErs1U`AS6>oMg1KZSJ zo~wLAYxa;uCfOH3d`fVJI5G<Dj@c6?b@7TXPH(q2d5yz>DiRhb6IlTB<R6HuAjFhV z#IR5;I7wz>C&*ij0~;O>LT*y{fBHa0EMN6eT5K4nB{F3cm^~)FcG7ds|E`>%iRqhg z2!wC=LN+pq`_)2f>Pydf!$P!P!q+^n*L&NOshC7(lHbhHM5x!`rPCEHgrg%NN2h6n zB=0gF+zT(2T$SIJwSQXV>iits%v5_!oV|a1^FfOsm;Yqer`uU=-myD$ykt5$My#;h zyuvai2~W$?)Yeo#qrjLVE221|Iei<Dwpysbe8vJ{GamBBA3>E<dwJ@2<eiJuRDeAR zZcu0-%B^JkE3=dSMo@1T^F|rzK56ao&i`Wmp2D8CkEaMV^=#vj6e(n&D5|2e)*VDt zo)eyg8P*8oM1=WC-XjGUlHe}!<-{VWFlxt7mW3s0v0YRT#*-^ebvzrFsmJ`&#})ib zI5gNF>w7!G4J0IM=o|V?*`YU48$$wo=w?L8%}$;IeuZoqOmR}C0+*fJm@GvnGFq;c zsdP9&`qt2F4QtnB_TH$Cl!$~lL8n^d?9hP%jeQnh79L;fsC>>2<*t~S7UC;xIcMjw zrP5d$GpmBj&4o<L;+78vYZ_nHh4Jv1w?bu&i=taLebZg*n(;>lyEJbndDlx_FS)cA z=i;rA?0VmWxNa-)E-;8A|NglnT_?2;IP^~ETW~LlDQv@+PZl#ic~p?k2M_+tW|G}S zL<=KFZe$0`YT9>V2B!Pq`I)I%amiF)5AIA8YeE|uYUzYf<q_Q(>hi0lPrq>3P3=kg zG<l^??(qQWM%mj5r(Y=-{_&Lm?N4(BEUse<{jm<I>x8=HCWr%)hnMcQr%rV8O64yT zs69*%A#52X0`4C!N19FWxHGcbwy*SlU0{_iM=H46imE$@9(=g9Y1IS3RhF3ps7LQT z{)jzoTzPh@W&+TFil{Y2Jtf?A+cBFJ-Q+gJ=6PeHE?J|uTnu+$cXj4ZhW0!Y_up`D zW+K1Ss#L+;@)c9W`oj{W%5@MB-KvQiv}O0Qw5FA{&)AYs=@Ce28B3Re)uP(!Ny?)s zxpwT=J{RaZnej2Z!<$IY@MOWRZX9E0<VmJ`0SxZ+cAMJGO=CLie>942Mt)<m8kHv8 zS<5B<Aiinl=Xou_{rZt}43|~wdJVdii(2kUEnljcGIn|~xrp<8DgH95@J{fbf0jio z(IbP;X2h)BoW=8&)(b;2CBkUDo6PgY+V12%3Mf2(BJZ_iqJk8Z$?<|mTIfmbwBQi| z;ikFd;Svj#1sUr(zmXvs7k}S)=?xYVXShx>a3cWxKgxFkf^o?KXsG(|X9&S&+Jsu| z^4Bj+yAHXG+u4a}aHX0>$BIx1h%CkKpidbr)q!7!2ns5ZHP;!54Qgn}3;qL6AhP1t zEaPP*o8d5NI!c)-r;yOD*jE`G-7B+_F5)&+%a%1uey%y=Dt4MuON|>>tg9o-cD2gp zAg9x>s(e&$YpCKmDDcxPO=6*aq_*{_JK7V}1?N(1|2k$M%kL0#NU#2#mN)WVO-Fm} zs~P`l1YCgaF9U4F7(8TOBo-9w|ISw*We9nEC2nTkS?>D~O*Dw^H~_MxV=AAxr59h> z{|h>J#pY?zeQ1i9$S_}j*gO{glPo-t^$qYvERz!}3PCaAGXOB%xZ5gZCIP&bNeA>N zwv2<8$T8B-2O9aLse$l-hP@vgrV?cI@H9d#)r~p*O;M+$#+*Gm`ehjY?)rfov7MN> z_!3)KVOTNPh8r<iu{fm)Y3K7e`*l~7g33(}ty3|N30Ymd&c!txWO~l^dHh>bFht=5 z^tGJPAj`OF*+0MX9|PauZsEzeLc4`cvN^Jr^aS(reC@LK)wh<3Ov(_@lWES4W!9ZV z34htF#R2!@<&)ob^WJ3DWnRgL6Mi84ktsc((=dKOVwzc6(lN7-F%bBy-n#kEKlgHo zJdS{~MdN0X$B}Ow09MuN8TJhlVeuK>JxRzt7JhJeZNplE@-Os(iV;k(4rc=+jTsec zczGyD$eez8ogglB)tEmV4^FAbL%)ucRL%}0R74#nc5p_?S27G)WG5$t2OBpEk=8n= z4~ym9#L<Pt9rBD88@f$-@=49%u3KOY8*)EdmL!hj%fA0mrE0M>rWyM^lhg@IEj#u6 zti#qWiAbUj;#7P3>0P~%$va}R5Mra1?(M@kC#9-_zk@UD`m~%IMy3_FVrCLyoW;GN z*WH#rv1W!+Ir)8Qsxmt7T0ue$qpI98TgZ!P=iUgnWD%tB!&vk~S*Eav0ds9`&aa=R zvKwd9Y9jv{R8;&duz9(9_Pp}x`!C=0L-L8!5&bCN_1mpeRNQjlpNfwawkxhgjf=-q znj;s6T0E$QqnA-|Kov8MCs*}0Q#BAK^0W{tl#(90Cl)LjW0g7_jter)WQ6BX&BUZ7 zeTy3MQF=-G+LVDW7Ou;?@slX34D}@s;=#7jg^-@=Vc=fedV4p1Hqe?*zi!m{PI@wg zx+U2V^D__h$U-Y|nhJa9E(*X{*uH1u>Ia<-DymQrV2*QDb3+z>>-l@;!dbm-RV0tP zQMwd5raKmO%NWRESJU3I?XuPP4W7T?omQ>E97AsHTEe|<r4|N9ZOs*NhDN8DO10{3 z34e&NTeU%|9B)YR&nSA_D}l`9V%%TMujG-RL8<ZDWqiD$GZY*;cly79qJMtO{PWMF zTrH0&`q_FT%ZKgLvs<ChQhWs_Ng9`ksxf65CnmI@pL?AiEWzgvsi<1f#?*XEZDrQ1 zmE?#zrinb@OnP*)eVWR+y+L%bh%kx7ED%0G9|EVI5@KqagbU~i>zC$s#l8QDokBjx zaG|FM#*9K9Q<UZ&_`@qq88P&+oR7OWL9JWzNYda}vBO5wrws0`ydL1l&u2k`WTEPn z<F3MFV60?-ut}#`U)rRqaogJ7&iBO6z(hicGuVd7c^!*j_qw9*P|G_AgdxEJd`3Qe zwtc6n@@r&IKaxRQM_r@#Jaa8_1bD6Qx_d>MqerGl7GcwvT0>Tzk=O6BX~+Dq(ae8* zWDf44AOlJ1&T?xGHQzM1kz`d!MEv=u&n3iCz_D_F?H))>khP{H4$=F*qG&N?x85|> zW^`m|;3hH#Kk`}VA__ryidY~s2^~f~Z)p)eOdVRHR|)5aPns7Sy^c$!1(5wRC>9K0 z*r2NwhS0|~ZBnNNGGPWcp?<{@Mkq>09!{1C6Vv*+IJ!uRKObVUQawRpZI@neY=Cl- zAvJd)I^EXmpDZ^ACH<i)^Q(<2dv?<;9=e)uI%PoknY46T>SxHY{;;`o{CK!w!KnNT z&-h3?NBK9movc$Kv-@Uc1sl#Ot`1yw5q?~+UF~gOO79X3O1Bzw&DrgWNZA7sVk)OT z6$I~i0i@W9xct8j>0VzOJjG>CLHri!v}^={sei~C%CL|;yD*A&EU-larncy#egEn6 zTgT`%A9vJ1uZb;_)AwU*<Wq~x(yXp~e_m~$RSjGDUh|=gKfs);*~oLX>%htZLEx-b zCI-?%X1=87U8Hk`|3z%mlQbl7^WJXJT3(`Y9vPE%h!G+CQwl>{Dr*UIz>ofu=Ygd^ z$0TO4kWij7uX2*itJL1mS_PxFzH&z!llPprU6T|*y!z(5Zmf=Pcy6Tb?ZBjwifmku zP3A9<!_CYYS{Zpts1bsUxmyV^g#)_9_F6~&T?Ju`VSHLnq~xuEWn%p$wpTGe=`~%l zubIT;iQ3QG^1pkZ_zd52_+QH4SG1u7p_*AmrJTf;t6yJcPAQ!_GH^N8m)_^P45pSA z0i@VyqSIarQ##8A_VHim55~=TXzPXf|LM~p`Aax(XNXGFDle;#7oP=_Bl5JEEEaBX zDc{L&_CbW0rqq>JP}(^y&rYvcy(R<Anu-6Gt`dW%wF{EU9=%RDK`3-IC|yLaa46h6 zIhl^a2RTVcls<<%3KPj2C}U=qAo78i7U-*HiZ!kWyNW*@GRhGihwxdbMcu!=nT9ym zalO(FGAAH=D4G#i5R1Gj?TU$ClG3LvTgGoxZ<5ik1H!mT<K8EoSH;(`kFg8J$}-+= zoFrZtQH1?n*bqIlYpf`fEg1WV{pJc@u7NX|@dMAt-d-(M8rC=DNk6Fsz@079(a&H{ zEvaJu__{aFZ6#WgTE@TJ$Dr{x(n!7@{EA_DWoo|3U+xTNOUTDLN@<*06+LP`@}B(H zZ{Dr&OW36ln-Va%^Sd}YJUQjSr6`&~;)xR)ZIIaj=H_mSb1E)BveDm6FUmcYI&UjV z@J(fv{({O!Fv(zc3K|B4r|gm2jZqyg%?E{dB$)S_N1U)b0UgJ_Flxt4RK7!Vk3MBi zI2+;UA7EtmA1UR>p^zpUmOBFAn7SdAnA@r4dt8w@EZ>M&5GkQfA*p4eR!T|#gre;> zr4p2a+DObCrNJpBu9!Rtgx(4~>t7xXlO}j1CfULQypovj4kjfjlN|h6vxMI@Y}9FS z&V&_8Ezwq8i#`bz;7F<a+iUaYO`hd|%+bC#lkAt3cBRLxou#2$oj6;F6GozpK=BE2 zD&ds)=<fAov4dsxSqqV9*w{T2s{Nv!vi|v}T)9-(7U1`8$K#_+7IT`ZY2~1nsJ!`i z38gb3JIeAaG(j@$Xtg=Pudzy&j6U@WYbaUCHdIq+ql8eYGsK>5=J}4%N8pa?=-&AN zBOdu&K*=;O;vqZ3d^R!dFD^OL9N0}mdY9C~B|FomKB`uw){nGW`L^NeUTJPsW>m9S zXkv4t$+(hSNTC{X?Ks9EoVZ4&!hiu+LUkcLB_lp&sUt514Wox-tXhy5pgJBGK8K8n z#@Y>X+@;`bhCqUpL;~xpT*|59$)14Ksy%l3<Qyc+uc|V~6WUL)f2(blX>#VbgTDn+ zksKe12)k+V5Tv|bohLn4uC46!Na`)CqDWiZ58Dk}46-?FY~W>uQCq_?A0`itZS(&( zA08^>DD1kkxK3zty}#Gm&J$Nay`q*=Po%0iotGkAye=QF$O7gsrBAcpwdA2|0hk&& z$`HjPhGYtIVlRME4sU24X_F>m^lwrLqrbADDdwvv-M~db6qrcZ%luM#bKEg5g=<B@ z9=;5!G|B3+K3<103>gYC710PSNVbfq%WdO$uXu*hxa}{)Z?LDLY!Wh3wAk@s#j<d< zo)rBUjywI(nQ2c(X++tFB|EcP*bFh|eF6|2fP+W_hNcz<)030WOpA*4!{BITfxuRs zxQ^C!OR&x%-OZcHo+hs`Yo0&L;uz@+ggiCbWigu4{Op!93-CTxcna&D$_oB<vzyf^ zlTH|VmIGN~8XEj$k>4gsEsI;Hi1kG1|M};GSgEk5#3t4>sR>F=TK(MZ!}fH^w^n;A zkA-gj9%dWXB;foZ9S2LddqWK_G%<MmT9*TbR^AWJgkVCd<CHqJNF`bSm~z^DuJ@Pq zWcZ(!SLP8;SfN@olLGjKJXWFgEJqhPCq85A)5L+{p3n&wmc@KSc4Xf&rMF#MwOjJY zxf;4(^%-!sEGoUfkg>i?EHyT?FhSbx)E=Z*B3o8khXx};MUDa>>C(BN;FXx!GJy7z zkQA@74)io-h8KM<U5$QMzTf)3^(mpA!3OM&)(f#1M(w^>TD%yNd8<w`a8RPkGDRkI z=+|HKJzV94XbFGfbPVKd7<oqSe2gL9chUAi^VS;UZAVkPP^F537p|#>Cvsr>_a74f z^vMy)6%GJwt`nYWtkiM~7SZCNA>CI#1kaago_%q85zf<Z7I)TaZK~!wC_BMkQd;zR zIVlJjBUByX{OTW+wmHCC0L}T6Y3k$MP(;o#SW>rnf%EIxV`W{A1)=)D2+bpr1{Umj zOa5gvq-wU<SrTYg)c^jK`b0aVet1-9HYb3s1>dP;-j(U`hA4GJUWUE~DW;4y{pC8l zO2HgYi5pU=2Zea>=C2#b=UswtHbw?W6n&&ulkKTod7mX2Q#7&Z`xW%H)%Jp!L^UNi zSS5WO$R|rSIF3Kr6l<z~S9G47OVG&x)p{Zz?Xs?jIiZbG1<!0`mKZD|dUJQIl<Kr0 zlq=THm<r#K{7uLgHfD2+E0XTao?G<h{%^kbvv8`g4`4x`kd*4-3=7h}(lO!l2;@pP zp7NbNdY&Tf#>!$bRn&T9Shjq~aubR{5(fQc1<=1pL|P3MQ*R8u;YWTpw!~4<_sDMp ze=V<>b!+}$iI6>Pt+A<P()mYdZ6-q3+`FLnY}@)ZulUVLQgfcZl>Dz4%6b^8g4w<A zd8dwX?m{Zt#MpDPdJve+XQ$%y?JN_O!dL>DgX<foQDjR;V{;C{y5tTPQo;{!;-}bp z)A`4EZld5iT)N*Mz&CCzB7Vlp@}j+<7}NVH-A0iyRWzO|2m?LKR28nXyGiQjs%(}w za<qrnPrbZphk+%*hT4a}ZqJn%d=~T?IS&;Lw?7Ux^b)|fv$O7<L)8_Zb<*ZKFgQ~B z|M}+_;vsC6IMJ|$WAXLLp44bqM(g4&Mih0Gq&L|ihM}|7H>*#?@1IyEgSor_)CjrA znw-FvQ%-GE!BVt3H|kO%2Q&BATY>jpJ3?`-5qF}lgXU8B526LfKjEZOXw+h{rW|r0 z2(j4IBD~~x2*WWScX>0R-$Fa}-9*kDIWh_q%?>?eYhHVorR1O9@>P8|9-b!z-6*By zvEA)~9Xf>zk``Xka}3WE2$*_8Q(q_7dwptX6-U0kEupq%X9>`7)k~!a?v}42P&sxz zN^JL~m*?bP%4OYby)UUI>`uL)(n2ejUS-|SYNT}NtVKG*+we+AL|2~nBN}p?AmG;F zL-v|}=W!HZ${4(|fg$T=2-7}n#oF#=@y`B_KlW4gN!Y4k3bd8H2+H(jb`Srow#8{4 zis|^i1<f1`H$qcw%d%<7&clr`pyXS_zVyR+4@C(zG<bMIl7OFUBcqfZIGh1__A+hQ z9jY=oqu&fDj3YKGI^_m1i39%fM3$Zp43CCnZ^_pC{4TIhcAGPgLRJ_r7sN$`K`2my zdpzLogCOj<!zFl?-dt$(J5FP22~`*fp~*?FvB$gLpH*f2-52GEb8%_)hAJ&O3dk|b zTg|(Z275G%%$~T=Tq1<X!hYBfN^jwHQz#<2KD%X5FSEid(umWQDHy?nqi!N<MWJ1u zWVN-8cTp1IYcuF*3s5~HZFU8Sk=`0H4ln(aK!%O4NaD2w!#l;X)LN42!(&F)JPvgF zr+@w#lI9ckOdO)zlC88P)%ZK3SE+5FLnsy^jxJ0fLT^=Gx5^fYRMScoSI#)`Rl=%3 zwcu@_-7cFR&j)THL2~({l8XzP0yd^ca6DSEmsP1Iul<~pR_z7g1Yze{xo|=8R}3_g zF>WW7GZ8an96B<sj}y#*V+O*P4u%gAB8Ovm<Iu>&6}wysJJOUUz9m2>zIyv8|30Xg z&4TJwCKH>mRORIx_iglb+x8@#1$$(+(4!K;&>c*XZ?E;O@bVbhMF<P;;4YdUk3&f! z{X1mKk8e0koBZ%HR8d>j3}roIJbn4?v^vDm*uV>R%n<9h%y!Y_@-et?k#OS?4x=Sm z;PxQVs7ucGAEX;#-j*tH+HcjhHLwVH>X`${{^>I!_9WmN>`yREM5QEySJf=ePw0;| z@diISDD8)SjlVHzvIlc?QZ}s#8FJ*5*2D(=+jxGCk(g*GdN<$y;u+QV8cev&Pp(v@ zpdhqF3|oJ{Ea4b}<~$5(MvM(TI{Ikc2_UFH5f!-0z=SVQ9;=X;#j4;s>|5}DR&Uww z#OVIe&dLT#Q+cEdz4&sJFhxKVCp)oIky;I$G@T<oEr-8*pY>iPJWekga9{_51nw_a zoZyu;nLxyC@nOqYkwY~?uAflHlz@)kcioT2N?T-jHkrS1FZO85K)GVOQj+JBe=r&v zd~&R|5RWsJ_7<61_#hZqdK7r|ykS)KE}hyeGjcu3+PZ`}Gqa|cHQb^^WgIUabCkI7 zpFZm%n6K|=f2`@mZZ?^wM_GJ&6w9K}PP|I+aMb_HH-!VP<%P;=gy&6kJHOgBHVkJ( zQo_V07-LQ-jeiFa3WV~?<BFVB+F2B%D&nV>8MVcDRApm$sQAFc=YQC&3FAnXspL@D z?Qs~%WBr(y;hd0QEs1yGxW9U8m|X(<(xHZUTA?nvOj0ezVLn4qii_CG>XD1VxK!OD zY5ObE%_~|4lJj<Ng@9nZt|-S<UH@nJCUsmrgCXgcfB;b^JE5bIM1nj^`j?c&G-c^# zYXgGl;ZTBc99Am5C-&%+WBuaE;!O|AAck@EokAR1x%tYgBee@qGDj8ddrSg}FUwLW z3Uc}?cHD>pbUa4$mvHh4G$ca*@e}SK0m4qELj=Q2)Y6Jp0klMKK#mb5F5lw?ob5P+ z`H%217XsQXq(Gp4vwa4h@Gs{{#DAmd!MvDx+OzQbq`@Mm3AU!FqI;zSaCspq9IWIy z$u&}%>=o|^y(^T=T#8PGe1D|N=AD-dT`nh@pLVMQQ@J<DMZz!f4c6?N+d2(etX0Y} z575<qD!CdKx;^${LI)W*%bc4AC+P(A3lA^!2=UFmM$vQ$1Z7O~gfBnoWS6_^)&PH< zKxV<pc-D#aV5PGg_$ybFv<uOOZ&6`dO#?LgdJrny@J)eftLs<+v>K{=6;ghBPEG<f z#dN})kFt_cr+cPHjA$0?S=mFt-zHvuHjX_fVMpmXAWLb*6OUizdO9whtt0>Z^CH_K zY}!Bd+Th88%ztbYCV9jV4K)}M(lxY?m~Uu1Cto|s3#us9=5K=o3cp8akOk533W^}0 zrVu5=lTu40#x9y$HE^3tS^0x?321Pr8OVVkz%h9jw9o)&_np{8Q|Q<}ov^FX(NR=$ zU#YFfvaa2oXZde$UE(2{g*<lmx~j!fk0*y+TuG$<4QSysh-AQ-Jp8W_H(VPs&sSlL zLDWx?=DqbWLzT#Cl*Bt6;R{duRXaYYLU^^8p#eF;lM;E287L4)g@Q;@NL?TbB^c&y z^2v3Xyy+)aVT8AmI8Ro`mrh|A+ejUl3+`F+q(DQJ#@9wnDPfSbeIzO&*Uq>!ZzpDk z?Y_a;j}PL?>b>Hv=g%IPlYQa(r_XO$sIVzubM1aou35XbTsHDYL(3zfoZO6X)rkFS zve3_HAriJrLPBOG<Yhn!=|JvqC_s9EJQa&ms%2o9BDnwjA`=C`5+cN(=@`-pBKRC$ zhz}14LvZotNyOQQ$E8OM_6<%%%QCZLw-+VE(d({=MaQ$cR3=o|)5S*UHNXjOR?j@- zK+N780%(8G;SlzhSW0Gq^@N6}#pHHj*KlgrQZ$X`MH&=IA{l&PM#2NZ!NP7>;j+4Z zKIF=p?&d(`DaoO2(?l|>GPbF_R>7$pS?c<Ri?zIaU46}MR{pnNQ^&RFGMu!SX6SDX zPRcL``dqlV^Tz8+)1vJ1tLXQ<h<96zB>9-6EGdJHFu7F_A5g?Mck7M3|C>+vsa-0p zCx4f}Ri|{G(Z>NVuW-x=|EwSrqZ5&_ca3XC>Gn1g$6c?zHy`md-kO}OeKh>~^+#fg zv`*eqSyo9-nin$kKnr=;JQ`EVj8CrE6Oi_E#tP30vW1LLf5qa{a=TZ^QD{V^vCeYS zlp6L#B`N;IjgS#MoVaw1mxxGEAEmF6>1>-xhEL<x<28*4kBP;tj6kkC%qs{7L_`Nl zBeY=XO=$~<Nqnmx-V^ZsRvPueuh;9?^?51(sb*n#(+*1sw+#Rf&u=CvQBSIe>xSfP zdCL0Ee1e~q<SO88*<TrwZL`c%{Ph4@s*+UwjMdHruZQW+FE2*VKW?LVzY4m(ylg9A zVjd}0p3q&_@NC<#+5Adkv9u@uFaK2w@epv_xg-^3tpj>14)@Q$MOwmRZLM~A$0sUp zVl+4j`SHH34N-9ci-sHQ3mP6*V3baXBGQ<g^crh)v#jR=iSX$zdyX+#zxcoQTdr2T zcE`3$cl0C<=LSSzU<qEBuN!qf#b7SU3bjEBhv*1YczBQywg4TaLx)4YY(p_W<+b1? zsCj4-$MATS6S5V82N{tNfb&MCpv~&lHMn327RE%rX151pQ4wy7Q-wS>^^R?}-;6`p zx}6bkJx_&QE~1gRm-s!IwtC&$f1X~yQ~cUHrB=>`^3I!*#_lNIB-4Ah?3M!$9`BZX zOGjKRaIY8Fqh?3Bv-rb@kE%8Pr^v6F%!{-rWEp!BE9lIf`_$g0L-C?b5i7^P_0<Z| z5_X~9U!2@ul23!HR3;WmH1B*AALdjdaeO{5yA*N#YZp1rrjH_v80xX$o7gF0*Y`%v z^g4Q`iImBgB4upo7rIUEZ-HYRu^;vi<r;Bw?6-ow#G^zurg42~D}u>?k+5WixKYuR zU`Fx+?4k$@A~8GDh3>D7lR_dqC%|Dk$)pgN<Po71>CTbEHQXsnz7W|BaEE!)F(Xc< zy~qT*TQ~W=;LXCOaW7HD<;{xch5FY7y{tez0;2$y)hy~_+!_3-;aqkec&<!PL=P=r z`*+7@#oV|g7NzfhcnwH2Q6~O6qnVDPW7#k(xj)GhA!3LrH=%dR9}AZMtDmyAwo#di zZy98;aq;FidvcMoa@j)(?{^tir23~%pVT>z2jEC=8~I>YC>^3!cRbH{K_h5O5-{_K zp0}RR{gd2xhE&8v=hyK4L3$`d@~vSM;$o0lc8xD<4}?3tipci({d;A%#hY%hNUepy zciq3HJ(t~nPwxyKKk9`;XB4BurzPa4P_Pg|xPGP(>$qy?pR(o+sG>-WnZ6BL4e;5f zmWWZNp(S~k#IXptNCN~N(QwqOTl@SDKOGr|n~deW%>Pn)va19Mwtq`sxma5&KJbj! z^4fvhkrs%ZXIyk#d97;k(7aMyTF5cz#h#H7n$r)vpCeG<Cxfy$eu>bOG)0z{UmmSk zu6X5+Xlm(mYAx>NdUVsvqdS-)HI+*2;{zQ%s(}7H$@wJ~FE6ckMKMf<N1Igt?N0~5 zQjx)cjg;Zyavaz?w|s#!s;5w~<tZs^BTsVYbF*}i38R`%a8n$`<OY1O5IolEJcI}J zE$SQYG;S|TKo%>i-){dMWwrW%JJ4UD3MXr@(CwiFBl{7p9+?9A<sqJuiY5UkyO<Of zLHr9Mx?AEFFaIU0PTGLJEm<6XisqhJimqTT^^VzLe(c0TgX~(tqW+w}jKc+LB;)}n zoaRqtI%BMtg+4+9#|V%rC43ZLsP;>6)L&Lyov|-ofFdIRB&JL^UXRA!^QBdHs-*?Y zb2uoyyQ$L!wZAT$nit&tq%%6Nv=;mDPn%>%*kL`p!5-YZKtYXq_`!T-=G00%R$vk2 zqu`5t*%aA(_eNw*1(0zOE^!z4fBm^X<bN@`ENw1MSmE(S#|uqh4$&*5v1g(qn@<%( zTy1>$KYsy(5}A!4@Tj@~xGMCqY+**nnPsI9w}06)83Bo6`XCl^>MpZTnl|)NRqQNJ zR?Cntd8<XVH+657`BXt2)FWXxBnTli&44&aSu-j{ytxnv6x?PO?9w3*kIslBr#t!N zQPjn>A(wL|5@GP00d(no;eP%1%$sd1yCOp_dy`mZ*~>uPROy(&$+B+bN5>kapO!Fv zih@kr>A?3qebGfbK$i=jF9V_MPgC?*SIH8->o;O$gP&jaU!H#47QCwTa#l>3rLS$e z@y1vhM1UWKvWC%xp646!h4x^oKl+L8O#J13lsJmNW5)UyyOYGy2yI3Y!vg-l_4QSf z?%Gj$@7;xV84In!%0pk-OZBt*Mv38@*lVkvrZ7^%X9Jtzq&_=bJod4ThDZ1z_#-`d zGBbEn_?US$>wD2G%28~$eD{FB0lRow>v_6(=Fo#?LW<F3Yg5+jhil7naCJPRJylo) zG7d39#H8&~4$tWO@B4RKl&uvPx3TM;I_B_^RZh<CVuuMgb;@2tr{`U@wbInVg=wMv z(9?V|l{L$s2eI_ihN}(ew7M*zcIAAC>8i;uQHXF6)v>Q+-W;~gz=5c;JBLX-*B}0~ zXV_L5_i?;`wr}k{!Xb@3%!huFqgRKK(7LXU@&zM2YfYOMLwgx*UV$Fh0M>~$K_(uz zeA~D4rJ5eba5V{1v&HSV%|Bn3jhFuEb1rE_rUgC1|ErVM%(a48p>fc%>UaHKnngL% zg1oGxQKBdfizV0x44e%!3mf0g@<mn;i{Z{%3A+lR?jfU00tvA|$%mGPqxq(JLoEAI ziJ?3Qm_rhL)5M|oLr+LB90~dXR6HmFpobb2le_<WQz|$ij@ks*{vebhdP(R)`hf32 zYMF)MdQw4|qECPF+qWZ)`&J6xGv-D84|Fc))4c6>WOiyd=ZmSXMU43QN)j$p14?O0 zo-~th;}>hnhJCV^A37?^-7t7%1Zp|Ia&3Gm$X@06BxAiNZe;Dq2Qdjbd{deFc1Xaf zEB!j(6S{1#pZ!_(>|IPnS$PeU9JhALB3dzCOw8nhQf=Mz*E;Jax2^<ELkEA6|IRla zWVaZFsPFW?1@+l)2GW(pTRVP9Im;5wnRJM?OhN+Frykm;1lcg^97!tvU))6X|IY)c z(c`XQpf;i;Pa%ioWs<>V<v<l*uc_I>&FCINLWdCMaV3XPF=1FzQOrxV(Ro-Sz#gGP z$wo7PHDa~t5F4$0P7!D1Jhh)wtsENjb%hh=MEXMp%Y|j1%DyvAaz9Ooq%bJZwFz)T z6eA<)WF}C&B5_&u#CalId2w7Kc0-RD?8X9V!i0H@m)lF&kyp`H%_Xe9dAZagMU_gm zb2Pvb{Uv_(!||3)>SgozgV=2KM)kMn(UEJ9fBxy>4WQHt+JinxMiXigTA(y;G%0=P zc&)TJ&kEoCCy2H-AQdFG4NtH)a}|CuF&B2v+bir*1o}b?CKtoyEPAbd8Oj~RJs;q) z9FPbY3MU`<Ijgn_#7D&jNC5_m8F*<4-AyPQk|n(VrcRnr^mRz+>y#$zIvT*{%w_|P z7gwS^yEMggcbM%uN2&Cs;RNl=fi{xZ>*>=5)t}dFk?i}iS>y2_M(?6y;lGWg|8;sp z?W}+c58tGWN&R+;JG**x|IqjI&>*6@i>NR;ek@$*LCH{Td_swA5}i*~L?TM``VwwW z{>x$C=jA#D-B=)dDz=BuhRnh>CXy%SY-9Uh8!)OQ+8XZQ2Rk06@G^UW=|(N~B-&i0 z|MsU3QslyXT9^1+MQT&>C=upz^dZqIsALevkF$}leeCdI@TEh)5U1a&hL2?er!nxh zCTi7TiTG-xJ;E>`isg_N9F!J;exL^(Miti6?+C<{gj*Bhham%mv;Z)~{%H`Rbnu!Q zK*Q=MwlZ3@kYNiM#1PxcDeresIg2k%x_)NfoWbNOsJ!>?@aAE&tfF-B6JMTlkVbSJ zJGZJBmTn{yg|*JeV(=(yCfQzl+-&Gk=ciyj$d5SsoAIWO00u^IXtP(5ouU2FzcNi5 z*T=a&FQhmb{Bdky#&ndjBQC0fN{|4h91yvYep*WrKeWgVD8SrOdUa&6`PCg*Y|Z<e zCk>--%}92f{GF!xmg4^XH(rs?h>Ps!U;p{%MGi*E4u7Y&eIp{YCBpjvt+TdaSuNA@ z<5J6Q{?y~+YE?1jz;F$}j2Ao(015;2V^JAM%^9k~J6yA{ad%^FgL7ao1=y7$nQIhx z?6@7!F4Q$xEfOKj7L`2`Xa+EQi(d|eOEkU4ga?U$5D*G?T;NO5CF&csP^#h0x2H`H z6eJc`@X~rTDj0(Z*m7&8_&Gi}C~zoQHrgKYQahY_QfM-0sU&tBZbgw@A6cEps2Gt- ziD|<>b<6C^wG5EsQo9{eQDze60LeZEU&Hf}(qxh9)xhC~xZ7(KRce&kHsCZBeqomb z7kN7q3>EmiJf40jEXVqiTF4#t0OW+MoUH$q@`dC|DKnG~DZ1{Xc9DR7R5g@Qmi;fE ztNA*q0)J<+eZ{NsS^279=ugF;7}*YXZq4I?$1Ru@2u-1ZcOHSgmkuQt?Z=JSd;d$@ z%ugQ~+(}5C%0JIER|oFNt*+2)9$~CRdTMZllD+=%P9>ppaoT&B8_jFWR!`|w0}6SN zF)}c{K8bxr{YF<ZNE#1#5Iq0Bd)utj3sd6*aT5;Hxr8&Dc??nSe<;N?Vgpfwnw-G& z>N3?mxSAxc<Ti>y&J@vGuXt*MxvR;!{o3xAr@5@zG3zQ*d*6Wi1WA@?z5LOc3i}+F z^Rh&3V~*(NX(L<Q=*ktN3Uy8J`x%p4{B?iNy<}(Q+JB$>mwk0kVtQFLG8z-sSxhS~ z#9zsTn@4+wRzekVG7p}YZgp2(6Egq#r$Z<eSxA0Qb;edDnSx0~RfST_B-Sgzp*`q1 z_T;6yxSB;Oatv*>Fgg}zq*#sk4UXu0Rs;YeIJTUMo+$`?WY0b(KzI}<FWZrdOrRqN zM~eK~(aD5nO@&Y_^(AuGF3cP)vz#;ojU^L7J!~Mv5^g}jlJWqt+m3QPLor||$&c*k zZBOI!pU>$ZW`BGr@UA`Gtf(Q21*0W)e-Znm(4L@6msgq<l<QZoRh7#XDV%lQ%b$#8 z-XRMIu#?R^p@o68zIu3Vwf8Ll`sv+mAi&Uf6Y$|>q_?m^kO3nMu*DhC|G9lI_yg4p zEFQyl|NhccmQIyGl6a1y?$Ri~is)0(Nbl^kSj+brT*i2KJfXxyCfu<Zg#_WIl>ho2 zzDxHo(gV&`CocT^O7{0C0<;6Rjkp_Xu!}IQ*o#$+iSZ-nf5P>LvQbA}gD#<GHEDm8 zB=F+oSi+We2M*0rAXWX76NQlvZL?@&>k4hWX6a=@d`8?xk=#aPAbMO<WN0`mG#VFX zO*k!z2Z56Sv9oKuVM)%>nasL#D>(6d^4+SjdfC3=dU<lK`FL4t_1Ep)O4I4E9jV4j zL25VGOx)6^*z8WO8sjJnSOipw6_s$(V<Gpx=dyH5ln}qmc*c)kE|+ZM8}VbknQVOT z)6);|<9%xlG6LSTNw|-2xNX@xwd8bd^C+B;OQ?skvEN6C7Do(JU^R~`7#mNkRN^lM zpI=5!w}#!{jNv&`PZBaf>a474-o=0ZZ$9C>R4OqLaA~r|-^bGZ?Xut{U?uhC-Pzts z8^g?p(TnQkGmN#?bj}-NO~Le=X{2RFI-uT$)nO&~s13K|@QpRVk1V&rQJQ5K<A?wP zCtog49*J!r89p$Ijv5-r$P7mmr9_8@hot_6P@fGdZlt46yYi<e^}esFcKG3Wt?#|l zf+%WQ<D=UMSWi>=Qz4TciS3w8j5C)CdgRhopb}w(>$pw6%8nATsHo1Bb)eX9bf3oe zJx8K}3h2!~zn78({)S-tyo>0_Da#^JpFrCi4l1_D;&Q76C)QP%zO^v={h0h|UuyZS zb^F!%<c&dyPMJSU^S&>E(5$p&M6})^%>jmk^FClbpxnHdEoRrc%V%(K-#~IYk?io} zfB%P4^uho*BOgE;Fp2frn`NuO<kQ~d*U?MF^%XXvb2*ZvRd!A;+UYw_1Rzm{U0Mx3 zFpy9o0!}fL9X%#}emi@LSTa1=KXZ@(FG`*lh#4*!c8>=H1_OU0N5N;Smq@?>nKX!t znRUD?;5bxz06cUmgA{?^^)vS_AyF+@l5n9hNOtoWscB4KQ^lu*?b2z$i+Ep3l~q?? z?<*h764O;{cNK5U+U=gmkijt&NykzX8#Vi!D4Xjy4Jkq}H$S;1F|4+JzP}6*)5mP% z`qaziV$#y>cF2+Zl+4;k>dxn=fy6}Sq5o&V?k6OQ=4Hg5VH4g2!%BRYElXQI7dr4h zbHYQ1l6~Y2q2vZ7yJ<+LQxD}1L-RuUfA8mOn^f=tP5wN<CmP0&JH1YHh;KGqlY+$0 z|BBv6Y>=Y}Y(}C~(*(hz1efsWvM_C6Y{(N*XLEsHfvi~ITBH#6H}b}bn&Y`A9<XwW zu31Z9rvj|Uz*3CbhC{E1b8;?lUNIYWE3Irio^EG8B2o`2v98{)pX1j8=QXTm*@*&v zVg=Rm4W5hzI+l4=p%ze2SUQ{zQt^oGP|Hxoa7)2kzIeaUwDhQI6{T#qQ;6TsfE<#> z9YT(E4&`#gV#b28@x|A0!p^|cn8*@yNdRgyJ^HYEu-~FVT5s$pUOWXJ?(#v?i1J1y z{<ul65(j)F7wNFX74(F+oQE~PDTSu9K4}=vT60&gddhE9@hg69xi_{a$vC!ZzW?VR zYwcg)UvPPA05fI#FCe@aQlJ`h3QbICFd&uG3l<FQBq&`fnszkGP;}ZTHi>V6e-hmT zxa=_cm4js)Q*P`gyPeDkE*Q1kx)zT9=GI>U=9vix;TAg7=*Q3p6pr*<ECy1ENG6vp zl{d1$y1aXF<J+87{vyOTbvm<^SkmmXk?@7FiWH;(3Itp@nx9&P{cuIszq_?gQR{v< z7pDrB*I?QG$~V{g_{mZJ^cb(O6??Vug{@{=s?os5?Q90~^AGK4nug&5n`dnV=<s0! zc{N$mMwErOdDCNq4FARgsm!0Ynyrel(~XMO2!UGz^*o0HuXkWojf6B{B&F>a8nutF z2a142npoM-yjR{q#;nmJWuIE?7E>jz|LH>``I>)cTQ>z%F<|PabO<M$xdc)m(l7@R zkclzzeEuc6_wX0Jks>vLi!>!=kakNe;Hp$Xg*cy*v>g*YOfa@ej_A71a}EF84V-Wg zCfs2|Hh2`HW06V#onE14_PjTLhrpp%d4?3GF8Z*_s!<%BxXjw@$Y;Vy-uf}riO5u~ zQ^aBVD-TZyHlfJmKHPRhv1HQV{LXq3>t#Wktcf!8Z9#v>bpkoFp1FY<2Utj521AF( zNK9T`iUMlTkZcoyGE8_nF@_xZOHj+yr$>r6kmds+Nv<YcVuv3&8W+wHCeD<CoNS** zkPsy+GMa&x2GPb2&JC!8i94XgGKzWJK3QS?!7@O;&LuJ24t^9hl93S^sd9GK#L@Mi zK3s3Z!ROaQYXI^0wXH)xuk_Wmm~Bzm3`K=m8jRRVcGyWEqSVBbO5dodlG3TtNhs4% zA;G1kY;4#vBlBsq<Ck2zd1*?jMS6i~OL>KTizNlM2|Q-x)#3ua6mhkbeUT>e*81Ym z#IE(&RQQdxMWF0j0a@!P{oR~c6b`drTh%+;pCc_WimYN*zD+9!|9OsVz4M*53k7G5 zvcdQLo|rD2+?=9jY_kPADjW-ArH=$!1k80vwnrZEU*?(lM)LjKHgT!yjBJ<QHe^OH zWq?$XjPa~w3N_xS+1e99Qjm0UG==1=^bG>#Z4Hz$#+ZGSL!4+#JZ!v_NktSpZ*)i% z2TQN21GdesSGJEe4EST_9sQ<J5}9qQ`~K-8F9ZYsydKB|@J5XI=<<72A4(Q4{U)cN z7*~{`C0&0FI!J|)gUKg7UE;`Ek%l_u7cAvvF=UStv-`x|%lKOCXs=T~ps&=_``)CP zTYzhpixy6)YKLqSR0?~P=(EN@6El`%=s(uc-Wz|Ia+306Hf<Tae`9%nH_TvX%wu=7 zZbnhgX0@tAu!hlAd_dcIz|qVh5=V4Fx3OMUnql*Bh)AYQ9jnCpg-^qEu?mSyyUEQm z)6pe$lDo)~hrL2*Z&OzLjEeD)$st`y-bzqei*rpYPt<})r-PxUK7HR~cYM>^k92P^ zDT`)5Wq%ZBegy{YT%@@gqmHnOcXT^FwQtu*-xBvrH!w6~r4e(u`gxP^#V)GFInVdM ze7BE~2l$+MP!G5wq_I(vY<wUPmS?3)!pU!%L`68a(%-UoYcz8Cq0Z!(1e(sjS$i|$ z`Q{yGJ}2jA^r87{uVtM@G`QiYN*r}U$f!~cV*-cAc0`A<k*8FPG|IM!YYT0gUW;Ek zem9GfyhX|wh<g!_g4)umS3=sr=dDacxr$^5xU7lpNUb48Z_Mv3K3@ph84D~*{fLA1 zeXJ#MmD|pQKuCwAV9knqP_stJ`LD|KN>gNGd9I$%fEi1&=V{Wg3T=7AQ_FC*_Vp1V zvz5i*<n96-7;Hv5lVjpMVTalc`ii^fTohr4c(hpR*$o7f9v9`eE5&OY^a+@bG$c%u zWM_|BoKRYc9L10RIU`V}5_iC_<6wM;2KeWnD)2P;ymBBD_XKo$YG(#Xx@aTd3y6GY zpd+uWbFzIxLfq5&aXVwBtzxxLjBkC01Y|58FT6Ba=xgEB){(30iDhm}_z{nlW%R@# zV3CKT-id}<mPXrhgFEwnSRVf?)Ic#cx0_5DMA8kn7xF-A<~k`YzwbCnJEBohgHiO` zG{95TYW$rUX^24;%sCsOc@%|>(W=-F5BK3__^ueAS$<+Tg~(o`u#Su{uNO{ibh#Yl zllZ4fv9w*a$`!EIvwodsNl!T2uo=Z%6z~(Rmh%C6;_7wGQlDOFV~xC`aOoUN46-4! zW9zi``$0VS-N!!MJSzb4#S<<u3ySf!p;n8);Z83oo}0ofpjtl0KeLG2=d&d}E#!Ls zpFT?><ga{yK|Mk%S;~-61(ZWe+gZ_FH11HEyPh(;L8-F;${2Y$xj~?ROi^tj`cL1O zkFO0?6<|7{bX0M>oJr&7;u{}Mzn&jp$jDFE58;<%3c2xnjsJ1AQuo~x=#9+2P;p9N zZIc&n5twNBE8ht$U1kg_Qro7gc$C@M*Ap-<QiON$VVq%rq~l#1r|aN`j_{K9H9WPt zz9pJ$u>hNIaKM+>*o+P11uEHv45L(c1Q=r(Nr)L`BqYvmL#px3tCs}kKpsoq*^?b9 zMpeZ!SrrsJJQD6?cOC5l!h`m?XbKz}Gjz2k==`+R%w^d@DRC$gBi{+vM!fpT1%s;P z=9fuK_wbBSt01?nWIkK0ZuE$5T?hZ=<IcsGz*lZVYfoAol_E>iTR!sHrDhoOD|B8{ z^F*j&mp1a=j7N-2h)Tp0b9q2qw6TL9^!PLN^2kMd?(X&YZ!(sYTHWtm%$CX8B-L7P zH_=UKcOk>BoCZ0j92ESpz4D@i1f%gXo{!@PiD}6TL9S#rvgEXK?i%%llCs=VvE`OJ z23Cq<81|yaF~kUD+`D1elZbnHnTK@i>ZtyQ7$bgV*v&aGk8ZF|+nW0nyjWru_Uo+@ zm<T5hzpPgmN$?Gu3I_&j1^l0KX@A}TYveVgt3O1^v0xUPcyuUBmTD)HW&yiWhOe9s zf(+6PERZf|1vgDY;zz(=)(`9~YIteTQ|q-V!BPCQL0(L6<v8_;$0Rqqf>9;E|K?j> zq__UZAKR#uta(Dk`3n4j4y;pLkr|}V$~k2zUcBFNqpTEWOfHReQ4k?OEMr&<Q~M>w zL}CD#PT|qV@cE0|J1flt?uly2Ol6vxY76(>dBN3;0PoZp@ZmlXXBE|_GIWv|dL32A z^F;qpu2N4(=v;_JJo!ROfKIS%=BVNrDnLz1aQxlZ_cHA|m&Jmm4ZZiq$bC|8*pIjf zGSySla)%bDFxl5+0$k=wRrpO8uVsv>gJfjkiw0%Zjbih|um5iDDqn9zVZAFRnQ^a* zeZcQd_+;Fi=WR?kX9Gh@@>1SrtkCGpuXn6M=4JjN(2(b7swW>mHnHr#6toIG0Gt`s zRwc<>#Nh9_wE@ve1*|I5G)t)dJ70a4C;Z26p#+x4dG1go;ryu}vb!4_`d;p09qqMT zdgCKPHnX&c_?-H{bIPRiOGk}Lx+!|NttfTUg3$%{SM_z$fb`@>RYGLx=2Xh6e3|=e zVAUU?PitGH-#?$lbE8b<Yp12TNle;Z=7N-^Q3XX+j{2DjRz0XKn=x(%=8R5qk}}n& zLF2rnd=a}Ud$Q!rnHsHBe7=^xCCvkG=62xPC8m?$#$qz3#c!wGPS=?i|G3aMIs7QI zchTxiYuD2!I0_+=GsGQVI1<jn@6-^Yn`4wc(c*Ix_Ie=fHq*r4q?kC=COnhdXM;`x ztCx)`15>84#z$bhB04=ZPUF#-vx+(bJm2eG48%3qIki=5(#C#&<SD{Gy8ZY5`S~CJ zXVL_92!BvU+&CQwZ8Ti=)UDSXnt{eb_8Vxo$mctvThZ43su^N-(OKPASuev%LF5`| z`z-C)n#1qFG1+oopA?nbvw3!;w2e`Yg19xrfGNu`b$c!%53VsAU)=m|s*&s|L%r*a zDaYh!Wh6#b+x*bnEk3e5PKq2-#(-#8=?9C22jGPL;WSTg^<#szJ=>t(AB#5lfvdJv z!stlz>rRQ`Om&mUQYl)t=AuE@9eF4=33Abn5DrKQf8e=V5TIX_>7~FROEaixm&6qq z5F---i8w3^PLq*#eZU6TYsvn?h|gh8g(bj!XWUmJeZHL--RmWyu7GUQ$kM@)X@ja# zrh53Pmz3HOvW{rGT=|aE-u~0)Qu;qW5|vMEQ>RmHtF@_m!wecKl!H_?-C#vaNe=xr zh&xy-Q-fb&X~3dk^ejz>cOPNUp(!RDSmvP2GkS!I(tr(1NaZBfh*MM97WzM&on=s) zT@;{$TX1)`;99&$a1R#TU5Xbd#fm#YgB5poYm2+PyGsRaDJ@ICo!yz8o&B@PO#Z*m zoV@qmbI*CsYjI9SP+Ch-2#>9DiM~t6FTcS<K~6z0goq&`cGkdx^uSM__36K^E}|NN zw-$Y5`7pc!YrRq|`NH>gPXj)$gy*ys&++D~$?3>y)>Q7&+Wz6MXGlPnIvf{cpoo49 z`g68yYyeI`cWTCbgSL+^MvaZF2^}0uZrNXK*aOUbwCz{Suz9^t&)?Z9GN47`!i|=l zprdfl#uAtMbD?z6^-_&FObi1l7L+GMn%lvy%g+c;3Adss4Al?jop!XO8+<DQ%AUPf zxWmt=|1W;PF8}$Xay=7cm2EN<qwhMA!XIm+Z{&1*Ec7>*l8@arno2WLlm(X%5eORz zLJerr_6u!2?$J?;_f`unSGVYI6VZv8OU1oU56>DyO+IP}l%>AR_*O>jpAH_fRpk{j zDl?8;wQ5im@Jfw8+~$<`)ohi`-IOvhq%bs4v~1E>5?WMVVD9;$k$5Ltdf`JWWtFaU zT<;=C=5)}U-o{Wm1rx&aEX2iTmh0R|K1p1siiYGFCHo!WAf7c|ER;@mi8g=@d9;le zla)H02<`{>eTon0yF+}sz~8tq<vgwTrpBZ?bVxK*R$TB5m)zF2KMGsOrq}IPCy{-S zgmilTCGauPq)OLtL-kZ{mA(S+nij7!>uCr4=WqD|))n1TdhtU(n)%4R#_^pYL2P$J zFWon}X)4Z~8hm6cPC%_PB*(}jX6>IO*3Mq{&0-eM#M4qZAh)buDi;=J_-s+;+o&-g z1_<sEl-{o6lr_bo8zN7ZnOMD}MPJfKDg>f?5_+nbhr2g3At!%~@K`L>E<_6@alk5~ z_-vWTESqjeX27t<%wm82i%KnGicFHJ;d2H-0Ca@GLvr}XJGLZ}JPJ>}lrJ9ORI!Lx z28^PiDXu@;a(EXZRbSycg4Zr;+=5Fs{OAW>s>`Mr_R0N=-UhKfAx7`iOlKa`*{ww_ zejMI590}b=6LH*+@11V2*)k&gZ!|}RP2?vG%tzDbO}CT*L(Z%+hZn~=lLR3`BzlT5 zBMJZAFJJ4&i%tR-zHc5gqi2HT6dDR@@)`?~G@W@au6H<-`N>lC!pzJ5z#CAZEfm+! z+y|Y2_C_L32M*>pWvOr7ZZ#1Xg4lJ6lYje-ve=$)egk!P9rCr~e>4l6pP;NE|1i~7 zc>*Z55nMGo&@xKiq`0YK0v~hHmwh4C)~`?NjFi%{s5I31xzTn0mm0Fa%%@dBah=Y} z`DrO=7O|ZDZ>sD2S9;3xD);W)rndnmaeEMEQ4F1}HG@Xdyq_v<(8j$A6-|j5f!F>z zVcfB`O=)GIs#;Xx=hol14(g#)Y%=`h(jLSq=5b6f<}Yu~Hn-Bzr%hr*zb^;s_Vrfl zo>?FP(Q~axxpLL=<@BpCZHQnmsg_r=Z8)35+yDA=tIX6y2P-zd1CB1AVwtfr!}>di zo*b=Z0?I>GwRf8`#l0*N*L#cb*@%`pSUoB0ZncAyBG{>3G-N9632GMv7%%CgWxKGD zv{F@Ni*R}!66E@vuxDFk&tVvxC*wxly2=KI%_XpY@CQv~wC$5|_BW--rspJtGS+NW zY31*qQ<nD?WhS{Wrr?poVI|R3@v&$M297KGByCMZli^bscCTt`QSEj1gbX)i9c>%T z7fy>FcTAiy$!u!fn!@V{BptLW*3`sC4ER0hyY+oW!v4xA+en8^>Cs{^`VZb%ZpMYf zv>hY}!&4E|<e_M?C6%&9F`(ILCA<%h68a36X6fkjX5Df#2ku#ftrm5B`6bC`7Do1> zRM@&I|K2}xsx6|cfCU=7yo(B!SoE!so(h%<q$O;Htl>u!Us|A(LWIm>Z@;l>Ry}px z;FWHe8(r<V^<0p3&48_JvV*@}LtU&T_v(0UMSOE8Z_iUOn`-PFuX^SUK8s^-v1dtv zW=_mt5xsXXz4e;Wq46zy9oIN(K@NBL9|M@~)0eo>v!Q0>!!B<6wsIW`^wCj^?bmUQ zCoP|6yyg_#S&ee^moMM58K@Tjz-tA;N`6e5e5~Lsd=09|6;CeeHObLh>`Fc^#sr@a zoTA+<i@fuIFitVOA8Rg=rKz;CQld>55`*t?%E5OFD~7-)95QM9?ih$DtahH-363U{ zP0F}~4q3Q19{z4RQ$f1ddfsKMl%sK_JXOo=ac}<f_p*Zj^Jmghkuy>e$lFV}?FK^d zz_JcQj=VuyhE))}(>~&6e5S?IOFz$2kLO}gorj|&;H5=Qnqkc&%qJ0f$eflSzmV|G z;cuFs34MSUtDt)^BHu^i-UR?>2aD(sYobIo2$?k82dQP`Y=&%G3$(DnWU{Q<->~cU zJ>7AW_~nWVIEm?Kyv=A;krrEOQ)*PxM&)qma`}cO;OWkwHM1mJXmctKwYdUY{f%ze zzUP`zkv~c{f^NNzSe3HENB-<-1zkB)3$PTKd2KoDoI{i~d00qMtBFH@5hHBrB@gGW zRbmrpoaxj~*z@3~lU+rxDzxvJO<`4Cm+C^A(?2LgJq@;6yRo%`k#Eh+Z2Hwe{pq)9 z&maHA58FTgD*6PCgs+`1k+3j+ik|Y&ATxbheS%`Gy%jCN5ArF0e4N>bNsN2Xzxugs zo=0@e#5%q{ib^~znIPY2F5D$IHLg)d_s-?PJ^?B{jHb=~rqLP~{p35&Q~`2=suI!E zaUZLaV_&V>LCr7j0lguv{MN)8F2LLmLbSi*Gvp>hj~A^f)6Z{~h}uS&TD!v}@vfkW z5JeX^y}F`~%F;k+>ZLM&!c=-3mm6q6uSQQwe#A=GWY2(ne3;3rlnM1-e$rRKat8Qj z=2(oexbje06Q8E+)fS{$g|Y(AB@xJ5sw`N6y%czK6#a=O?^=(ZtZ8Eca-rCeV-<0s za;Qq80g*y8r{mO5NglAU(raSA@3PYsX3PGseuG`+R`l?N51Fg02ToO;9&dciM`>#w z^($wm3eIcOL}cRD@j?Z^f*h^VW_j+XHrnxDb@5OLPvc050yDC`y-KOba&N6pF@;tp zD@uHGZ&*$-DEu7WljG`Jrc-6ntX7)c>cwYbli>R*&NYgkU#~OykiID}jrA*?04uRF zRz>>d(A~NG2R18DY*H=qxo<gK-zTFjtkg%GZHuaq;)P22X~O$c)xNVb$2+%AuN^Qk z<VUyHlk=w8NXtWN*(j~A0%%@j5ndi(J`T;OANw7zA+PhiGpzcS&etQ@RM0Lt>uE>K z^H@ujk+XYyfAMjw8&!<{gzYbY!;uOX;8cFu#^N$d)de5<94hXxZOX=gO+lv6D09or z|L)%f#c%&RzoT=AMz~2*q|EUjZ@)INifr5;)9UR{V5>54nm<VeHEJVBDAQ@_9?HQU z#TqK+RL}{N<d-X$&z$fNTLP7Nun9R65sX9%x|5_jPKVE#G7LF34;?I~xZ*A*Ga1=x z#Ed*WBE&k37(f3Ig8hKUcF|IS1T-!wD%E#*xS?PTGw;)(B0~qIfEZ(O2`cQUmBlCr zdp(n`GI>_gj@EPBm-KT-@}|+z*W4fvaTnCDB6;u0k1>uJGNER>-#alVkOv(;O}H;r zdo6fQc&BDwP8rlI2k5-GJ;mZ&b$Y;_PJUbdOKybAg!d%lat`TLjh1_c2qPXfH&@EB z%*OQjE*?Ai_LkoE1|U86ytXdi%`(r3|NLwQ61SpP?l0$e$H70Ik^AO`lf<CX-ke+( zOg^eYzXNvPs#^U3vx63@7#|+n`qZ`T5|dwSakl(sA`+5KzQZ5mAMt@H?|53ti*!n+ zYT3VFiZO{aQ8HA-Y7e{2zcu%0ODUpUv$~CL_%>0;Yw6i^3(Gil$Vkwx6@f;!9{rTD z2ncGw*SzdGyE{v*R`f!}TM=}RVJLBcvGMR0EJpu`iOC*L)ecBAf&u)*Qh8(OQs;VF z22Zzc<o!M~$g^3s#<EnqWh72SCCMuZ(Cc0c?dbH6u|zt;RlI~GBqhS8h%?Fvvvu9Z zO4xKwUUwLX9+2WnRQRnkjgF68vYRwIH8&*;0z2^{RZQeqn0XkPuRVS(_<@hFkS>`2 zy?=aPU5eh`req=Oa)7hx^LauEHr26dPBU+{$mj)xg<v10{PqqV7r06E+PS0a1baMu zlg!N4HaHE*`&%yzb319FU%I|OE<SFr)TL&Zo~7|eFy)Fr`&M0d`Ck(|6zh0Z`E*lr zyq%jz_4mI%ylm}wOF&DqEmUJ41nd3)amz?qP49+{UYZ#bNbz!}p*2T*Em7&2t<twc zY9DDFeXo|~!^e>lxcb9u$ZG4SM;PGeXY)l-R@_l7bi28>Vq+(QIZu=Uq%beMl7hi# zRf&0qYj(5GTc8x#Vt8l7c^Ug-?{2NLjfZX~>-g~5eVN!UTQ+ZhxvFFrf_5hIO_(ev z@&yJUNj?Ljc9rKrP3?;UIm>iieTP4WwPE~^Po*rf=;1$qxBvm4t|sx(y2O>sxB_bW z?ODeFr(OPfoG#O5sl9Dr=o=bGlnF-3njNw(!O`RK=#c4}z+Y-_^-+z9bxd_<ptW;_ zTul;|A+^Pm48*FK@{`_|wr$0CQ^v$bgSX#o5eYo(xHY!2Bbi&4iZUHsCOCGYKt04; zS!p`ESyr29sxr8c;esy5GuIfac~d-+L}*!DJf`gk^8EZ@8L4-W%|0l7bY$E`S?ony zugvxRG|<4*v;W$9n$f1D3xk8Ja|Ih55ZX7J9oevlo>=NH+FDk4?QzK9-mR69REnX* zm$9GH7&9KFlOLlzic|WY&;5rdrDf&0eiF7hk+iolKf{~=_WSwyNXDhg8PShi^@AbN z|M;}X9Eu)EPtZ#E+ZrP`EV2j$6gE-C-Z(x|bV6(ipp32UXJ*^U-r^*MjFg;bDH!hE z_^StgNd`-7UQGI@HTp3wuRS$Hn;Gyy2V>e8*PjV7YRZ-Q9QQ*ljXAp5SrP|>VtI@k zG5z8dN;@c~{1b*5U=xJAmtEdn>VX-WP(u#zCv=aSqgWC#8I~^3fG2u!l&1X*bewu= z!$ZZ?#29>{>8S%w(oHEk61*vz&I{`a)yZ3d=wFoFWTk3>6WG;aRd!B=y=+|<2fW3p zC3&5tXI<5Pg`SdR`gEdIw*jJTP;y<+z>iCPB}K7Gh0GzH;X4zsr1X-&P%8s_r5B3r zTl^*i;&&>9#4#qv>$@E`u6CBO70sTFgsK1de31$NFMggnpLDCIzTrDqu1y(;OQxSK zSYbKkM@=RIh1EduN62%87g@ZJY@=9MWEHq^Fc>iGJ0a%JJsHKm)EUf3{x-!?x*6kF z;2S!ye>G1V#(^~l4en+VH`=%&1S?<HhY_AzC(AAvkTu%jn{dg`kV{)afHmxpZG{q4 z6BvJ0mZfFPT%i|{I;^S8OnlEnMqec-NgelBjR;PVN^DMUDO8Aer<1|6<dd1dj3L62 z3m2)><zlQbkYN_UCYXd$ZCEiKS@rc(9HJSpFjFw8%Bp+>w@|jF>sWaWc}^KMavk15 z7`4W}iaUs@u)VVQ#XvDSd*y3-Yx4nu??>e7sN0fUu^Jwv8^#P=BS%;9-s7~N!1%{! zQj$pYwBjZIJR&a~Hxd<vM&To%#Arple-s`$yQRB~i9R<c(B%#_vY3O9M{vYNR}p8c zLqg4)suIwTJi%0)^syRI0*tat@~2LmJ!h?WHQl7rav6y9zgv!|1v%Eb3~v@;qUg$_ zKlAA;{O1Iam#kf5YvG;|BUw9|<A2sYMv!|(Nqmf2_1}ZAy380|;>V&jlns||BsEy( z$D31f`xUrEX``I03m7;?mCH<8=DpAxXZu7>sDW3&sEXT+BXLjo0*q$1#NEq4=}+#R zmIM=w!R3!_??6zThlPSRL(H5z+_Td_0?zV_H%VI}j}!y%WrNbpSv#ySSq<Nd-=Q97 zlx_H-f8Xl$i>v8p+b^?*E-zQHQhkYke9pmxqG#R%G(;}0QF^6}ZESOg7G{BmR*D8< zw$elLeXlY~gK0qHrR-$;1EL406a2jdGRU*E3Di_Y*&IN5=9$6C*W%O}Q{c)uSFY2u zMI3_mc18U;%Q18l_DZQu2v_WK>%D@ouf75smzu|N%j)`aK{v~;glF=f*V;}nP3(rR z{Hbj1D9RWz^KyOfK8-Mv9x$=ePE;uFZRT+_L<Uv3Q%&NG81ylIz^p3hRQAHbCE_)v z?8rb(YH=neR=Fjhr6Q@oFe<^&P;ss)Q1P*9q)R-pevBN%iUAWxL-(LvBBY!AmNJXZ zt{@u!!W5hGp+)=aR>ys^?Gi$yQCrw-AqMtW9tz?qm5E%b8V?!}%SSO2_TdCa*nfPa zTSU)Z^7B1Eb-LgmCD>yI{VwO1!UujT5(!o~ytUzRqqgCJJ3oE0j94|lo+`jr*U95s zd1tHUGhMl2^yEW>E_dQ9`%u1)XOlGGKB_AM7DdX<D^dJa))HLuGM6lwARbND(WIxG z`|8w^0E7{Wu3No3Bs*XFT}>ued92qClbEcnLRNuw*D<M&i5bVi<)pf~<ya1?DEdw- zTalhuZdBBZ@)%w=1S{APojn<0QAgB@4~Ul~0BVye27bd0-*-GG==rFehlNIJP`$d# zr?UO`Ytie)vWsO_j74np3a8Qy6&41X-Tv@Wdir_Rr5GjqVx|<M6dln*PF_k%!i9z} z28|;!Y?;$OVrL6L8)lUX!$ALPwSoWcFCJAnMGpy~et=K=rE#hXb!0m1x5i#r6z|jF zMTmRDOa`OX%ogozL|SDjC}Ql1iV4y8Q1#eM$}M&5KOu-knLs1<rb(udgKlBllu=HR zl`ug^y+;6fAV~$5a6()mH5O<4Iy!nSMob|uxOEnfX#^T_T$zffr#mZi6#j1RPd%AZ z=0La}k-b%=(qM$f)Wmhj{CU2L4ZHva5e-{{NP=7U#DJ$5sIm;&f@3|xd~F&)?KL6& z8R;o?3ZA{NGh`QG45b!UPI`~wGAsIb*{7qV7J_jV?T(ATv%&_5wQ){Md5bN+H!V$P zx1yp^W<>WJ^>1}w>Z$Jm|I}9%+D#m<P^hSufcokMbrcryg#}yWM?3<=r{~X7{>9I( z`K9P7Ms_ctwCg>6c7l*%46z~ReYp<Pj;80W$UQtq&YKZ$BYWH`VM{&^pxyMXu0yuY zEgk_bp%#{RrN}HMqma=6VcC<)nMlm*;yQsdSPpVG{FR(x82IciTkj4pj;}k}tNPY# z{-%O0f7!iniIP9(4Sbkemk(RuUg4}dONTK<qhpd!uP{<;a?R&nH&4akr<_w=_8GL> zG&6z0;w|~83&ENb8c+aHCZv}aOxIu88x_kK;fmNxPDYQW^FstssuD60-Ed~I`)X`< z>*G;Hi}79BlvQF_*aTeeN**}hKFc2_{Eh#IZ5tV3+OvvzSs{Ih7OW>V4`d88pKQPA ziugDw<ooq!$D1Q|-=#3_FRAkKL>OQH@$oWf5q&~<;bS~gU5(rHvpVd$oP$M!L<9bT zoma^sVg`|}Gx~2X#(vKh+O{DrA`H9GeBQvczAj?)T%Jnz+(H?1+CYFl)beP$JvAbD z_|%q&=N2Os4>tDk*nlNMERoZRWrB9%8>A$oIRkSF-tvAmiZE}~VZt;_a7BgIjRUao zN<bc24o6}_)4-!5*JIWB4b{YhDsYGn(`SWEHL_`dUf&oxVeC+E{dr*pD`+5lVliP_ zEJQ={9D_8nm}{s@pgphB%}R#<ezP4bS#vS7%j6Snw!1ZFYp_`i4@FAka!jKbz5|67 zZdhgE8r750W>^bUIHOYCiqD}@zXrmx3!nqKo9LDc4seIH!{2-;H$v1d4Pvkl{l|w@ zF-<Jd9NvqVlLP?n2*|eo1+RPr2R;uVUu!8|4ymNh92Bz!v9k&q51-?OFIdlb4|LLm zou08%zjvh_10e3#Ib-t$@s1KRQN@~EJor2iDMX@<EXX5_(d~D|t{&4Dn=}VJ2+W8t zyemun5|5zC`4;7K5RWQro-;vr$9dUJyIIe%KJNhR&tj|ZQ^M^qK4qFxJw!JvS+A9| zJ>3xH-7kn&+OGkko&hY%emjk$S*;;gaacv%8%kfMWj8te^_DD%yg;RL?hvui%t`R? z-+wMTEMLk6o}V`#|NPY%+>O%SwESL9y;_exV_ILGH<Kdr<(a?!weLs6znxv}=KypI zK(TQsVhO`k7{aVY=T=f0W|;I8CqDOo{vS#i=lvkAjb4P+Cb$JLfG2PQImOuLP-rWF z)5dFwok^EWZi$(&vFjc0lg7>c@6WY-4lJkN33&l&+)Xox=#fb(45Ygu%r-RWwNY80 zXb&CC8d{VcfHG3>4K<b94K`&)^-z2wwDv!VqzN?@NL-E$&T^TMhab(B^PlVt5a-9w zDr>g;%gk%qH-mZpo}UP9L8~{`N1n4MuAEK2QvLCQnonRocs8q!OIm9}2J123<$Wbz z$#1PheXyGr79D#!6Av&<IH&4P)O+Pr`uA<eh2&oSwtkIKPWJGLXtj9%%tmWB<Z5%p zdFS!k{7tZTsqj&mwg9kf(gv4T8oEd#_c|(set?XlANd%c6s{)Hmr|glmJ^AJDK7jk ze%Ry=_T69l<DN>JNGuZgR;icDeOdQ@9_Bp<m(>WZwYA+V=BKZwjw4<d$69m4Lh)*} z`3hxlGDh=}>XNbOoqz?2Ow;;f#EQ*AGIVjUwH@!>SngHgR0(_&RTHC-O4KYjHaso- zQ6)ZzfVw5RO+L<^EA_)xlFH7*dp{8f+OMVoo?_>8$w%YWo9E+yCGemdZJ`^-y8E5w zrpM}I4I|gr_?}VrmtKlaQ>&q(2-@?AS(xQf`%3WhX<f~R_6@zX*CEL*Uhetc-s=M8 zTn5MRHMcW(iEySJT#0238c)K?OeUuA)1+$&95(e!FQe8=BL#sPKKx?}0tfu4@F&m4 zbAMSeIB(SuFi3Kdbx{$Z-xlusGOPcq-{1f@Z@U0C42N(K%;SW7<Z#pu2DDEKvcpQa z#OP7X16Ma4Yr4nc8!ognYL}ZnA|Un_rgGA;@bjh7p;w?od(kEZC9zbC(wW!1%1uG> z=d9TL6^<&YSACjLOF6BK+5(x`(6AFSLjv!{dAfBAHiF5wq_tVmW?|mQak!R&%F2A; ziaCSe=R2lHVDZ4_Rv_OTIA8>FRqZk0T`}=8Jr5lvpVQLaTn6Y3k_zz+VCI&J(Yp1! zj!{8cNyA-8Q{TizQ;G%YR+xCWvF5p7>JY@RDI4e<d)nF7Pl$3rDwVE>9MDdar!*n? zU1;2zDfnWezzL$Q@13kl?9o}tA$@Fm4zey0_nAg-O*9S2xbc8DOrJHe)oLMZ|Kf*( zXW%-8W??q>W3}Cs8E=c<V)ThPN4nkn^5)8_A~0^zeGo>o*ViGbFM|4*Wj=>6d7ET! z`s71*Z}kt_Q-+fqg0wa;s;H7rI#M5B3e*2`DoF^3Hq1e}Onl7aDq_#qDm{mD()3NY z)(6-d^e5!0xqwp)WiW?iJ)XGxP`lQo=U)kb+b&sXXwjHNj>RDJNVmASG!}TA<foiu zKtwi#X<odJ<<T1|eC*Gk_`5jweGp)wA;2Qpl4k-EjuQq3%M?5j!H`UcbmhlnT9uAQ zy~;9{&Q^?@6Y?cJkGBGSPkGF8i>BZuh-j!Tc5yGltV~!55BNJk>vL6r(Hq{1nzPBB zr$}fO5o$UTo$8@Fq&7CJn8Ci&m;Ar`(_j(GM-$zH`Hp@OX8uDKfr>+){NN?q=}+z3 z#PkBR(Vp$qtw<VzYHX%D9IqP{3L=y<Oi?dgQKw$|Zw$*wVVoc3D1K~id@+B1emF5; z(`fo@<(Sp1LUQBjZcC0F9zDnFqC0uqusxeMPVcEk$eEDJ6pxA(S#-m0Nce$DfFkmM zRzVICAehg2(^5OdM2Dcqa3`kGYD8*>994a!P^+Zcdi65tLP2#f|9&tn6msntPk+Wy zM^9hK@i=_J-E!Y-?CR)iD)s(&5Pp+NH8D0{VQT5M{KxrX$W{@23QiAynHS^CT8&$1 zY%Dl%W+ir8?a;>Z*%|T5<kv;HX4a5%(D^z+(qK;4{6*8=%?7fw#k+F2?@DQ*$G3m+ z(<l}$CQWZ@ftWq{cd2fqtn*F&uN=2!6!vtg!Ol}PH!p(Yt?DRUQ_SsFIM{RV;ZcuO z$$rI{&_Fcit?^on*K92S!00Bt6$e1(8;K7{R<mMIWgCqo!It7eMZ$rzOAq0>@W`H* zM_Rm^rYo%_(U`WLz|WOVaMhAM+o+Kr@`d^9bX_)<$Z?Ik{w|FBvP?UMrBwZj(t|$6 zmLa2?qY04N0g0Xj+z)hh^C(4(_Tj77mxI8am?C{^Dn4wDY?j5Cb(?zpIbEkBCdfbK zy$#GR8mC>H->c81>7q9tAVax@$hG8oLP7=sdTPB01*6Gbto)I*ah@IT>v)IfHt>=# z5qJkedvoockdw14eh3D+s)dA;OQH1r=O0@)dlJ)E&7K7SL-wb$p;d#V5Cnp7R4qSx z)ugq_@3~h|p2MWI7vxZpy-b3?4YC`s4<J-Fx<X-Qx)jUa^?(8^q`67y1BUMtj55Pj zw#qs%IBzi-9W+E#hEA(Ma3FJ|(wZXp*nfhZ(|q94@}6(%yO9zg%IMN5XyfqrC!8an z+kzy<FI36KvGPb!sc61wsDcIOc~X(G0||zJD(-`Dq{y;$+Bytx+A(7%FD+b+V<INX zk|PRlAuigKJ@BBO=yF9cjuc<@w*5n?q<zqVs}5nSjD6l)n(UukNmjhC<2bowv)58A z{~o%;(Nb>e8RboVVyUwX_WW8-hPoi3C;7=)MZaEujTDoV6?yXtOkh9ixCmF^NFDzd zKOW*wn338ueh6eLvcO3nz5VrsM<0rD4^rl__pcILaPZRBH7Hw(T^8D9v{i<1un-${ zujB*;(*BqzMj_6v0B84mBL}a=($Ag}KR?(v(2iaU7{>;v55?6=B(;fB*}bPRm9dH- z`Mxjb{m3Mg?rl7j0I!eT&Ur|{Vf|Lvv)ry+4y3XfYe^J4h|#K;`#^|`ljRp$%|b4V zF~61rH%!69!s82ZN6coo{k3dPY?@lr_p><4`i@3Pv_tn3uxNiRQZF!}X3l$bdU^!g zD54VQrLuZ9)k(g&e)?k<Q9z#UqWY|5zJE~?iGR{fT6*;d{b}(?I5$>^ZFB+W0FX=8 zp^BZliZz9#-etNSLlwXBYvrobCtv%i8R$PgY2rEaVbU8@)Xhw)ZuM#7#lIf2#X*%f zX&**#q-Gf`LCHCrsR}6&ks$0*G&+|AXGLYM2|@w647i1}+=Gj{!5b!Z!9~N(k}pmR zuy3Ngn^*R(j(2s5e(4`Nulh6XhB^9L(UJny0_p9u0u8e;vrS_vi77b@hgsmf=&(jV zt-da`fvM(t)7vDi@q#D1YTZRhtk~U~1Gk}$Pf^#m2CwFA<GYpK0`ap$M=ee__n71o zGr!4MXc;tS@s-$W_A0TT*Uh7Zh$z?06Bxd&7lb*6jtl6}FkGW^<2djZmgkU-W9k#5 zNSF_Yb}L)HvU@h=o4e>{p3?vF{P%gU#6)BFpdSaLz?X~py4u;{cz&K~tm1@oy97GE z{*O<GNby%6>20;mayS!FO7Cj&4Yq)!*q%D>-`{!9zf)<4kG$V)G;3_i+lW8Ghrb`M zo%Jo%sGwT*m0?D@7ez{abwg$m6b>F?VVOok;$oWv+PX-CUQ+~YNVqx)+UK1$V=G_D zTaG$W!g`64TJ815JPw^3{n^Cbxz;e44AGm#!w@5Jkdyl_laYH<msDpflmI#ayRaB; ztIKRF3{EDFfQ1a=3297BdJmVSJ;3Or1p$%`4Lzx>;G{1w71*y<C`X7NkHHBrQh&|O zY%TBu`q%p+USRm#Tq?lsMnHhm=fuvpx!HOBX2Kz`GqHW8l6RZztSzXV%p*zC;oWCS zQP%Hsab=4Q#5fv5Bc*-@)bxeD%_gy@2isf!_?*ffJURfjp_{pncKgGNvUNWouU@v; z+JDA9S*>3$rG2W-c$-w{S+6_cSs)bA1OSb`<)eTUn*)uSdp2hunR<Qevk7}oSnwG6 zEqDU-+T_ps*mg75c3cD?qc^`RbO}Xs^?t<GkhUiwvDqd)@?#*=5ZR#=>|&^ZFc8f+ zkC;>esaFUbd#v+xtMOPN{wM&tENK+CLLFo0IO1^DG}PZXAGj)0*eqna4gkOk%VaAV z!xq!m1WF)$?u}vJ*fv|$^aVMAdwUU3o>POf<JI%jgW8u8tl;NgpLw2t-xxWYJ^MeC z#O0_KE7#(fbncNO2J9X0P5$~FpOTxKYeK$VhJQFDRp`=Y@v5z0h_vgIy!}ELTUB%y z(SQ5rZ?ZS*Hi!!gQ#T;glJORJ`bUpjQ~9`!B^}jQcHl_+4C#Fe8_CFw!tgN4IPwUx z9O;;NKKPzTEkX7Q-%1Tl3_A?*Lq?9)hohkIk~m}Z<EHed$G?Y;d0>;;FAIXR7oZps z0Tr?Kg`Os@UttaC#VU;%3LR>^y(rRVI14?EI=^bNc9})Y7_8Lv(PRnu964ljgpR%z zfUfG1(Fdx~``%M7J0#b14(gPtlZNd?+2MNAterl6CJ!m6LzgUjJ<X{4C|WV?4L?Z~ zb^dj1V#)Kz<Yv?MkL!_aDbKq<ip=f5ZMA1XR?I?-u770YFVC#_g@j~fc)<!Lp&cdB zt2bh8eGzuY>v}LlHjVK`_t&X(!{up37*lL>R{!GX9t;$Q0=6MfA}lnLH49%AAfUib z3l}Lhf0X1y_Kfi&NA{|8JOI6{tqsLoSr#2#hy+HcomViHIKQ%a|8G(ZXko=2VO&1Z zoL_*D(a_dJt;zxag!{pUxEae;^Wj$==EtyJ$IQnE`-`KffznOmq>ALTG(3JBBso09 zY3Tv@WiA)|us}p9>{}#iH5Gi(mp?2drLT_XC;6eN^C!`TT2H}|_hLj%Ncyh|A)=s{ ztGJdGn;%q;o@KR@4bwfJPgwae@gwJDa1jyZcd*?2dRE@Ne^J;*+=jp&uPVvum`o97 z*CbomJiT1sX~Cu<D3!R^`gL51z#3iQF;i8-MaaY3(lV$*X;URxwXJq*NKB{W0*SE* z@<pou`23VBp|JogL;mz{FmN92pYpAov^FvLfCxg@^F?wk9(!sOnv|Q29MQ8G*<qH- z<ovz6bXr(_IZdOgxb8G%8ovdUn6DK&nYgvd^igqf=0s!CTA?v``adX_xeO%5m&sEY z=U<0lkeZO;6*{Ghv$UMD`!@g_^+JirtyrLa9Uxwtaa1BZHv|fnGIi}KWNQZ0Xf4;i z;dpOSeDxF=LUSfn0SzEu>H+Yr2*HhR$Y?qffF9Fo5(SNHHj)mHyf8x6>ljWqG>68> zUo=hl!A9+6bc~q2N|FM%(LjOZBKUjAem{crMBNUjnExj&EDL8##PxpED&fPWWKBRJ zU&q3%)FfYItxua7YDn+ed`Y@=n%n+2A5umEkAHlg>;%Q2faSZ8y_Xt)=UQI@1*Nj{ zyfo(D9Pt7zMDd+YF_dJSXn3)W_8#4ho=1(uNYXbz1R^Swg7k<}EA-u(+FD87U=4q? z@go<0+_%*FDwPX~Gyx{QO$DB5UM4a7U54TaT5nY-Tjuvp+-NoAIYZ1BS9Fv<C>njo z#<GvAkx*8pi5hBsS9oT9KGX_q2Pvvggq*Hl!Z_^GXJeZK_rD)G1kq)(<5V$mmMJCZ ziZVe8&r*KvCw-`WtIt!Vfv2L=p}57PVOkVgLZ4C~ND*m1{Izz&itw^a4o?%jYdfE@ zUq`&y!Kp{7!eC3s2V;epF~vFxrO6n?z9iw&mKP`pR$Y34)Dx!I>+G}<W2a1z7|q$u z!{YXTe0+r`L5Xx4*siK%VDv-eH9lebwdq^GpE8JzFtOGY;XFkC;=L#eWkyUvP(d6I zHt;)iKqVOhu#A-_kPlJj8wh)Aq&!}86<$cr$BsBZ+Oro+FJF}NvZK(-3^FCniuF{^ z#3m+)IiFqw8dfdU3YNY&p%Biq`MWigO=JJsjLV9qB~sQFINP6KNCwJ5M?)|am3`BH z20N@h$<}C3#1z*ahM6^$h)Zx2gcZ9mV{Q+dWwTS%98GGpg~VEvA^=M{8jphGpkK?J z-gRPq(kZOrQdx8A{M_&zoB?zvZxJPnGu)yk;^Ugo8#8oB`<YegO^9S`i`TX?`}MkY zsTMQ}^C7H!9Kf>sd>QZ}QN45UHge>WaCe$s`p2hM3@98)r$Mfjdd_97b$aG_hY79e zxFDL=L#b62qI@^k_oMc(Y8<w)^|+Ni*<|voq!|kD;Z^OKc{^VdWR^Ko|GF0Re~Kq6 z#-SSeo!)NEh#My7B$h2Af6Nx5T7B*Lt|ww7t)80fgxGy_VTKaABvHJykr5qEVn*rH zMGv_x`})EaOL*FU{gx=RKc@`|&J%-l=ET!=+c^9fI8n&cwcOdOjM{n*N&}U<OqvdR zx#d<s9W^52Gu@&o8W8!Qzr;tG_6G*MY27UXohNkDgDTlG@7{E5uN^`xbPXqvAkiOL z4Sjr7rN5V3c}+A|e#8ZrV_*!L)2S3DNw3H+c^yApDVCpX$oih8<VBhNxBnS?B?yY7 zv%~h_&y<^gOA^7^arWREl~+5St60JCe?Ip==>|YVNQDuj#HQ#K9Wvs+;NY^`sMfbH z38f}i2UW`~?XO<vdy6a$-zg_RR?1_=U}I{+zgvl?r*<A$1n;yU`u4z~uyPy3D4}}{ zb{5qt@0|QDFanL!q?vV@jKBjfQmnDCOI;klBVu+K4BIjgE*4_bvv!8L&(3?)ocT*= zfse+>xT)3XA{F_IFfGU3+14b(vPz}GK0e#e7N>RF2I>M4W^rTxV2xZyHsJ*3veQn? z8cy$!1gy#&3l%XIqNyq$O2d8wZGMAgO#)a;k>`0|=C_1z>d*_IZn<zK!@Oyn_IKZ= zd1q?9?ce|PpI%8*f&6y|2=}Pk;@e~|JPdBHmxII$HRnYb1@uDiWbkV_vmCx7DxCw> zM$;yl;yKYB7J+ITEbmM+hNz;JGk_a0wB_%29jLn|%&CfQw92ipf{IjsJ4RL&Bun=f ztYwldh7E_5sa}tvD?&$=EgZwKQ4B-E!T<!wc)J+=LpEzJ@kJjL<$-fjz(PShBR^&+ zUDV{4c)tk5pL`5`ljtEf;u#I;-hYX-R+*4x2KVyu=<F!!O`y4AWpzjXqfgnU##CFY zH%}|2<1tFJNtvXI+JiWCx}HZ(>oY43-B))`uB}FGan);gD(@OSS1Fy>Bdl?`i~^4C z%)5g6q$2*>a;Psi7?50bQ;+wzJ6&+-Pi6c1q_^g__5J&QJu7e$I23F{|2i4U7riqH zwN1j$-Qh6J`un-+8908_w+1ch<t*)l52NDa=e*aq+3hUWwx<$E2eXvYkeMJ7p%8?m z>DiVE0R2g;P2T1C`W0)w$kOwVCB-h)x&#Q?IEZa<v&U3VZVi=m*@Q=X@znO*q-W_E zRoLtiGSnypqkl4G(eVWWA;N8r%J7gPw0^BgV}UfhVPrlWX<1u3MtZsMJ{><IWGeZ$ z4YSg8A!9gZ1ml_PwImip@n~qMVf_246__SN0B$~TG=!qMkY3$YcRvNwbjBGc`1#>a z@N)JRYlkD{H@Pn_>8ZAZe9Lc@rZD>?ThZi#!`Vw28M0F1cYIL?T18;Gr(PFo=$_SS zsmiyzZ`sw2|M>ipw?Q>QK2H9FPSX&sMjJ&x>!XNv$t|7C?-zs&MnLy3o{)}E`EaRA z1V=}=SZ0sbR3IXy{vK-+_M?OC7y${D4+})bXsFZ-sE9}Y^wEKYfAiDXV#;6d>`TPo z!Yr`1HG-Iiy;=T%3`7QAd^)H!JuiCCL~k)qQoJ1&9*!j*CK<i=dtOWq09C(wgggZ= zhfJ7%`e+rJ)xlhpHw%3j-j6}n6pO;xhAPD8glf4|{P#wKp)IbPLhEH1H6aaJg<AdC zN+AHtUN{|{2q73ig^OA~iU_gw-gthIiEX((KPK!vKLxKmQdF<4S&MGXJn)aUu7C^K z?IvtlKIzO%BvNpjd5=wqJn@N^GH}&siwUZOF&wke`v1!x-h)jrp@_!@n%lxXl7jd) zKD@9-=HsClyn~im-wKz&i!k+1LXi;wM&-T{rPjq>phcnspPy%AXm~to7AgY5;UE)c zFvRq?zVbt8bGuLuna=f*vA@=YQ4i59bP{KV=0`7L2Y%?Bi8K{DfQ&QWvOzQnsZ53f zA;wOoa2EjvL??3Oi?*VQ_Er#$;$`tDdy+dtTtZ<AMAh2W?JF3TQ;M28&_2c9dB?Kn z{&Wer9%z|)iWst+^3SR(ozE_o!>7t*wh$d39^C9GBjH+X)O?}xzdk!!QP1D%pFiF` zs@P#)dm@6JH(54Zn{3vKQSr%yS($m8n1BVjoJGj~CHzDjf*5K&`GYJ8bFawX*!q6l z5&suI58xV#7d|>VR)`mgR0Rzs91P3Ttoj^mM8C*NK0Kor4?}SGdpV@&#}SYO=K+mT zyp&az?tK%zy%F?92=tbie5C?Ray`&w1e_k+?7YV1(dxEhqHzzEFWbyNH+6G7Wt;Gj zq*0K=ae0~JcNtg;TVp5yspx~{1tz?+d|JhQ1iT^)y@U+%1z~`&@b%vMsa=R%VK11T zDy5hGdVm%!k=*`k4T59i?iyX>?vubnLMf=|%FP+M*wLjZr1yfeOWiR;@Rjys%2$}F zCVlR4_jAu*%Ds>OA-aBk{l$GFDP79MW*3T2P!ke=#*n5kAHQ99ic3gCoW;aKtb<vc zUt{Qb*^i`xKNa7yMZdymasyIN`=LZX{~sUu8X73_G3*a>rQMok+x~|mlQ=G-=?-ZY z?A{b|UF8Qe1+;=lUUZ)Vq{`V-X*t6$m}JC-&1j(rQ%@bf4a3UPMHUt)Y|ieKOm^ys zOtZV`Ifm~PNNO5~s*a?wS&;nbhI@Z<-zOT$gvLX*<kBkyk%<D^n$oIL+bLp;!EF=- zC$8uKu)CUtO(ecD5k4PtDzU><Uaiu{w+EYNt2K)lB&w|yp9oE#9ix9n0$#*&mSH`q zg{shZX48n#QuUigRE7DQb>>xn1~d+K3%O(Ykf;hQ3M^81J1FgHx_+ND=l}Fw^k6tG zT*bu$!T`siC_1J}bl(o+X${Bt72Ye=E3v~Hkqd>+afUh<ZGbW@clHlPLe(ZmLXrRY z+{jIeS)jh0zv?&{g+(S}$ktbe;2gqb<0t}e$k+VjCLFVvU90}Dg&yEEi-p^YT20?V z;F_IVFr3y0lXg=g1rEgW?PpM9o3R>g%H+MO2Hq%Gu##W+*nwUz%uY7`g$>z4Rxwd+ zx%b|tC?9(}?!Y&MhpfEx$16?Q3MzDlMz83GA<P&h-;I*b3T2`P9|;8=v{Tp?ez_bO zzDiD+q*nCHDuC8`4#=7K_K$By2^g#so-ThkmkoT}ZZP{C_}g8trghokQ@}kZheB<T zhXXfH!xU8Wqu<MX&{2@!zH!=MOQQ3EhL^0@kv0Pu=B1QUD@cSG=M^f=Hbx+ToRzNU zawOc9sI(c(B>P|g#gP;dgJsLYlwoTC`~(RAbJrp;pIcO!X3CV2ar(co{J*_pfM6J< z799>T2_pXR7?f(!&&hzk5XGvW5fG&YQQ)d5z}R_fo`FOj+Xbm0b)P!E-tWye>G;%7 zjpgCW(Ol7odPRamo@F&`W#kH37XR95BexcFEBAh;t;a5C;=*(-0oyjF+7vD}{=?KS zE#4uU(Yv841;An-FZCX(-;Xw{S1`)IK4LIbCT&<oxHJUB)70yFKqXSyzS~5rZr?1# zk1*)Wl1@Wmu!)cr7($1_bjNSvQ-;^h+4LVIN(m}3PJ+bGu7@)9qC0gt)ZvFE22!{G z;%8BGlYu?{V18>&9x}wXZpd~t!ow%#E6}>;VMYAE`TW<K`~UZY_Q#fdNu1+*KguLS z{Ie-A{d5IOrmS>a$rhn3tm0<)Ghcv;SnCx;rB&EYrSB+0KAwS95k(p_?V=mqmw{)| zC<TsIGZ~<#moB-bFvl*;F+>OJ_}7TEMB7Bp%vq>atxKTP0h1!+3kSpG!)us02&_ON z11ZvjO31M0GrA^ZCGCN&rt37{FU>PTa}@FYNet{Zf-z)pJUO2`ACnW?`5)w8+~ys> z@tDSlvD()yToftztLf-G-2GR-+AR<dVtZOw`_oOH@>OUecgx{oQ~th6Ks4S>2$YK- z6JI0<*wHN2dnGH~dw!t!VKSbJmknJOg#sX-)1u!)?QxqZMg()LeCXF*SHATxH?BH^ zIHsX(7~{WUmV<+(6NIFu2o$Wp{2(4jVYI5GE3*!Q;pi0Ey&|#l5A$tD^$F<Y4agaB zA{7U<yjeQ;HYHR__wcdTX3aP)NcUU~W^(c1S1WSUY8I7#oyI1x<MfajP^s0}D$K_) z-QMcqo`R+F_WSC>NRx+cXdX32t2zIR(m^%qx;W|Q75a^GO!#<wt%zrloM)6UC}sR` zU{Mm!gB^D%vea}Vk8cb+Z6dPZ1F38Ls55ACf}886MriU*BN4`#mOoR__Ah>(m3scC z-$v0`r?kB9578IeH(Wc4fprTf=tPO@g;tUIM!M~Dm&V<GUGEXm=|a#M68mhWmBXZ2 zNM2`WA~KN?Ad?SvSso~7EtTz{vv#uzj+GDq5wPP+2sFCt*V>BU=-Nd@1RwL$wNqm^ zL#UN3zAsLWC5sm95+ahsAsu6K+8^;}nxRdbp<^SWQ=n4-Na&NZ^7hHoLV-xElT!lU zpA?S{=oHknj8jowu~-Q($njbW_VIeKM<s^)3oFuMPlzj9^roO@tHnzS4*|%cqdD2Q zF~tmyd0oiFLKp!E&(l4B&S9hOBeENzp#XMN_Z`6n6eJRSMMiF10vl9RQhDBZGA0M8 z4dzwHCzhMP_S<*7@laj||9^bGISz`+$K<X8`h3;{ej3H55aaiKq2Nlzz{-O9iAX|o zC`~z|*>Z=_lCdXm1}>e3RLSQpuP4o0sI+X2jP4m!08Drj_@Djg=)1mrdkkUqEOn0S z)D%IGhDdAS7YNdE(pZKt(g9=GWJzvz{A*lSzmf&Nti2EUt^8Ps7yzzghZTa!K7`z~ z2LJV4Ix;G9J%%k7=pa-32tXD&K8@)!3k#z4?zsgrPM~7g{GKOkBS55K^gAY76h33u z2@860FNqO05HU21pZxAUQZ7t|7X?$;#+n)#5Q-Q!NiAUv9Ymaja`6DE<EOwdcc2Ly zg)DNrInj<I0&>cL(lH99gqstUN3_V#R|2R&8T?sd?*{=ox_I{5denb>jMZHDePmwh zHx%j#*uQJI6YO(@0<p<}v8v3}2|@;8sAaBeJrZ|K?SJKDaKz%j(pJ*HwHPg@ZJQ_A zeSJ?pX_Pnk{>;TNRlJSiN7qkFGi`a+oTTpirfpAZYh^R%EX(fK8Jz~y7gx@~Yn3(p zc_YD@U$0CYeQMua9CezRw!i5fHzfN6(X*m@e}oIX&BVI?S*O#+@cq`#^zMzO`oLRy zJv$pW)hmE}3GR?e!=M#;gks)fvdNU>Z(-ZkGKV_TZe07E8(dFiwhUv>eGk#oEgIvJ zj38gWnVXCwzpCL@8FccKS{TA>uW>|&-fxQ{Adq!9Vy}xkH-o*+ahu5j00C&Iw23eC zGKE7L*(RiqqO!)M3KJvW`{5rS3FS=|znA>{Ppm43<XCO+A_5yeuhyXr-sD?MM~0UX zi=?i{ECX0yWf|Y%#fBai4bBkia={GCFyG3<egHT8>}cN5y^>kf^JlUGR81FTwEz}P z>@$#^lW)dHSB;JQY$jtiXp&=(Z5TULkLZ~_hqY2GqvHz`Vp=*>Y<|7dHgh=f%6YT) zwBFqwyT(gj8t|>f{W732Ft8E0L+j@!sas2T>bA~rC{{-@_8p#&5#L)NBPwZ#a7=(D zY}4XzACI4%p@wj?PsKVC_u=LdjN?^dJ!i`wj?Xo85(l;E@ZLBn8*Hk+82?_I9*IVK zQy(zuc$X~o#5A%cSYFu1Z=;{HFwqJ}KqC)3g^-}zz{n$5l-Q$itkmEQ|M;+ii73L6 zm&vwJmAYcH6sVXY-c2MAXm^J5Dja8_hwK+15YK+i57P$1S-|}`GDRwq)DDg@FnEnk zhXH#C0K;ygsYsQI>Mr}V5yispn~BJJ*SEi({&K(4XgMRf#G5?=^F@TdIwYiz<~X_$ zHnPQK%rPmI26IaAfIr`uCty36NyQ8T_JT~5^y}`-N!ralGvRRfGyEy6`R2Xx=Tc{# zz`LfQ^%?iGayC}352;1ad408m!}1ZmjCwoJxU-<=N2`IGm6(eH3CI|4@7}@C#j45g z2;J4rbT|6<aFJ0{ao)f?&}njk*;RI0yB|22SiVxFn$rGITBT4u@H=}Mm4$L72@a-V z*b@ebi~t2kkE%9&aB)>6*7qMDc9}rTXvAg64(IJ}10Mdk`qKPEVUg=pCA%fRMHX9T zfLd%_0xC5<YS}m_iZdk<;>`h(XAwi+C77<p6G@d$kx9?f)l%EC)@(-oz+S<mCfuX2 ziG>EHA#>Bc{i65Lj)PH$>5GRAHgjb+|1udPI%0S&1Cs1uKQd!)NMk`YuWRz$VTN9| z(NXY=z54k%Aku7E@#D^mZ<s%=)R?ROYVBP!XFLSV!knvEnXx1bttjCqCL^R~7Hq<w z2qj=y1J6NhI5AULY@(M`<_ZF`^D4}+D>FH>iIueub&Mq$w*BJQLws0$k?5$r=Ft@? zLrVt$cv`+{wbM6=*8^%8rJ;a=o3gBG;#Di{ipSC%(`w>FTU82@hT4mNe1rwngriaC z)x(>*%`;;FW8vNTP17|FO-H8H<>c=!0AKXB(<2;P%ixQjDT2$Z=6hoZ9kSC$PA))^ zOnfnT)%b+a5`1P6GB@WL&j|c5V<zqW5IVV>W`pO6;Lhh`iO!-^IKvH4{PpwS9qWh8 zzejtCcGK61&ZOXs<AZ?p8gFe^CzUTR2W`zCU&OTGRj)u5#HQsBZ0X9RGk>o|4z;tC zQkS$p&XL`YIwG+oPoKy0i>e(L%Q0ZeAR{AY38Igx7JU=Er}!=A^`m1WMdp<uobMGJ z-Sl|fXEjB_gAC=-CFLYY$M&`x-5Fys!TE=7rZ|#+8kbaNc8C~uq6}y_=iy8KSkL6~ zb;*UMs(P=!#)X03;N8K&;D7Ul-@ft|vp_z;kG4M5h}xAOG1T%dsx$rS9>M^6yAXUq zv%$|)s3KAbNFSuMTzI~n;-H=rGKei0P_f2tD;(ncA<Mg90T$HojNKYC+<AT;SDZc4 zj%5@aL|ZO~MK#Gzr=zh;;KBHulst%}aR-gL+VXxZ+RP$T4>Dm$WNU|%M2X0xkaQBG zETOq$iA2T<o5=s1T<%ujVwhBEL4yOtLl|isK7__!^p&;s`iTnUpA>G^DYGP}w>*Z@ zeSDn=yFM;AsOZ_wnt6PteL4={p*l8hj~%P`d-%Kyo_KI4zVO!P)q8p;`W2b;<lV!t zP|eOl@bmM^%a^6PlItPQUi5b5%a?(v_uJ2JKi}1UGuU|?iOPH9;gRrS!hVUL8sSAT zcN8N(K>eo+!<py#kNXd|rPkXk_x^%@Z(DbM;+G~qGHhE&$K_mnO}p!1OI5p0kDQ@F zAtnO=r5{9!b%!Gkf=$LWhoNF{Ogp=M%Pd-?sAd#~$z%zBK!vm$Z!&Tp*b4&%nN9!= zJHBg3aCEX)>1Kp+ayE&KI@<(bNyk1zAL-7BiwJ}e3ve8Mi!4Ts&aMobI<CV&LK(&d zWKQb74AcPmSo6-N4fK1DBB7SD{jFRM3*(k9fQCR(Nf5$b=C%#VN;4WpgjAW_4X8=; zN`n!k-&Z2}<C&KsqLFo|S<&%D%DhoyVk6~UT^*q-Mgk~@eBDeWLO=?YwL=2L0R9JS zZxt2w`-OcE>Cg>BN(?dN&@gm&H_|07Eg&EbLw9#~x1w~5bgOiOG=d;6{GPS`>pgkS zp5r;1&)Ro<_r9;|0#FmiYmU7-pG}|bqg-r5)Uo2TVb*-%vt_ysQ%A{J2lN|^L(;p_ z*+=P<V(j+>zn@ZkZ~uoJZmB(8<M?ZByX)JXaEQ+kgg)(WSl5Ehnt(snqR~H6-M-TJ zA4?P64Jf@R2S!eA_H)$~mb|p9r6mO=T?>1sLwmP*&Fyh-TH`7q8}0QNX+J;@U7YsP zr%Gi{w@PoxlhV4Lw5FJ?H#hrRCa>&~oLrtBKRzN`41CG+91lO(`Tcit^z|5h0tCa# z?hQg;7Rzp>fI#H)LCoo=BpKOiYSDut`baYJrO?bANqkUyq9=u)Bdn3mtUjCxVF-x^ z@C&*S3`L0ys|z>5i8PQ*;vwe5z>+vqr!F5opHQIn^Cup(r3QR3uafCAX3xrH%eC9w z@bckN3b&q9BYNTEC4kALkFt`1fQD%0HW@1kR*NsD6|ZtM;UFg^7n~z+ybG|kh(#t4 zzMt`*(aZ~;8>yQ7<nMG-sp#e4S&4suKwY9;Wl_#qIisMOG%fM<>6)$kgO<my_X-MJ zti>Z`<t;ysZKGsWlN8<E!)?h}?`1Ws0D+|+Y&ww}NVZW*5Q~HR#o1tbUWQNVvP&#s zfG=YTL~nu}7!E#?f7*OQK>A8CaLOeb#dpCSvlUy8lIfMwXgRcOsjf*iw9q?BT+%a= z{$x2PEWbAABl!`{5V(f%HIa-=BPvnntm((MyH8@!UtM!i=xaB7m4dSD&C7S>_~m4^ zo|D_Y&DBD*%z~LiEydW(MRlH%uj25;FB1f)`{7>IDo?Y&|65-b9JBYHUi}z?zo|>} z#y?Cl$<TjL9TAyyjg$!8^knP*-#5HqLNrvqc&KbhQG$j%1wvrdAc!)pr~%sdr6pt^ ziGgapI%Kr%Z$Z%JQ{*2fU}f4s4Asni1;2M?<`i_4P~}a!u6wr1NSo6rhtlt0!RnP; zM<c(s)J3i8Tbz)LN)-%e9%hxbyNkLPcbkQ<eJMC@Qzz^3PXoqfpdodn1$4tMW=c-m z0y5Ko(U<vHRyg3|D9}aAlq%K5Mq2{2J%83(1np&Uu$ty{Ip*chUdCvBUDpq=>Hctx zh#Xb`M8cMAc=$#Z#43#ouw+4YZ{9TM<@(D1``?G>htf}v#q*0luc!FJO%UNp@LeaN z3H4yi+yC@mml7o~ZR~M^lq}l<4Rj%&OQp#hr%WAE!J=0Rz5@ViXXutOYTcPH|IoA5 zRUrq^$Ypd0A*@W@jfj>zg-(KrEi()|GERA_WfVcliWy|SZxbqO)*l{cWDa1;GDmh` z>P4IYVun&v%Lk(3#PIV4yVI9&M-Vb(#LqRWZg;wItIq<vAxfBX-|7hZ!pQjNUD-hS z6k~Ke1mZYh%-ndW?8@UQg2EA!zU%I|=Ga`?5UsFAiaIsf3E-l6)ZZ%QixtBK7?()! z+~WJ{o0YNOUd>;X@6usT@A4iVtnFu?ql9F^X$pl*c!EYEgu|p(gZA^CdUgtbe?e^0 zMU=~KKmPRnd1>Sq>yUm?$xGaw)Iye;Tp5~GXgK>{|L{rnGfen7zmi9j-|<WtA=I*& zzK)_+o)V{bN;5?PwLm)CLOm?Z!nznCS-`Hr9vE2ACm*AnAs@p`rmPOaj4SS?2_EN> zMm6iTC@3SOg=1Fq%3vZJ4F_rS;eQzg7&f>73V_WuU71o6KMV+x267_~{(ZC`cTZ6> zEX%K$EhvArb(ON{yj1^brP{Yt<)Wr+qReXnpSjDM{*t%uS8a;$Z?&&=<Cnzo4U4)F zz{#2^(@Cj-v~PxXjz9JOQlyTxcYb?Q+N>sKGRWt#P;ET4eNFV>Hu>;y^Oe%AyMBj_ zUAWR@_g2&QScsdn618`}0CGS$=%DlO#55yztk$0Up>N?|+(cOxRq;?-`bDixncw}a z<~@t9UG0DK!CP5jE()YKJqT%w^=zHjJ3P7-Z#ibxzP|NxFrBy4sd__rEfjYfez?>I z1OTan5F`v&aS^B2#JTt^L70TlDfMY4g6Q@MCP&_DeQ|P5eb(>n+;~EfSd5EppfrR~ zWK^U;2GTNFntU6C_+cz`IiIRs`4F>$&z}<YJ0oQZ10^8gy?EjRRvKPV_G*1F4xCGN zx<8;lu!vf`0r1rc)YF5d6BaZ&)q|G3RL@Z<P@oJYM1;*SNr9x+)ya<KHl7Mfg6JR9 zM4~EAn{{2Wp4B(7Y;1Bo$JpLaP2N<gf5*K}io~Jl&;e8`9GSqDP0J*CyH0c@RAlU3 z5GJxDt*MlA)YGZy{cgkK6Kf}a2h|lr7Nd{?d6~t3?-S0gtY88fIr{*j`lvj`J3meo zo{`d5dv`Z^EltTdIo~lkGb3eLyReE8?dm81|LQYRoIiCEd?d9La~KUy#af|s-dROL zh$~H1HxzsKl%E^B=M^yYo<0uz?KAgRB>6IijmQaF=B3peOQhgHXa{P7Jl{?VAQnp? zBv4lMx+IH_zZB5bS3ZD^jIhv9fTYce01^P81k$1O5TJ;2u7i5jG4)xXgg6>mRw7tN z*2+GmBnEo?q*BzD7zzmEcY9e*nmXhO$mZ&V;z`5@b!^)`n4H|U{1_YQXd2p_X(B5( z_xEs~i?6}!LzC;Jf>W~LMpq+siZZ{8{4LH%A*_Doi!Ri6C%I3`Hh#Zd->mMafA$M2 z{L=n&edFfk{BjeRgLx1wc>~_F$fTY$K6JUb4MG(Bux<pt)!lmEW(SbB1CyG2->y!4 zO==SSoW@6fM1t#t$Zpx2d~owfXOR_~kw7K=_1~X=RF_vb{!dPZHTjxvNLFM0yj?8~ z@3RuhV-0(pTd)MV1ubzLgccjjdP8`G8os>~Ii!7EwmI`o>YE7#e2|iUYP>Kb)Oa)` zbyh}B0|QQcOdi#bTg#!>Y#>!F;<ZpL>Qryy<3xo(kfficinxoCL{kW=s@MP?^t`K+ zuGFgG5YvJf#VR?>(a;ELkAYa}CjWe5JsO!#=*`%4IAJj9{&jBxPp)jlb?mT6qr2*I z;%3-FFjV>`_2i6e>6B?mx<$!A@9Ol%eo@iDM^TYz)}Z=@Pa*&F{jjK?fX4|Qkug~O zcmOYv>wK?RPqG538Hg)hsMg|;7fEJo45rtG^2K5&KJ*$c)7OUJAL4aA$HHp!Td^^_ zX!5;d{$DHhxkVj>G(tkv5)P$dmkkF<*oeRJSlwLV!s=!@$(_?}ZOE<x67t6)pdIHa z6q6cr>`+;oVJdM1O|x>!dX-4x7ZY?Lc@$SNO^>9@f$pS_i;p-}J|2E6cpDwYS>i3h zp4akhtKPZ#cN}vGJS8DgT;5GkA0Wua_h~Yjl;p9D<bA&Gf?j1+n@H%5qAVyzYA!j0 z-|K-sInFL;k2(mVbjSzCmzl^hEC@OJl4FV6thGQZW5Y=8;`oLdu8`QTL#r#7;tOuG zM*dGfIKq$2<*M9|MM>3Qpy|_sL3zcznqwkBX3p`dYUTM)2X;_vxPE3w<v`7BQ1!${ zh*x1KpvaAyvfe5T!ICw_%AhthPeccZQ@d0w^_!hls87Acr->p!2^>O1PYV8(`bBn< zdW%C2FZ9ZtKC!~%<U0v#^}))JMSQ(BZxf;?DXw0G9THye5iia#95sSrOJ(ISOCfs# zpb|fu!poehXkx`4G3G-NK%%*ME<>(6^yhF|X#YwumZJEN;~2!aC6=X}!=5e`nrBHe zCP>eCR@QN(=UM06DqEr&;AZ3Uh^CU$Ib`5{WN=*hw$t2!u^qpyWxm1^WA;2KfCl-h ztYheVj^XDBDH5kkZmuDM%4WMD^dI^3*W<)_LofC7AT<ZmF4$VSzs>vs834(}jNCV) zTRF0xuKvLJ-`xjV6$qdJlqGGTBdT)!RB2+u8$h>7`#{#`>mUWGFAg`wK^fe|K|u%{ z%mfpcOo%tX0}MrSxy6n{2Q~mf19-iF15%dkZmh{pLj2B=6T9*{D*L1icI52*-R5e| zhr-){#l93Nb{HLJ_z@^heT9V@=BUY7gg7)T)tYJS&Ja~OsR%$+RL3gL%uLhWnw>os zwz3i;4;k386ME77xQw<N*MR9O#!6$Im%AH843d*m8GA{o?K#>iZus?h3H~&@+YZ3G z>|3uVyXu!<NABpwmgv7pW|=v)oZgsZW-b1^l%vR0R9PNVnT(q@J#U|b`19qw@VS<x zU^dR#&B&!nKZU+l-hSv+yrM5}8~Nwlz@kHls}DrTyh6*Ef>JP<XB6ON&oBYd`=yKN zlrRX875Wi-w=kq|d##ud3P)*?!Wiz7sm&$;syRD*09!A2Z`^4b!71drq6{cO;8?Q8 zMp_Sg&`!f;!-p%iEz_c;=QfL)XFwwM*t~K=2Lr|Q9A_@KLp2$j9%w>tQBdc&Huxzo z<}Pi)mmfciP5V}b!Ny4;v%8@rw)uyz`In!!ndSVd&6Fw5h6YFFAC6e|+&`q?u%;xa z%A#Q=migneH!DpNLE-ll*{#SZt%!;i1dQ8Ov?{vz6DdUY;|<J#dvk*IPct<ZBhxj4 zCfjXWxU6qHVyjFM?swQq!0E$XcrSdO{WIB>(N8mSk<LfOF&L0(fF%L!Y@f(&m%cXQ zM~N$h$Tf!aLxMAcl35B<nOvJnNs|ZcqEOCbfsk~ByV$_Yd~`6dW%RSrt57CDUpQAO z-e+}YmMr-qBW>N3LKXk?2CQuRUcg5I4+Bm}v=2M}f-f>N55>Gp#EUYQ=VSu^<1 znypBW59JXNt;YyrWXk#Z_ljj{uHlJ~+g5#@;nmT__V9O)Z@z8IC)O*AHoNSTJe>~R zu{>R~C+t2I%QYN&%OY<Kmy^jRj%S=LEP4|YwqFwt6v*Q+r`fmj+AlN5m4laxu4+gn z8*<UF`Dzu5l&UMPR<#HU?L&(^kLC-L)wmix*f({?(|Ni`^c;9ZYbG$F>_#hI`23S$ zh4BDRGyLRiitycgP^kf%cRT~xgeSf$ElJ?K>;LV$&(klU7VRs5L^x;_1cNWf+R2hm zBuiwGiIRGS-o@kUXV4;Vgv%R&C;=LQW(ZNwv6W^BX&ybSNW=~MSRh0I<lP(2Kyjq7 zKni&zb?+7%L;xSsQUK8P-#frDL?TO6r`W@c<ZYRfRQApGCLFGVVjX0Ph9&z&L&{&6 z<=wA0@kT4}nXtBZ=F3gOd<`!7)?6!CXKLEzvbtRQPb_%1q@1j0+LEMNcl?i1-LEck zttT-POeFGr9PdT0J3K=NGinw~-?Ntvk+!qF6L$E!xP|*We>?uQ=WS~%cIVFr2fw{v zqRSx<ln%c+J>_|aE~EWk_?$@~je&Sgyb<<3ez=+-`@J`$KOm6&ApMBIEsh8P0s;{L zJ;i7lI@FR3Ip*I$(+p;u@(6WOqa(<f=3`I_b)uGD1X4nCL{JbyFXC>X3;<EjUKIqD zr4Wo|#uk+(>;-~U9gQigj}Q$J>1Dy^@i25<bn5*EcU#A~C{DEA@MZhg$V7>mX4KmI z0A*SE-n}y!9$i{=5GmK|^@3TDpnP4p<hATZN0+EVa}<vUFg->c-K4b@$DwBW14O(6 zT!~4*l%}}t5%>3rBx3mki)jfpsbxH?Z7g!P!&x%}kI)JL+mH`1eWWUe54YOp7u~uQ z48wDVq`xZhanW(n$2FqCMa{z{B~H%OV)IB-X>p}NFR&iwP~ZP=zWnqS3^P!9*6);l zAi$GL;!plc-uAd-@vF$OfEgJ|8ia2K4+%pId`_hxL;z|b3!nn6Ft3=!2LVuOP?viU zUWU1R9b$_-C5;N2=1B$=6^uMQvPF>{y`g5G?6PJG`{2dD()55U0%ypsizx-{Z^(4G z>TX(>lDW2^EF^mTH1^8rnt+DO#`nn$2e*dBU|}jFDCUp^br$rUn_!$Hk<LVgjq`eT zeZdUJ3VvlN=1&J4fyxhi3`!HOb=MB=>iJ#aE^>@fDeTdrga<!JO}Yh$4HrsJDtSDn z)tmHq{Ug#RO-5@QbTnr_+mqUHug!MF{#_o?{bg|=kENnUrREEdLvI)vJ6o>A;1@cH z4<zdOvNuD|<<)j(^HM*aFhVXrgk^7p;2Q_sCYyQ(`T3_!-fpYelgC}Q_2Cc1gn4gO z#?wgO7`P)C*>QI8_CKrdld68V6h^IusXuD}kirI&oO@K-=nRK`7ky*l*KKYcE$`{p zyu=d0D58tqn9=BXf7OD~)3$j8b$4=jI?4E||HD06Dq3G56%#>4(cX4rFi4UhD_A)h z)e-4CW@ttM(9_R_nl`WsF;z`}zo(%;SRJ_u6W8vL0)yC4+#m!HCIRvo=kqEE@3uZK zZN{FNm@$acd@H6R{wR}S8C*|-!Dn`2xB2<@Yc17U68>qp144-g@_7X?!fQ7OP1!Eg zTIlcF6Wp7B&yB<<`>LwU$Is&d^W5C4A2Z9!ZT}5XbQ{J|t%qrizwpTybVPT-T+WPn zO&%*%LOYB-yj4uM*^qR&GVegI!AKS#T5Ltz=!$19R9Z86G?UB!RJY*eg(1BWHrsQ| z^6;%3c0Q=sHzSZx_}O_!wD$jZCHOyY831R1HESF1FR0{*43yj*OyWJvSOW^i3H*rJ z#l5{AGYP@6;#NE608~WL6fnY&q;~8`3^l?#^VfBCd(*0a-?zaYI;Btbg!zRpCf;)j zVlZ1zGx#l~kFPjq4K?EnqdG|$_;+N(D;VEw4`wH`zjN{s=<L;l(BThje{4_WdiuC} z^xVHF{Cz7R-2T++RsWRY7?YC*@4IRe&|u_!tz$d*?&<0LoJ3APjjSlm4nLD^wyOM_ zSLLt2;|?!;K6B4rco45nJYL$x4J)94WO*GpIX;eMXk&hVIF!}Ig+g)ZkO0&$6y(Gr zCTIpz5JK>C;A3@g2lUm%>wfAUbs&Z)HBwoQ;W@25CZHb$fDa`>u=X?aK&(aJ4*~3y z;{qf*R*)+5KWMQfJ?^$6o|pen(6h&&w`lg1QgA5A;Tbu3#a;y78tJ0o4u_6c3=JtK zbG7G9`i+z2?a3^~A{%(lg(nB~D%h6j6Cwv=3QL*TzNgJw(!V`%10V*GP+#E9#}s43 zF3Q6T?&T~V=7859o}NTCUT4!A?<vKimDUTx;qaX98u#w9Rve*wrPh(fclA%VrfkAy zFa_FTO(wczJ8ms=<NyU}4!_y@yr)OHkz@m&EuH`FKTopC%<6y}-A8>ToI$DDSAvUz zna_3u=8cVwJ1Q#iJfqgpI=Xa#AObawWC%bCD>iw(yq^To9oeYgyDtPKlmMi1i3PZr z0fDJI7*Jlx#=ff1G(!W1MAeyj3k<(5q&}qrA*O9RA#e%Wf8Ig7nF~-+?fe}XNH#bQ zBvum$$G0J2N*+=KnA6IAsz>uV5QpPHm>BycvmH&6CN<zxZhyY`jnQKYvL}}lN{u%x z`z0iukV_B{6B*5-y&NElszxm}$kYZ4Nd;z>22A@W3jOkx!oaUp((vhzPk$YrB22cd z3J5B6DVPx}62~y=pEbmw0Xg5Hef1N@y7s5DF<*wT*2v$=5(IFhE9~*$@+@N0r`xBC z!qNZqxqqbQn3Vyix{qTYX2tRtb0sRDMsLVP>c3tSm3*fvY)DmG2YT5PBw79`GTUsh z^%}D{d!88soV}^ds1+^F+EA%d*7~69`bbbf12ltI%@S3TmWIds6;zz#9pZW<kk%<2 zVf0Olfb2Rd`7#*GwPDau2;aF6gY2jeO=I_M*!2em6@~OjX|BNtw89jz^|Q)eCe{U$ z065nd&0sR;*fwmZQI(j61%=(U441R-pB~X{f)=Wh28de`#HH|<p}Jk?0ZQ|Hba=g5 z>YHX|&ZDJlzsTZJ-jH6FB)N+{J(aJ%J~r<~MWZ%yrjqs|O~TQ81~d?BID0?~uex&l z{?sH{8q%<owJCq3BhfIAtYCm_lr^*T^=WDB@5}o7E8W5+|H5at*v_tP&B<Kj^vB@8 zm0Q?Tq<dP`_ZFJ}U4UE#CgdcL5Qv{s1(AR<mkr;GaeA5ov3<6{29yk1QE|)^0n#AF z5Cr|AC(KAkly~W4et{U;1d%zz#ZlG1!h1LNZby6m#goh8erPxawfJzhgR9u|36|Ft zNiixUiTA-sfw}@Pl+}nOqjp<<CMV*2pI<_za$qb)$!8%3Q%*3dlOv@XJX`6?hzT_c z+5;;wIB|RD+>XSdt*UY?`0Ht$s{O9kscRR#R(G3-S^F+P2U=m#{_f4wQ=}O4WA{Xt z@6aJx4ui2D0a?m2b<28*G<AQkxv)nVAPc4trsMYOc?yTZ;b=P8E}zNJ-?KmY!skLl z8>WwXw3vXKCjA=x^XcgipRd%`>^F#PN0ZJA8y#@PxA4GO@$rUj>YmND$L*zIGwK1Y z$fX3U-azfXsS@u%X9R#y8VV`qD2i!OAfVrckHIVf{oDAVxjJ$^GIbYGyO}MulJX~5 zt>lsM`+X$|Y$WCpQhCBirs6@n46TB_?ookb5lOD5izhywaMp$-AU<|6N%-PyHtccQ z(!%~@8vR@=i<s(`OJPg#L~Kx54GZ77%53g-I`;zVZTalsTBf_QZ{ZyUH(?`Ww^8NQ z6npW4Mg09Z8Xac^MXwe&E3b+|@zX4<HLK30t=X+8IN7<pl0-sLLTbWnV8jPy!Z(Me z=+`~aASG$_K03w}F9{7yUo6o7!slMfic1;wl<W}>_wb6RCHi`ufF?aH{p>pkuc7~6 z?fmcQm;!UDG)*(Tsqz<kXd?8kRf`re2Ah&s;oum6B`%Kv4oo+I#^?r+k&!Hs5!;W; z+j8hXKj^`yR|pgzyWpC%^8O8+GgYkl$>au{76u`y%KiwepzSoQ{>k2x7Ti-q*Ch=k zMMXL*!KbE*>lI~pbg~a8A(Lwcqqc)T(dSl@WolT?WXNXNif`~Di%c|P6vEhsstzb5 zqVMifl`MJm1YmV=x90n#r++2!VH3(yjto0Q8DdXI7MGdiDv>rNp`$RdJr(6x)%X&) zuoZA>9RrEc%{=<4s+qq1iJzCUFKAj6z2X$RFo0@f>4nd;z5u3;y0z-qrG_u{?Wtn_ zTD&I(f}^#ITv+s+78+_MQ87fbVBWB9;<c4$y&q*3`0U`j3+BiGQDvNwHTIa{qXB|? z;9(^SDZxzCU_c}TK7!qfBnp$jy1uy!dpD`KXAAmBbK%lw2Fyq$V}@)TR<t56y9{VL zyob3dsB}x3(c8?5)Go*)W&SNGdf|}>&xDOa1?i<E5z2tOE?FGUx$D2|xHiFqh+sOz zhKgAeHYHAr(jr|!1glK0lnlf+{7Vd;r@&xUzCS~hjZ)|++_<_BWM{3F1$gn&YAu~= zyGg5VYX!3;AK;!!I;SNrn$6*<=1c!thW;TZ74oJz?OX+Q{;~fM=mY*%sH!yRJ<7gd ze@08gz3B^|C%I6V0N~W{;fCYulRy_!k(FZ7kkv)7jr8o1%<CtOV04Wt)LB0Mmr|ld z=M2P@r2}D{QLxj1)N@JE5%H*pC|=npy;@%%0ikCYA*J~Y<*+vxma*h%=RK?ct$D)f znzjX5bp`2?)pR~7kfqI4<NN2m#aB$JYr=qTw_4W_`L~SFSoCQMzD{q+;>%#;Lx=Zp zRHHD@l~~yu4nJ%wJjZ|hY_iZW%EHovGgJmw3^Gtl+cLFK<)&$nr(zRV!P=!zbly`c zv)I|C={DXO{(G~3Ihb@mGh<_C+|K#&cjkP(2YbDwHK}Vlm2#D9fCzeK=1^Uiy+F=p zI`#m;UenZ%`+$IdJL>h=yQ*scj%&mYR?T%(%(Tc~_?(Haaq(cEf^tuXA0MW-E(JYI z`K4fsV*{SUG`#;?U)T~QT;?F4WzVrYmbxtqtGQ6dJ7IbA3T=ztAxi~T3w@9o2%mzm z3Roh%Rv09X@_M<Z`zp&A6*C08djDeFQTL%%KUT@wuuvagGq9Vh7_Ojh$^2?Yc!ORU zolA-I2yFRhMhvBrLreaMx*U@@BL>%FY3SrD>AW!0rKwXhH&?F)KTEtNBtp?M)$QKL zuq-^hJgpifO#875S&kG!Mlix*<HO^#p>yiTtH|Waq;Iok#P<z7yN=`EzveT~6d4&d zzMJlorr0!aA6jv44IGp*!n2g4{lGdib4=@ZaKXwPijgX57L1P0$Y2LCRQP2q!m9kj z=SI?xi*W8}R-}?mI{oK!wwIC70AqED^sF6v2`&J@86v<>8pS%D2+hn;M?s4qZO!CB z?G3NsCnW$@2moV12!!|uIMschbQGdxl8KQf>k>O1#4>4W@NRWU^LkHN4nvsy;ZX`? zSuVcm`kbjY3g{N}rRQ)Qj%=8<F~QX#Ib~t$DqV*bd$DxDqU_;&;1u*m?{7>iw~#OX zOq}1R=L4A){436Fcl%fNusRr;a+%DQ77g2!x<qF(sX6<VGBkaN-C*x$9=!!wc7?0V z2_#Df%<uL!d+0H=TW(z2P{b+bn|U%*Wi@RW&T~0)c_$b|wgrqb8to=TtLS$<wOSaR zdmUnqA{rQn=SOj!Ta`h0iny&qF1YdkTVJv<f`0&)OAiN*Lq`fhczX#77z2lVU_3R| zq@19@<5zKM^z-UwHs(_vKF2QJ(_nLRY_`iRLdp=uVq|$-MC@Y}ASe<4GmWUO$K{5r zy<R+da`OI<T@n##`>9vO_`Shkx$W(f#Ia2UHYROG5~)Ha2sLou>Rh0kbn#GrRWrQX zaI>)Bej5Cxz&<cRFx8YXEi@HU#9DCY_i2v}IKGCD$dzy=5E-M<zfG%OGel@@H9>+s zF)7^puA|T;spHGX^6!l5-2!O^0xT;b1HQtqVHFL523%}XNKW|-aC6_P?<O<_^qyFT zfjyb%GoOmoAAP*%Q&EbOYM=c;drlBK&3%Fx=+#bgBtOP4wncsCRu&LA?3$_g?f$~& zRti(_3U~?<U(F_B|Hoshey|~sqsU8}FNz-*PtA-R0|g>N8-n;)l7-AUIe`M)<}&zK z*d&75C_ouzPjqX@7`@Hw`nc`>!%HUXkkXD;^k_E&vzp$b3nC>}paO9oiJ>@DUU;g2 zx{isIR@7;W;h2uJ4u(IVPF#u=60&W1fL&s#wK*24u>Y<>utkfLt-|9fRnrM$P;Y_* z{c6p{&=6%VFXB*co3@|bK$t@SZf9<Ho;;?6fvuv6hJ;ML?#o9JLrfGK72#~;!2^SW z6?!2_mNe#_LHpb2@Stgr7$x<VQQ)y4GaLSF%y61g9INd3o^sz&BIpYoLhrzWF(gAf zEzooMU2e11An-jqfzDdr(Y_px-j^3XkJ8WiXd|afU=LxR>E^Dx#AyQszw-?Z<&T0J zHRL)Nkf6d4By^tG<UlRld@=>wWSX}_lB6{Vp-L8E_*kzMqOl3<)FdzK4<2b$k0v@y zm~&}66XC*wlwpfVheY|Q^g<vazGXAb3W5l)X-rHxZ7ay2xSjlV5NwY1h-q$<Dvy7a zsvMvBQ|X*T+xx_|%r|j2q$#Y<Y(h~(>#<4|dX8}^it{XG2C1&e=u$GO&l`1}h1I)i zvXjug(9ba@dgb|@P%-avtsJ7UXD7f64hOC|>u_OV9qY`*zNqri8-taw3v#=Rg&dt_ zH%XAq#DqcW&T30UrvPUThuU7M`j95?Sf$%}>dZ-$wPsS9b|aaJsxl+2RK?ou|L{o> zyy9A=@`EScDT!$Hdv@4cW%YT^N*}&uR43P=Q@k-$SRk2>V-EXa;F;;@JRycKM8{`h zyCx{VE`@#E21*y7VmW8%9Q7!?Es8nx(vrfmMDD>MJ1;W+Mn{7M3OldGFlD#9;%vRg z&6UcGw?_Eq115ZH7=|t|wF^={R!Yg<SqmS1Xo=J<qM>IhHgi)Ng4JY>28b8h&(ZjU zLPfQRQLg#YnbTlJsUwPH6AdXEKXk0vOoI=rGlOs}YJL|~$3uVylisZIcb$Zv?B8mL zm~zO=Xfs;ey^~TeMUoVgw@;iTApMgjq7-RDPIq7wz$v9xS*lsmT=pl|Y3vjvx-yNm zh0K5zf#?wZxubD6@ozaVO(G`pfA2FN<)j6V0sYHH1&SLI9$xxR=fXnKW6~wD=_uH6 zWef1A>LfExpR);c0Id`D*uO>XM24ZJREEJFKp<v9&s-R}nl6>&Qc!OF>n*>Kpb|k+ z|Flvflf5rJD0Qhg#DaaATC<)^CDObw%poB911Y;ke#Hk#nvN|wE|yI?-I>Ew->h-w z9HzMU$knBW6cyTxdKmYC9mWz`b?Y#FM@wA_+11%=Jx@i~up!Tuu11QrzCtUFiumb) zB-jG!B&v)Xex9V7wZ!gYx9j^8Y{hwN+#O8@FlKjfniih}HgRKj*%h<w=Y~px@MJm- zcd0C`ZEHYH64Lf(AdcesXN#4KA@V-aQr2q6fhQ(^FCwshH{!rmDdS=tyKLl3{ailh zTb-dl^Jz0Hv@hO~D|P+*T@|=WU~%q#gcC<V@A(%88A;ycCOYbWhyDLur1Jmb7XWD5 zb(}7?h5VCa7p~1$c-BemZ?S#yvW2H+e)sowKldeUXeA}%1i&+j&<Oejh6N{rw1j3# zL_Yrxv?_(_q!goZ^^*@D&cWu19C*3x3mjSdR)Eyfs3dLj&7BMG0e7cJqG$cr7o8ov zvIW0coD8E&c7Z}SH+4-R{N=<q49$`uswZwsElKc_7e3L9(Z0+kTq=dE=%6=DxF6}i zCKSqFu>=5&JJIF`(0_<gMNo+-l5N{9e=NEHg~lK3*^e{5*835nNV_EaD@xG%&way@ z>`3-I(@4)ITK##(&O^zV;xqz9zXaM54NmmZab`lJQ6-_-&kxcxEFpKYjYWU@(v6$U zZSUCkqA^HI{BuzrV^PaAWo+(kOcF&a0^I3LO0mH-x;_x%8JzK}`Ze5Z){#U3u%76o z<z1}02btW1wg}ioTbn9AJnO(yU%9~-g3`w|G^sD_HDXWpPA>__GFq$)EIw%PP~*kW zA?oR%;-sNtsDx6HBwu}iKn)Gu%cVo;`!mj5X2a%h%qm8pIMH}b-Dbhf@yPf-CWyBq z*JD3FD7L)td6FIYuYM?C6mcWNIH_{lQ&{owa!5^$&OH6iwC2;+)9WyXU8T*~?{B|{ zV1%!{v*L<M7;iV>3zsv|szBTH*WRA$iYfK@I1uhhqjIoSOID{Pra^@{c;T6fRu-zE zIiflOZ7<xzm;RcE3^>!tt+f1s-<Qj_OXq9sPaLZii<cHbZ6z4@s<=p_0wrY_WvJpU zGE&JorL>2lkq`<oD>xQ9VInSs6a5<=dbv3s7L}|lSu%i9Hjr8!-;(Da+7==15%5}` z7BD3eS6($&Oe6b6hzgWa6x$<^2-y#DBlRhE!E7N17Ksz<6A>h=1ppT<N-xg!)#!b! zp#s#D=IZCOH)A_I(WFvIg%^cHn|&b!1$_F3egElmZ)KGQ&j8Q<y;#5k7{PGkn%GR! z{DyeK^Yto;B;?jdGPY9tGfW4N!^F;vCdn(2leiMu<7vK6Y>GLy(XoBX`PP|}URFbj zxv;TZeu!As*1l9Knc(@YrpPpF#m-9Ls2C`L#X;`g(<q#I{+0V5ptkk{sG7O8?Qkqd zo&`7Lx}>o{aY=<41;5+Ju+my2G<4X>wUe?#gN>Czn1OEq^)1q!UxS&Rp(wm!0oDrE z=qu!MXZVg$suM0ZrW~(&fDm=*W-em!Rxb2gAu>)pQY0BY8d5%wRe-qq{S1?fv*L(! zf$LP{Rm=~IzRFA^mk5&7(a`>6lsQ}!K9~?I8YZ2B1tZW=cBS#S?Q`B(nsjo=G8?}_ zKzxhpOZ}Wl8VUZP?q6DSn&7C%pN{X7qEB_Kx}s0a>?W37){ra#w}QwsEr^%QqIwPq zG&{p!r&0cr&#BAJjIZn1NNiKG4JB1bF-GP~cufjHcHEs9T#87*FA1(K`LHD{K7ZO2 z^qgueq*2@?sp*Oq8eHe8GTzjR@G`6DffeH*MK`9k6ua`+$V-87?YuIW%PhwMpB~+* zC)uHASXlGuW~H519J?xT-X2tI)Y+_2sNdz%h0-bP{KX|3Z?e@gN8r^ZhR@7Epq`AY z$1r~PUk0KpRd&llLP(jcs8iO2ilV{l<a^}5zJyCZ7fNeDseUt2aWqyrx(%ZGq?~aQ z$Rlv04v5@<3$OB8Byg6fu%OW#Y7Wv$&KVfN_|G5mPRbDmxcD$@<ciThNtmX=g|7oA zo~@?^6e!@9k_iHcNBbL@!>h?KI<h|-3+(9kMaS&UxyTRvtVk2uTneq8Od4`8Okqs! z7J8lMP8&x`6iI}UwdgLn2@}#EUvk0cB2<#M6HtMoW^$>r5RA<uX13P+Q2qq@b~hmj znuAMimSp2@sSp=Nwlsu``lYz}Mf1-e5*NLPooDPOX)$S%i035aOTp%BuN4(srtwmq z7<acN|6J#WJ$QxDmb5`qj^0C6Wo_ZeJbz4tmwYSwbVb<qwR{GuQfGoN_!ocXeIpVp zq$dW&1uu>Hez7oyGG^*1dJSxr{*BI}DAlZKC}h=aGWI^OP|Lka4Ju62w|g_TUE-io zH}9PBQa}IXkp&+()w}`Dt|bMuc)-Du!k#f(*>^!)D!Cqo{TM_#y1F!Rj8ZptWtezv zWM)9%K45QJM3P|1fWTjCz=j54hCMi0wZ%-nYtpNnhM|efGF+SiV<v4}vW_b9(?18K z9(*X*Qu6E%kiJew*;K?S>01JX@G@PPRZ$r*(gArUHNv>g;=6Op?_<wRr=9St0&0Hl zX#zcZ1uYh=s``k)nVnB{vWN#o6ol7fH7(q1+_-t<LFV7+K(-@LJPC5-+>}qXmqqE@ z)U1=(?v%qixBg_~YO0OX1T-kzZg^7#cnE0-B8cLFn=<?wsk~vu!AFMUwTP~JM93VU zAn+FX!}%{EO>OQ-_4pO~u7U;du)u^QL<&y}iQpGL=Wn;4^Sd*bo(C+%AXV!Er-{zG z`?#82TM*(eru!j})PCPzOaLRi+p7M;U8~CqzRK*7m~pbJC?T8>q=;bhhAAc>t2DX= z4NK2(4`yVfEuysHkcUZVyhlzr#ewj+g4b+wNvG57-55vEhXR{5bvz^95y*lw)uSX> zmDPrmw3+l>x;ve1!j|J%h5G?=D?=VSh9Nd}f|)jvM1SZ?e|W#HI#a@65aN32k@vhl z6o3q^DOkTZL!`zPr_BsQK|7^mr=Cuw>sb#rNcZb2=;r{E1AriG09T&`XCT0S2t6~b zfHM}l&clGEEl4|L1l-4#REd;)=gz6<lE^TYEwx`!3O7PUGFL1XCK(L~Y|r$u{=VuV zl_B`T$4c7+=8Tln18@dlI5q8KBw&Kg{PN{GvubQ>Bgu4+pb6al4I4dYZ_Frgg*4>` zy{f9pY&32JxjviO{HPODDstx9`Ra&;&B9>(O89&a<s|%PrC-zOw%dwt>_vyI2gfRF z+_MIKu8!K_+fRmzc6&zj<X1$G@!e2tue(*s{7T&<h_Qvt+h6UH$nmoD87ixPEP%E% z5xFTVWp+lNusXHoZkT8EQr_irv-BU0RT-XeS2Cx4b-i;xSNXti{^#SbwAUDA@7bqC z&xB5hB3y^4qK4iwY^rL<A%-6tl&-n1gz0oUXg9akj=vZB0Q$!ISaWP?5r?SGs+rEK z4u$rU3Ja&=i)da{4<%5bS{_*T&%7G!$iS69!WDSoLoeO}bJ1Dt0qjHRS7PZ5qQWcQ zpr(j5qR5GGD+#iBS}*SCl;w}VPXA{8Q?5>GN&mRq<;IQO4fLL7R0h>`wfUlw5=kg- zVw}2F5k-mN<cRxNGHtsqJWWRF=US|Ncb;=iSz5Y}iuL3z+M%LjWvyk6ox@ww(xD$m zB13P<KRorHRjzy>Icv-_s$}Z!KCLNAZrf!YyKLrY`KNiKJiB|VkgKS)z}auG-2S=g z_r{OIHnFF!g{E<~<<q~{t?z}!&WF7>#+EdX(#uXJRozbct|wf^pWdG=rJH<8kyH>0 z7vmIS9}5hPu?lI+LQ_ilmm%zI)g=$nVp<Ly*p*?=SaVS(@XE9|EnpfSOfqvDmJ?qG z@Mwm~4**~I@QNbCf<AA3GeQCc$7H6l>^pxHj0gs$DIUp~Yuq~fx+NK_`1i;A>s+m< zB{X;NU>2#Gu4z}iAzQN<<tAqRPGmR0^iO3I!|Etqm4h)v`Hj{}y+cO=#X;5A_{@XX zi=f3dTe6{c|C4PQp*Am6McIFsC)+B{XEd9OmU(Z5oF?_6PgL_tSL!@-cT;q?p>L*I zX?GZ+CJyfThvI%N9bw%*t$E$mUvP8m936{XK_xQXJL8s}9phRPXx`cKyp^MxWx~Le zE=fe=WW_~6J3xQp+_RkF5IZYIB?SVI+w-l^nhRLWZVNNR(ajNA_t(+4LyM6^jJL2Y z#kfU^WkT#`;f<c(c4OB;xE841j1yBqAu7vevak0FdS3Xbt4PB<xPF?r@WuE3WF9|w zC&QN^O$21Oq$VxVugBgrAr8U<Ge`tsefwc)l2vznIQzJ8PB*fFLA>_Wc<hfR6P0iY z5N*)#^K2N(eRrj0{{-b%XBdecuN$~AH=6#zIZ?{bbEj3mz{$9gh4k1K3ZY=+m{1#x z3C5Yp-G36oxC~+<Ko0VvV;s*;MoWp{t|op%@QP)XI~u`dRS=n2^cSq#wvG99HBr<s z3hXr*`Hd_pT4`bkXI#tu7Z_DO*Ve}0ks3^=gwIH|D|s+k?~$93rP%6B{n;a@j%d_+ zB6zC7>7|Q)N{lM2uRUTw4zu*+Btss!1AYJ(w^IZlw99(zNKN1FrtPHLypcZ<KEqTv z4Ng~b!0P!gAB0^>nJ)%+cG)(P4x1T4x2qrlg1wLf^3{+3O^)ZK`$R`XGm>muJh-H{ z(=#A792ELSoq3r?T$3g$9QD<S+y~e1R)4b+6|04a5be0#Mn?*oTnA0cDSv{QgyL6) z{BzjTb4`}wT)fH>{D}=AvOhcY7vPD-l5t2MsHEjpN)U5vY7(Z+)XeGZ$Wg@Q1u*5f zU;xz`9|+bj6C+QSA-v=?z42H9Fn&}#yP{$C18dy}!aArsueA}YQ!L(cx$VW~Xm99y zA4u?37200T&i6RohgYv;wC?lSgV5``w$Sav$2mLKuC;Uk{9pjr=hxuv>|BV!jzzxv z;W^Vi(SgbNF7+21=h$|z@QNC3+N*@{PgwGj7;x<(b=sHu;pScaSp@Y}8AW^qe1T%0 z-&0&<?9^<#MGgjb2qp~9YU@pB>%gzc0tnbawh<w$iF9NbXECH!G09}i8v~^}q!{VA zawGd23nT1YlJONakADo2$Se-sIVMO{OQe+uXUzS%lD2>vQIQ2!4c{n|CQ>{GqoK1s z2{90f0kOd)*Q+GMI9x+X(SF%p<LQ;V*f&i!W8}mmPK-Q%b-W#R%dU#pgGx8S%~my7 zboQ#bNW^3aBZvrQ#z}_5DLL&bn&8G~i$UM9(~?JLf*4Kj7#+0fB-5E;gqBiM!gR#V zN{#q}FO(8qKBz%?LRd{1zOkSf1*XacSsio*<^a1sg6q_@=1i{ly!>1@H(>ez4?(U{ zF3t*cJ{c{77e3DXbf=ktO0%S7pk-3&130`k5x+3>l~R1V6uly=WHg$5*@(XflRj+M zX~TA)qEIwZTR?W4H7Y9djr7ojcXq0zT1PX+L<s@E?KK^4);tgIZp)IQ=C6M`^WZM} zLkHuPyqo!`uQST3pz)0_UEfZ^i5K{KN)&^b^)Z_QOMf3di<lwtd!Z<DVrjbe8zx!7 zarqtE4__=|_u*Q5i`E@dxKoX(Iu9uv{1@hX4WmgCuY>z7=%uPvSk@D2*x9fIf3MZO z4q(yiy>qNpJh~cPZs9sBg|db8G4VizKce>!Hf%Q4pijMWZ%xELnr37Bv($;uaav*$ zaQpe5oQ2u<Ny?Dx;cev9{cmMr<;G5HA++SSRrIKXDSxRKJ|(aLSQ>RdR>N7deqLL5 zTs>Q!^p>2xZhmeq6E_T2T+(#+4NtjH8X<=DD1wt^fMo@(?i`GZI~nX;g!Rs#!qCH0 zSn8c`d3pJ7_b{jBF@$9>PS3hs6VOfE;A}9zrml#IX6OK{@tnFWevU{)i>kb*hNcD8 zObP~g5p@<eFReQEsUId_1)#n`cQ<I<JZ(yM7Q!Z;P@w1B-c%@pEM3s+Gm+O<8VkDG zrg)S_=*NaEE;6meSHzL>SY4Z+zc+AdymY#S8F{p>@buAM9<6s;aJN^@Z8J6{M(CzS z)4wjo7{2l;Y8?Ai>`_5QnqLg*o)<vJI3|dRSI?AQE=;jU!%OX!9Q3Vqpzx?o-}sDo z^4eg!*})p3|H5ZNFoi4a*}o2=|5wCT&m_7WQf)ua_o+GgW_169tJU$%PohJ#k9XR# zZpoN7bpFGiC)dgxbOUxwes;RQ_vB@6`@tp&{rtpmaW8$b_nd{ojRt(8`_znCr|ZF` zE%#2HaA~Z_GrLQ*1C#Wp{R!KXORk)k6oHHJV}I3S1a}(H83O~qAg4KPX5X`Y;>##r zIurt0(<}LoJP?6n;i9F$LR*kar6Y_ZLqPUu!s1pd2FfG~H%wC?)~a*MuEA28R%Uej zp2}SexV3X<pVDDz1Ji)T=HW9-BXcgG=+sh5`H(SA-t$b%08)~9Y?(nm*<?kTg0u8+ z<bt7S?8RJ=z;KQ78%uSf0=8IM#spJSo8*p>2R$gueEom<M1Q1nU}56@%QXfFo?&R1 za>;{m64*RWtz|0%-q3G+^rQhG5e?JRE`93Z&A_&OFhoI3ieIyJoYbA%gX?r^O9+%c zZr?xMj;g;zW)}AO){lvQQZ!+wcf<;<0NYajNS$KqAt#m(=}hpuB)$t2a`v;+=)f4v zJmyQU*FT=!oHORkl?eZHP|W%urI{pV#iPS~_f}b0wTQ9=Y{W_aZHm$y+o*~t_Iru8 zfLzr2z)=oMC|PbVpl?)~=RxWl!N9f0LO3Kzaj9ZRItn5kncJ`-N}Im0H@dJC!&Z-R zs&ZK|;jPD_I~$eiQ!>`&58PK0XqIK{8-$NxV)OO<M2^Qlbf(a+9BvGAQN`or3^#He z6am8re$zzqywuMhDQy^hy?@pyw2v<pPJjqW<P&$Z9kic*pKmlLlR7Dg$i>eDv_b<Y zVjFh&TQv!tM{K@#*DHVPFt&B^r)`GKwVlvIs!NuY)}$OLAQ^_MUNS@MGQ0^TD8_85 zbWa+~yCo%xhD~dge%77Wbr`!_(bH;7zsAyS7IjvGB(e>UQJ5y~Yo_9TfaK<n`QY%8 zpk39O1(nU7J4LorDnDo9)n>*LQQ4j-&MWSWrt#b;cT`x#B<U$8vQ@Vf3+-UXS>r?G zi~Rm7>bSL0E!So@$f=1jnQ>*0Z3sxnH<jw?tv^!J>tibi-~#Y$MUkT91yqFb)tB*A z(M!ECs6k$$CgOCX45gf-0uQPfawKSDoVLlOTU?S<U1l$Q9_7GXe$V~qL~z`u0?*m; zsGHk@w4SFzQ;vRyf@a&Ka<YNsh}E5=^<X}z3a+n_p$b$|!OmGi#T=yA2-S@Q^>oSy z8e9E|!N9-)8y+A=WtUYKNFxsC6rw1w6fD~!<9YYUh9GE3o(nXh%X6t7Hz4!bDQY3B zcW4enUK_|$1M#%gO~_5Zul_C@2S<zZS3<;)BsGD`R<8<6lpI3$vbu>Q<b{l?7UEiV znHPX$Rg|wie>9J$Ray-yu^R?dsu>vSh=^29+mGfgx}~-_xH)|jLEJeo*4*Fp!61PO zdnW?Ij%+7fapm^x!AV%S5gG;9sS=gqkNvImw(+T1t=@Hu#!hA2_>G=A2KtIzv5Pu~ zlq8<d11IelK9|y9*o*&jPQ56%tY7Vih>Cc51?=3I)~Fmc0?TRJ!T$13WIm~^bkyp1 z<x=5<LX-(<W<>{ILS)gLLtdjsF_ZPEny+s}1{D+(;e-X52Ny8064G<hfclVK`Vm3^ ztn0{VxIy6(`)TQ6ZQ&%`Ei+}dRBN`?^1Nye7#!bNjs$hZxDLeFd<hK;sfEy87P$!= zxR$>0LLk&7<F4p|cp(*$=tTo~?<R3d6bEYz@!tHk#`O~qt<ktx8Y6aI{pFI4+v(*0 zmQO@MZb!~0_Y2Pq;Ury?w5?(~nYsk>u4LTXsxaRjznz-F`|v{_lYw~qaqy{4Ru-Bw zhefwm7J&m6e~k@$yQzNjgJ>G~eVn1c7iqy@YPuuGfBW;b3?Y|3`le}O`3EXiJ3>_) zt9ROsS=DdR?*{%H>d~lYXP+w_35I-5rNSe^$d=9?qPM5;Hnb(JY(d|6cMub1b4`-v z!eyP1Xe%Ygn^0Yh>UdC0-C+-hqnd<(38||fN)h5TbSlg(1#&E?mu@?w?-5pf^zfpU zqZn4X2ge<UFKUAnGbD#CH9f(fLX7yks~@DhElHMfXM9O*AfFrDnF!}phmEQ>hnn}M zxP_B{se=k+CrDcS$qCJUS882@qBVu%Efu%;Gj||0&TOr<r%#?i(S26msCDLS{&46F z9|?-+m8RBlYtW9CT8LkkM3K<1X%{|Q(3^`)B!51j^GOnUYaUTw`|wB4JX=)3`AIi~ zp5=KY@Ym+}zxt8%BU8rQ&2#b>U8y#+wMso*uVlRr*2n**yYx5DVKXoWae^=;bYKE+ zB6sF^#G7LxlDG!C@C_-`yI-9i^EsM1RAj;>XbCDw3gp}XZOAsm$W^8$_*eo*3i?S( zG5ZLZN@nC|dsdic6DTt{8<@;3jCmFNdtQ~#a;1zIk(~btrh|^Vxl%G_QG{nypS4o7 z)lCkEyU%8RL`Z^80uW)864bvkvJa<?Nbt7=t)qD@1n2?5D2mj|G8NqWJW0Gg2taJg zRN_V!Z3O$~@MyAQwxQDMT-KA3x$)m_pcDls%jf-%qT@ooXg0a^FTuQgZiX*XxYGzN z_vJ5kpM41J8H-87zfpSc?_H|M!-az1lk1u8`%a7|cwhKD7=vM8#9J;~qHA(V`>OWD zKcC(vjacuDsY^@9L@NMWo*mM{<BUu4F>@pZqhM99uyqhR%HFL1_z}U5nfTkl;W}sF zg#I3OyE*&g_lA9X_v!iG&H~TxuWDlvGRxV_8dl-5yB<TdH8e6rI`_IuD^{I~b?X&} z7v{6tF2`R=JIC;X1OOma>;yub3Agl;aSCkYDBB+;qsktd1@lu%6R%LGCME<C5w3ok z(}~5Q#mh-qkqN3>Qyl!zwYkJXgT|JTaaOQStZ%5pL&XtXB28r1N#`69XD~SFkS$b$ zAEZ<Jlqz?d=3eVI?YHiB+;Zy~{Bw9`tw3IgTuYCHq94}dSsgklBd;akNwET?hY~u) zNKE`t#QV`j`obs6#Dnjd57aT38bKu73W3Qyj)X`-SQfuUL&2@7EpYnRzxj;!rHN5( z<Vvb2<#yM1o&G9{$70FJiE@^n#!qF*S}I+Me=&9<WoVwoo3Cy0Mv**IrD}84A))-T z_g~)iw-S}7381@dw2uM0eihxeeXqeo9m%cmpK2N)dxIXXZ}?B@`r-2%p{9J*r77J` zPV0k`+6mGxx_k5o!CM})w|~1x%7u5lnm$sKwU@6~m_H<(W@WXxeJ;10f`gf@i^ZyG z%rvHXW@s{aubsbqDkz7nyq%gzGZ3VHZ(SerWld6t!oT8L7jc~rnYDr)o40FjaEHL0 z`&$5zQ$OQ34fjV$ECwvD4Eg@R4GB%ODO}@iQD}u(@5Z|L3m<OrPxnT;&;D%4y)>5S z0Du+o*%q2cf?uyriN<lcgpgLcd#3*Wa6%i+{_RnSB<+_u=<-~-2CD(l`H@om(8!s= z{`!UND=is$f*>=Q1nzHAlz-f-#t>b+@&CM??Pw(390`pf5HNQQlI^jp#(>{ka;n+Q zfm&QDkxKgON!;c<<=fhv7L2V9fzw6!%vZ&{3M(VB%{!CEA9t61dJT#fAVK;yC9^zZ z44%(x$SD+86RdQsubs(b(K%-S|Iu}paZUc;|EIgVVKk#tMhRnsjT|u=gwdt6fQWR1 zj4tUMAuT1+-6g5gtx|r#{G;E;@8<u`ZtlkOI_F&1dB0!hb<|$+kUSdsgh0R;HT2$# z6ejHQdiKi_(t+*U=$0P7eU;rqHS?r|pFj)?typbuC`PX;)5arDqCfK3TmzQVnkYLO zg~5>3n>vzcM&+z->z2@Ar-pqJP!IbFl^DqS%SRp3qY#L+?89o^JWEN~vxrT5yDk0L zG%@VA4BpFR@KNb?`V}3$YiDp_z99B)yx>nso-}DKVp6GR?EC)#Lmgh1N2FT~OedNi zi`u6I{P#^ZbLLm?)}+-ho9>3gDE+kC5t*X(4(pm5k@dmn`wvkIlTxq(x0bQa?f;bD zo23plFpf{$NDKzLGaEO9bUCw!_f~elu1H!*Ho@U9{uK3GR<xyj33%sw9(?QkOySN~ z@#g{SGT-OlKQdM%_6!5gwZ8IP2%)Fc8C(zf?n2oH*(Baf)$IqkncY8Y94szLyn8)8 zp_LRsMrzT>`jO-|kDm=`PVSvn1^_}-W{{c8<8Psr+^tW0D(-)rg-IhI>-;LB^2jVh z(iaH%$43t$7iW~;hsF5lYt~5jWNR@o41?)K{CGGsbOPj2H;7lD?$c&wdFSC}fA(hn zc+88*fvA&8(oKUI!>f3c=Z8LuEPAH%PVr@{#Cy@6^s7|umsCkMWgR<elh3ip?XtU5 z9l%UhUV{@k#20ia#s~gC=newJ#y&5uMr*OjiyvQwqCU6bg_q1J#T{aUAN`PivN0%_ zIw3LmNU*+6_7y+n#Cy~f+JEGW(fBdYW`;6zPW|HjW0b5iO_t1mb_wdI=t}*E`2!;+ zH=9>KEOI9lwWt2uavN%9P17iQu)o+=YG8G0xSM4$@ylhXP}AvJoiwwq`#MY1eBY{D zya*d<yfu-SH|U}lLqxR}1c7UvC-F1Gi19xo>i+c??2tO{*Q~jH*a&<<d|WYAE+MYV z_AuOcXIVjaVJ4fjiHqz^EE6WTJtZFzQDgpVrjPf2Xq>{om_6V1HAb%tlA9Rj8e24k zq^-mpbe)eBf|5-N_n2|Dy~?sK?;8NQJDX`)I`aBQ!MyGj=`!ydB7HVbRWIQw>mMql zrHu?bktf;Ku?cA`-c4D~$|y-w{Z^@+UFs~yWRbJ3W%Gbtq;$>4cVd+j_4u;m$4XNe zKF;*twC^iFP8x0s?kj)xD~h4%v}*Pxt7v6^eeaCD`>ygsK;5@T$`cAb2cQ?elVV>2 zM4qsAz0MYDS+4z47-awAdByjSv65dNJk8=s-@;Mj#uyDj-wT}3Xj3iLSi8OdB4wi@ z9Ovcmtm^gO_+fw8st_JN8;Ip!{q`sO(DKv&dUgXpG^u#M`C=<CjA|45z*D@!YhE<y z<{j%K&tUy7I1g+!7w)f2ez}y#!}<25Q$PhTOglQnFoY_D*9wm}kP(IR2$!sbgMz20 zH1KI@^0okgo5d!_IwcEmX$>7GXqLBH#N!*=_I}Q{!IW`Ed{u&Xp^d{r)#>2m(VSba zQ1X!IRjHGwhk<ZvZDS>M))^_VytI(zcOJKJ+TBBA@tn{s)ToD(XK1WrUZcy!+peW> z;uJQw{z$k5<~)LDOFfqSOnJmD^7+ON1^3R^U2u48$4iS}%DK1(A+%iF2|Re$YLvSH z^M_VSrjU@4ji+Z%mgDX&zuzBR*e$*hT`|op7yRCR>;3&-en~>^{p)O>tyzDBmc)NE zR{ADWYWoW&kT~zs7^k3<sVL*DNA<EJ@>i6;s<x)bfQ2d6l6-s)&@eX)vk(Eni7RA1 zAK8(xS{8;4n6X8CAjm<I%@xa#50C_xH0G=s6S@2V6JeJ+QKnAQO5aZNOwcazy2#lF z$$*U9M5Xvr(m$NE^bPCektf{kU{;nKj^tPL%C0{s<bEuw*)i(~UR6jQ1w4nA8?gwP z#HPmx9FJ1p0{Jot>7=yBIZ&elld-QAOkYaVJ^Ja$zGzNYx4NWGGwNt}e)N6ik!SWv zg#*LyY@XKx?T5-hEDgYA7u$sUg9>`bbC<c$!iusFO6yUVCLd{X3q?Sp-2<)73!V$S zIaANR{U@UL<B8SZ_>odDR*1lQHf^=5;TPN3aI!igdVIqzI*bf%Z(4M`Au&?)QZk|F z59H<F$u%77*lhSviUKmRrR1C)`n1{PgZr`Av?Bsy<KSLmPd>^C-|UXX!83HCe(lta zLzvw=za5Kxm`N`b`&i>uYa%@NpL^9=@)y(ch{v3m`YAM;w1=S<=+<Rn8;o)C@_rmw z94YL+3fvDkk5Ms+DIX$c%5bNB;FTaCU8qpRUspL;@{F~*{TMp@@%zfeRN&!^^fizx zx*j=-??15Un80b*Ar_(#_p=$l{i7pqkf!&ENQFXuZ_{R(s8dhI+MkHoMxg!=onl{e z{ZcLJ760_5i_~9_OkGBv%9Y#shC~$`I!ihc?w)t-mwrB*t6~4k$5IxLAo`R2w12!t zpI4Ux6%qY29Q@-Kf`ew`l1X^GB!O4@eI{~31$s{t6B+mh58BGYcX6h7XD(-(zL-tV z>ff$pnM6Nk{es`d8~!!iF%~AQr=T{gpo2?3bgDPldm?>ytDE_OwHxC8dE@Q7anE;O zU+ZRv?mjZ@W`22p@DR(dAST*eqBkfXCEwqY&MwD?>r*HPkmQXTF(g{I2ONKp5gTOH zZwuZ$?;_FCS`iXiYd_e6KD)`-<*om+{KRPgvo{@9pd@WQPG8`tfIT@Zga)T|Q!y5( zmhqK)S9n^XO&&m+)l%;fUe(`QgkTQqAsOVwUI$TCw}_ZYBOhTWdc1)RNf^h{@TR>y zkuHi6W*Imn{{R!N@+SYwCr?qaIsVheB0KNJWei}fbz(A)t%hboNy)3uG>w2#5Z08j zsH_rQvftuL?bk8I7><*1Erv7`OBXrObX9)@@Ikg8C#?Tw6<6-=p8D_GNzaqlSTPUg zok72{Z^rV>ZjGfJsSJgfqC)Moy-NpT$e3xVKjL8txox<R`)G>Aujj<;PuBI%ZT`%% z24v_?nG}QzgFJzV^=g*Tl6mQfXsx(}XnA+<|00mWGrbEJZ^A|kcyYEfh)GPfD&n-u z78mlFH4jC!9KM|P<&xbrGJ_Xm?o}VMc9R3F2|bcFk10!Ocr<YjU}I!6X;1jIMN9|u zYoo+KmTo``YJfQ&dO~|}#G<s(v4@m51RQ0o5oB18nk-4pjg9k9`^%?KY4Ss~>H*Cr z5D9_HU}#KGwKaCeM@Wwt+pF}6t>3$x2Gj8BB~;e#+A}n#x64Umk@HQ|ryac-jkbTL z5(E@rS~4@bDX1=m-?5mU3-2hL3{M2+iZ?<+lsQd&7g@8eb0enTlBqAL`+!_=ZSzI_ z!kER9T7}YVq>b|d;j2oV+RD`06_UVJ5%feNqB&F6CCm`JV0O#&n>H2NnL5^cP%NO% zX}D8oCsXYG*LTCc$ys}3rv0z2UX;G~_;b?UyjeM8{v>C2cE#V*QCGKUzsEn@Kg;|n zyG8MflH(4ir)JBaFGepa2J(%!erZ*_clC6RmJ9rCJ*i+}nf2!G37hw?tIL%~Da~7= zVrRR5zE8gUbw7Cd=l<uO&xT@pyGPmm_u-#YwxB;-yV?O+69Z;IZnkHaqZkAeBfVr{ zGgFQQ`r;+5Jclm|9;w>IgpK7InIpyY2e%~bmj9(7AQ~oK>q>-$jm4}uMcyycKSie= z&Z6x@gXfB&a30>9cd85L&Bh7~SA85}aG{l+&M6hOt3fB*dMzaJY3U<Dypo^}UKCXX z;5l(kS#DrWG*fOIa~heL@p3C>GW7W%NqA`&o!DozC?(BH;>aW&<4lr$>J?)oo@j!d zjLRtVz@r)q&fZFH3VIeI5kw0<vD8Il2kQlkZTg(2W`R8RWQ}1AfKiWMoY`&vgQTkJ z_AVBISWpVK_F|(8tIwKc&xbL)P6xx<l2Ds%ahFVZb*Lg^#~`2#>o1=_&N>PrfWlQQ zEN22<Ism&=s<-MQ4?YPtIiO;fQ6kl(%;&XZ;5%{MJG^%!R*#J%tJ<7;eDVaO&S6|G zQ0*HXHgX?Qd@P*kNHQvkKx(5b?Eb;8v>yGYmIKAtw<BjCTAaUZPdtA7OEjM*At=Rg zYGEm-xV)eqMVQkxtzF~9e<&bR9M$KN^M=aXp`=T?;;Sn6fqi@mf~3+5XOLn3jslN5 zJc@7`2Sx?Pqe@jJkns8mBXC1^^eruFM3NJz4!77cQSfGPM6o87n&Fe;km(V#Fy+;K zcJP|Gc6!Y0YuqFjho|EwOSgC?k4+ncGf=F<7AG;1twU!#I@CXSkj`PdP287AHiJ#k zz@dhPRf!`TNvAVKXO`OXmye&>l!C|dy??JAmdD!&)v-gGT%1Ufv`lE2y57cDRTf7< zez6GWLG5Au&76(v?{)i{xKf!O_6H<PB6$ua(!*F>sa{k;RAXKps<&!*r96^^gCd2} zoOM&SW8mzyTIUNJp~4!zjP|bs!(ug|OVXkV>P=N7%C~>Mw5ELzf(DsN{MkcEIt^ZU z_9be$SX-gi^JgrnDst05FYeykT>-sHUu51TRoiF2HZ*gh^z4M7OclGHKA&?<G3wNg z!ifENZeCX<;;3)KUVGbb++26{{zFYlgGIWABmIw*p#6GZql^_rt~NN{Lx$C;=BwK? zsHh^fmVzEIF5ADW!Yf%F2^5sBRf7=N%z$6ZiJZuqd0JxH))re>oc#Xs;gOBvP9<Co z#MU{#d9kK7(Gi|4@$ywg5^gG<?Y-YRPBCeOOamzIqf+b2tb4?-1oFh5{=&M4Gmh#@ zdrlgz9<w<wTfdv5Jqb~C1q$W*pxUyxiLa|A01GZAVGT=9UcUP?^R|{@`MUDm{amB{ zy4>N=ZG8*LBi50&i-*rYKJ9wBYqBx&y8g$LXV=pY@;?Tyyj2lRIBA`E^S1>2`Savu z(XU=ki;fWhIGRMp>eN5_lZCRc`0a2@pz9%49EnT$CeCiLk5@K~oA|M1#2yvYhdD1w zGTYVV9qIUN4C|2s-FxTEXfIw`8Jlqri@g}A5F0J15Nly*EU%Ee+^A+pBZZQTl7XDt z*%bp7(h$~3G=U^&bsO@V$)Nx8u@#$QNwOI*kd*5@vtZD|wlgqBMV9;txnLZ955#nP zZh;pQFg5(?-rjygdB~q>m<nlYQGLeIH|<9B-hTC7-kK62rI_~Y=6grVOHA#Hv-hMH z*V8Y8m&$<2GU;$RXr3j<vM)YZ9bdzDoW_^K8kZt(kdKs>FFKuc5EmP&4jk~Myx&?m z?BrHi$7GA=;^o^HE_F1RY{wlLey^CfV@k3eQ(?kM!&T*yEob5)u1FP>HcPB-)+*#9 z5K0_bak##5M9AzD7|$R6;H7Dc>T-YmL!m`DVPQN4jw5*)O_`USd=#P-U74^CaJe1p zGU6}DB6PC)=KjJz<0beRhiOW^aIn+Lht`&mKj-y~PE*aN@|_+QU!10E{_=UFnD|=) z>y$L3VeQoaA<&1nrunoW*Xl4YnB#Z*;wR%XavMP%)GLafQHp!R@zc;~{Q#!(E74WB z#cCSXz=q7fcbK%8q=dx(U;K>6+KcW~p6bb2qSSkg^$D1~CpZXGCuBmujNo=*l4&Qv zBV`I=%IIv1k@+#$I7uB)`0CRz9U8(ygo-9p0OO$K3+l+$e~b>-GmusuUX+`P_C-|Z z_djDWGesri`fPX~WGdDfBvQVnkmcQY-&p%Z1pWx6B;@#hrsdhAz%8B=5%Ky%&{nAH zTNLiudW+$Mp7i^%EcLiuWt9*z$hQV*v;0DG*E$PwjxS6jJ6R-Qz=sWYU^Csnd^)5q zze}`P24dIYJ0epVq0gO>UwVIT{_5-6yJNw|aw5R9J#p~E)*!|4a(W!0fv0xl^q8lT zsh`3Y!FIsWw{Dw4s9L%&VT+qih(*nUM+Fnhz=;oErOF`-GXu{Jmf)wFlcp~Hsg>0! zBaU$4&bb)LQN==d6^|20jJt%1&wTf7u3IEEm<z*T%}hEXg*K3Vm@sNCo)UjTNy(?L za8Bdt)u!qh?=2crl(8FBxip^W7SitbU=uos(z+vIawl8#Ivq=f^3#mBY3RKJGXp=W zR3<DoyY&!H_4zy(q0cdv?*r1D_V}G1@c<}zMeyDRXiZR*N~U7rxZ-N?HQ-Sif)r^2 z9zSdbh4jKjUU0z4Y(h6P&E{2e{>@+faYzFF950;1qR5#8;7!&LNvp9~nZKOs7EQj% z%~Cg7=s3L-iLID^T%pX@xlXJzLSMzdC+*$Ef+6N-QK{$4mqNj@38<v8`E3bQb{+=V z*&~+CJ$ju;(;$L8a$K<MUZ=WJq;PrpOmLPpup31>>+^f*eu>cyUl&|d>CH<^@mLg$ zsS%xJbNmiImq={0F59YtO@&W|H2k=XMaD%IM}{4)8YaO-IiCUn;?-Bj1T97|;y|g` zLD{dwMr^qJeI4Qrcrc1NSO;z;<zVmGOlSfnA@zd+;^^A+)L^g^8Tkv6C}5c0^_Ja} zfj1Cc!xA2R+^LcQEO*&>#jm&)SzY2+NgL#LGTc*5gC<O~Sebq2;dLUyasT!=y|AE@ za(s91Pg1&&u^+^K@ED>rBSYarLiMSp!`R*5tQM1(+?MP0XzUq5`+wadbwUYL1avm3 z2GRuNJlI_QP}^*$$veDplvIuG>+g-a4G4$otl~y2BH2_c2U%=d!z{L#IINiBxB%g( zLXpo}6|EMvLiLZcfrQ`NjaIuzw*5r>C2%OnB*Of;?*?;a-q>WNSYnR=eDVnIMqcYb zu4ilb&#n8x=actOcAYFI7B~O%HruBeIDQ>BW*fEmWasOU+?eI*YuvbE`vGhK^2O<s z*2;)IjJ32baHbvO7|M~Nuk)qN&2ZB7$<=+iXL}>GfQ(cYEvdp$HGn6b@tX<YF-?CF zVo_y9YEjr0n|%s;2eS=x5u`!cUnKm^FCqCoek<I~RNQD)HEdOw1d}b7#ell&(sm|( zHAbIXaRs_$CBvU4nys^XCuN~Ju7`3850_FU*Dzbkrj2VES{G8YC8Q1k8;%`OHcq7D zqwo~t&@66x>1A?IT-MkI95T^W9@~LPnD3~_*6FFF@`AyxFRouo*B1Iif}%>E4EjsG z5#6-*0aO~l(1AuR*}!nhy*Q0fdvqm18ki=ZE=43=Z;Jf#=Z{Ik(uqGn+tYffP2Abp z*&XB0Yt{GNnQfv1Ka5}3i|9yA_UrcixmsinVK;@c;3_XA$Zt48r$cD~i`1}F`@j~z z5{H{DB$3FW;?%rKg9~^~_bOB0x^j!ZvQzhkN#h{N=KIk6wg=f$My8dCD}HwW{Qb;i zc)mNBtQiaiuWYg+&J63iO%>n$ke{g6;^I#z7;YgA=Qa<uApuhC-X)+?>xH&4PE6;2 zFg02xz!$&JqO)o&noM%m_RwWMNB7=0w@V8VLA0^l+8lPcQ3Jbbsa}dc6Brbd&3l%d z^4E(^ffvW@JrX;4gJS}W{7lVu#l#=lu*3M6Ppr8r(Mf_aAE}s1_I!}&x+=}V4CYKZ zC$M~yhmw-SCn7u=s>CS~O)f~7>VX_BmiidAbzPy!Z;8X+&B1f0;oHwuo~C=hMD^Bc zQTf5=R?jl?tZGi7xf4^)acr-V@pZ#O4zo82n-jm~eD*%<&R+H9>Dfl%T?!@gH{a-L zh)8s~ZyZ|lGiWBJH4Z)eeYkzq`^)lg{NyS45g?3r45rJctaV4q;`zTtE^v_p;LL;3 zfP@i5_tw{op5JfJZ%YOC26>axcWMe~Ij9B283MzXyxknH9>4wg?$4ti4~hpgY3!M} z^Ks99YVn9BCS(|3mzF8r8x-Kj=qfZwnRzUGMl#+j)Ji3>i}#^C6gmB4h;XVos(t|P zD>gzVs|0%f92lJxISn>ZaM-Slv3B$~dbZh$q+c_<NkPtuPI9+TE-1dm2dk&dyyq~M z^suX@Ybc#2rQ!}MxW24QY_m_<c?0GIvr+H4PBgEa^e8f(ClAr$$yc$)RyRB=|ERQa z`DNVW^TRRra|t+u<QQAO8W{3T3cspz<?K(9gV_d~R_uW~i>i-A>ZfiJa@kjfF6G^S z`3!<&q>OcT=QcZPlD9qxcjZH>A)>MrQPd>bqx*Vg)z+^L9!R)^D#=q){~d5bBJs*f zcB&xa`RK}$LaaGxy-zqlp|SP8SgiS^yTX(aF?U91!_Fq7199T*o_brop&~US`)%<H z_KBtk*1O*84q5rc){iHEqg%I+sdVv9_+pJhY?|zb4Ch68h+b|GuLbDsnzx!QS#3PI z#?d^XmYntu=K<J+KK)NGq)|JbsIPJD`z{XF-63B1Tq-Xsg?+Bl*`1x;o>$-br7Mh$ zU!_DcT$=uJu>Aw(>OYo%i}$}~7ypbR>&ox1&^?3OzvRELn>HJMz<c&9_P7E(QhPxE z`tAO6<PevC73;_k+QCvYEzn{Dk#99%;^|*Lr}yWp9y*hpA@v2P{_3lw9XBs=l{@cx zqH2<&B#8tWEmR`z{XXd|82wSo>*wYay!^gcGsrhX{NKe1xildTsFKJE;)jVRGCM}C zxe4iY<I?f@Efz7@CcB-sOpFl8>Zuz^SRmCDj2<LuW`ohxr&$)^EpBDzSGI{N`1&=? z7Afe1M@E;HULf{zSy5iz=IaXl2P*4?uQL71TAH|F(y`IwKoMOJ5BJRAA~I3P{Ewj@ z>K!W~9VUY>ooCssQe%FJmfOB5?KNNH5T=jYO>YKVqe5bfE&@Wbe^k%3<Hx%fU=TT4 z9%K0~&ZhM>AFAmSKKy)9O~YXOjLl2M&Y-F2nfk|GDWZEPHN;%_id}U)^*aB_1-Ajy zzxl5}FggW~p=X?kBabZp;gT?#CI>`0&QGk{{Pf}^QdJ%&*I=39q(uHvvCK0JH3`Gj z9guVU&0|ol`+q`Q;98F<pjm?3r{6BN>gkj^&^lpVlhz-sL!cR+ZwGJXYSX6|3dO`c z#+Pq=_gN-a^<Y{w6Ibq43uTu2^OAne+{CEyM{)vj1BU-)H$CyrA>_hTPmf9&=JgGz z2|ktO>2=2bJuse{)Y{u`24WzFMoFcfX#<-V4u65eOE&}4YL*LRa>m1XSy^*ArGW=G z*N~?tfg=0ESE$^}wu{h#nkx$&=1@;(Lv}K}a?h%5AfvWx07G;cY)h|pb$WUHa8_-h zS}|%BO|zG~u7&&=z_8ozN{ga7`Xf&{GW@Uqx`4{u>o=Zp*73^v`2ZZ-h#$;@oFHuY zb5EIh*IdPX_zK`btX|-u3@-qL`Co?RX&<)H5_Uz`k3?~?Nd*+rHkTO2Yc)1Dhs$#^ zhx?WaNe&DOVHa}|5yi#|PDJ*l=8RMwZA*h3ew8~43#o;rDo{pi?THjnk}^g=Veh$9 zn!XYwqhaC9PTTg{mfg)cN!Ob!2PoYLIoQZlyV?!hJn6@Y>Q)=zTXyK>5wdVQ(`Q!6 zWh+wh@@9!n@k#QoXkrS#QsAUJd0IFT3>2vdY-pCoAED`q%3l=q`pk#``vdOh&IehU z?7m;)&QE>SUs=u=YPp=U6B3y>9847DZ(y6ja}lw=Xs=M_kOF2@{CpuQ4V)VsFZ`Qd zCl5muJc{i(L;TfqH)qPUx{#8!9BljhOdb+VQ^nlo+F+3h)*KXW?6e@lmC9I}Ni{9# z55|76pp(BmD3>P8o{bCixfP8~Y}TXQYuPb3IDIU4BU_+ay`F|m2N&K+ELA6;>%G$e z=q<kO;Xf3tDVzNPcRACwq$0xO=7*Oy6PPRfx;GGwYpgEQ(F{~-K6=QA`<ZfodoD&> zN~nQyFjXQ%Cz@>BO;<q}R!|Rgnv_{bdOv5)HtT`XWG=6ybz}mvT4IdcbY>Y1*sT-g zd-i{8p}lrZC`w&_n13^%L7J3}*Q9S$Z_(&z>2vgj><3Tggte+m`PQedP__uv#LssN z+iWUGCFQjS5e$|3F7NV>OAf6M3cShw%jXZIK+f~i3R4d<!^Ho6CBfQ}U}r#0vXQe$ zeH{|hK1Jsu&vh_LR@XBzh?_H29W^nmHlUF;X2TO@C^KIwok~TWoF5mFh<nrpk*U-m z!s>~JOkqJ>@L(Q5ltY9Qn}T05zg(CzxOB+LMP!62$saEM)Zl0G5C>eli1DQ=O%C(@ zD6PV)y13K=LLEN~q1w?>$kAaieyETm#Uj!HfM7J<9B^b+%?42E@=@CJfV~#OJqKT2 zopKo-V9sT3RbDU+2aQ?A`qUK`egpmYx92a)9(QBXkbL*2C|M6bT2M1Z1{{5wCgd(< zJ2$=~A{f=0izr+YnWs95+8<#7Xn3`iKI6+bXBiN2JRNv;N!#9#{x#{v#q9k*{pACl z`m2wkm5~H?&XnM<n`fd+%uRB~pRXV)d^PF03NgKrB8{v>j{97FWjqfEC0dFjc*qpV zsBx(y^j{KiO=gh~bxabG$s3Q2U_tlMy54;ZG7t_1d>7slI0KF(DVF9C?ubp8-EquN zuM;<c41xWp5iFcXnp+9nzU5zfQWkek6!h&SFm+nMul%_Zf`WkqVN*R^ozP<@tQ65P zH|AiDpxfFu^2XJ9Ul^Dso^m7{xzHF5W|}Cst7)S5*~+b2Yn+lZO=CG1y`v|c`LWCK z22`N==1*_x>|JLNH35@q`0>53QQB)2T;Ci&HYgdFzp_a4msiSp?FV9dLOF|YYMXtx zIPgbhnr<;)KE7RLMoaXM)-UeQ|MK|`xmIw%T1!8B!v(3MZqSIz#NXYu=6iR?J4a{k z4AGR^Ot_OobA;aSI(MeV3W;P{Viu)Oa`6QQ_Gd|TQzY0&aG4{7;Al2Rtf{o=gW&Sn z#FfSgx_P#^ATZke+7U8HMvdlj<|~#*97TO&Vc3<I(B=8QJtpwH<gJ%>aB+>>6eO%7 zPC|GhSww|7UP`jp!`aPod;!hIdmRi7)QNzQ7wZx?2IRs@ADd;ra{3-I$STgpSb!cn zn$k&0lOI<xU9(BhNOS+gO4rM0y|yHK?z#Er$4EshM5p?sv;e4(Q(<>ligy0keFGet z$9MwNbkOCiD3NuR>a#=Te3!gKPB?vXvnxF~bsQJn*fr}T!~4aBCXS4q|1Y0A^|Sl^ z=8KDQjxt&N-0lk*uDLUaP7>JQ*hu`UfbH_XlFT1*%zyiO?Ja&@tN9cI_Uk0NKsIAL zJF?_oo*L0{@VK}>Ea1YR%?R6FGeh2m>WxpBt%fdm)_6Eh+F^kZ8;fjOh<6eP8BU+u z@<S|cgsQTQ#IB&XP~)Whw;Btu@7~={9$V*OQAY!MVl{rTQgjzq?_-X>@-Q|iA5nu2 zm$w&)PMq2Q>^JWCD6WUhE2)XFS_!T7!o{NEO%00*KYyo+@}eV7Okqqqa3y=QQCVqe zunBOqZjG-&8+<plSXu(d2qK1^=1KwTY;IE{iPS6w2c_pNX>v_6&1IK!B|6lZBE+<u z;L}2@DqbU`6txx(N<`!zpZ~x;3J|i5MFii~w{`va5-Ri3161=q;&#fs7NUbo$*gc_ zG=8LP#1qI&mP5)GPNBUMSutVKgjWfGk_&$;UcFc0;6L+H|D|8R&wV9JKs7?Ornipj zvWC$`0<eb}7($6-9^q`GGh)YQqfh|s^nJ%PBYWzpR@g(cn&(kfTl$tc)l%}ozNY0C z7xG~a+>CI5;BQeS;aBaS#rP@J6A+Dx4&6?8sz1Fs5iiBY{Tday-r2V}px<I6@%k`a z#y!@O!t`X2PZ>@jFkp5<EI$<O?cgVtm6180pJuAFcDm9~>G7sX%`wgtAn5--QD>g` z`bd4(VY@O`0%(d)*ETXGw!0`>VZTb2EZQO}wo+Jx8b%76^Ut;Zul_hFaNX-Ot@L;$ zP$qh292xY{yw3)Detxw{wW#5?_&-;G0X2%a@F`pw|G1@Na^;?poXDstvSJt2$TS%| z*aRUdKDMAR$jtdSUYp^Hfnb|Ru&ij_(_AA@tWJGjY{xJv#%{NneAc%@7`Qma`(z<k za+;;-78*O=X08lIH3KdUs~879QONN;pQUVBn|k6(VPTpvLt5Y6&6brBe`^SUq8v-f zLcxYyo6m@w*kKrM_Wi7J(T&OK{y-0y?w5~a!H{(^?{WmB9krI!su23pLrdMjj>`=v z0^ag8OifMm3yinEI3Lv)S9aLn`r<SWNkfD7KD?RHfj3Ma>Ln#56}~q2PHb8HQ}k9O z=lZ6Fzp7#UFQ03*DFr%-dp^d^N~K(AH6GuOb=*2|<Fw2nKBNy(L?t;xfq{fibM7L= zfuuv8nhI)OEg}YZr)m_xA6|Ar@@(RuExUSj)~3RgY+^aajB=4s+9G}k&8JN=U%^aM zXML_HEHi!eK+SVOIlz<QJ-lqV!bT^XSo$tPk-0P)R5T?RrxPc@v$&70mrPw!s*9Re zEiCWRu=9b`nlNRU5><sa*&c}zgh3j8tl9N8j2qLCX6T58o9O7+{SG4{UY0n%VEI_m z{gxd!`$ahhLiG-EB`IT+B`h?P`so^CJ*{~dlb7^!-+G0fRw?9-W?F7gRo2oDje+B{ z5s8ix&=xvw40^>A<#f1bC0#-BOGwn{GrfBFjkk5_zxdG*0bP^quJ+7_G(P@rqc`ZD zo^8QFsk?Bob2wjvxz#PkNO%jD5a(SGjq06aoybnRd{S^#VgHu*6I%H-S&M()AN8Nm zK7vkRyU(YZTj2}TK+wmoNq$cy#pu^b<EV;VO>?w-MLJ>Rz#q1s#>K-;$M(<<2_B*< zF*Aqsz#><1{<Wi+x~VK*)v8UTv$V)K-4(@}IbEDKn>p(iG}J{!v7uZHFZl$K&%VFy zDOT+BYA5=N?y8!7=oLofrs?>cCL08hscUn+uO(3iPBoAQp;RtqM8pC}oITTgGP6+0 z+Cj20)YeH=JU7b{C@c-28`p#?#O@7YUM7_(D~>$^8BptI@+wQET|X`X$Te3APR8sl zUiPE^>7VbQexR>J8*6L{Z|%05)r2J3;lYP^+r$csf{1)i%{A}mj`TqmG^wbp9DG8) zugn%+T)t}NVm~f_E`@zkScn6nd?b;7a*|ByzDzi8kytp-um{##q8FfLx&(EhI0;?N zKc>t4BdfSvdLY*+b!OxlqBg!%se0jdL5AGaN)0`j)~>Hv4r_-g1bpu)C|fTEW%J&; zuLd^@GY|oebx@oXdmEIZj5sas9*ho`bSFDnO@T5LrLr0_ObRA;hW&k=rztn84^#q+ ze5Ehcj!TT&y^duw!-)gKc;xTyJcYa^Ziu~LHuUhWEy3LQ@kHJk6%`r?NsZxGSiv*k ztCc!Nys)vUZU@OdKWZ)wRO$s$piD%4F7HrB-9LYlZ;&O>C#*%z5I2>H*fR>rEFp3w zQ_xoYD?P1cJ@H)Rh52@$Blv{KU35I4vW4NCw^Fz3^bND9i;p&Q&NBz<@sO?u!l4xZ zjnS_)%ydI)C}p*y@S2OyJPi0BXT^N|<1XR+$Oc|vPCuqq!R=u1h(tQg+n+fB2kXXI zEE~q?{C!aLW3Tl@iKl&>k#3xrrjg(nymS>EgZ}W-O+`8@vAptx{glGA2+*VbkY;os zf4A@Rg=#|xAXdLsM~>YDzpx<5SjA2{C0v`L8g7)SC6@M4CacyVx}}9DK=WFFI+;)d ze@CmuS8Cxsi3iab^nxUTJc*QSVptrz6(zJiV3fwfgQvms=|Ta-d7c=B^QoW>56TZ> zUj6n+`fq;SK%zjOh$dHmx_w&qb4BNnnAUMoj$gvnx3@jcnD-z<aSq#`0(eZ=e9LB+ zx(Pq9qkW$5#T1&6uqF;~-=boOqM)vfzv!j^qYCHd-H+zEfi4*C;!^$G8w>S<mza(0 z;o7=rh15U^s(4rI+a!K&`tTE;q{Ln)QIkbgWn(<F!UV*ah@H6M1L<_!YDshmUD`PJ zktpq{5P5kn%5PxsLt`-is+VwD7F)85x$6DRfmT@4bnew+&M0&w^^3F_r3&~%;l-Lg z(DW$OYVqNFK9aYsCo#TqvIRu!^4X84@~zwJq)H}kzc5&rNQmuAH(x<y>y5{%AZ|#O z`u?}$9zT7WWot~fr0UCCOy0%?3GfJ6W~F-gw)Thq<?{^+xsRVoPPW&)bDT1@bYt_^ zckvuW8r+B*>9k22AKc&|&xNxAcQtI`W`d>wQGGQEBOh6EwYA~xqX5WV$9z{Q-0WEO zsBz3+g`vyT3?R_SBVy=%anx=b5bqNFlv$K0udX;EE2XG}Aqqu(IY9c^>^NNO3wO%i z)jNKUEWC8;<Aq7Pexc@ex}<vkTyIEP)d;qNa!Ry9y*y|hdTy!#t36v1@j_GRFIVsF z)RN$J#p;NjL}%UYiO-@AG>|6Lx-ROZ&(gYZy|E3H;-V!0a|{7){gb$3O_Z4}mw_`m z%T3f(ym;1`Rff+?()>xiA8c@=Pvj&zdQ&<q5~VHeur$F5wQk~P{75S<NptW-olRIx z%IhbHfA23S8|c&6B*$hs04kb{MyW|>qus`0x<JkKi5~pmq&H%(nz<~HQ|I(TcGqE! z3@)XqWX(oYVP}J4P?LC$oT5yH&m3{FxEO;iOD4QRj>mOCj~`Q%Sl)g?qKtUqf>sXR zK?J7466KvuB1(zFdsl3dG%K8aO2>4^%v^AUf`UP)qz5*3k5~M#qjXx^TeE{lpo6(( z>=I@n(_AMLKEvd4i}QX&hdc|z;ll!vodJ02MP>U~N*7wGpboq%aaJ-mXgIXm`{C<V zz{oyx88f0`-$BHS*>Y}VWgt?3`)vbqVcTns+A&RINoYcf<yBbw!R8mr43XX6)F0_C z9ozuG@XdHrt{s6gv1zjt8B#;^#;BH~5o!LnfBN~$fA!~{kAtT4ok_5;MUEK_zc8y& z8_?(J%44a;k(1BI$e^FsNe#;YtT6L%`Sz>ld#0im(S3^s3~)hDVi@FdTHid*-{c|j zu5=Pl9B(BVO(CxudN!j&wUex|2cq&6&b?cHlI&9%Giy{;P=N_hOehTSH~4Oe;;gCg z`uJFwhDv*YsJo0OzjB+(L5MCoUwyG@QU0yevV`_7Hz$Xm>p_SN=ef}JolbYQo4=(S zXglviy<?bLHR9g&`@5zsgvSy5@fE_9#DdYXv8C5oqLfowaJPIP+WcU{phN<s?A#+T z$C$09ektr-m0OTo)+od%oMnvhe*0>VK&o$Vlv!7yN$Eg?&JQ7}5%-$EdDuzcox14F z-}t$Jih%ahCOJZCAN53Zm7?GF(sZgj4sgtCHzY13FJb(L;ypRDODk#d<dzME-XM4_ zY1yPDi;t`kuaJ@wHHISzXd)F;m}?-1FtYZtW8w*D)*+%&*fnRL+;RJ=WsAk$YVM)2 zTeg<$06G&h?I!q=$v9t6QEXpInHbj8R-v(TY`ESnPk%p066A_f+PGog1%Gb~q8qbB zE3nR@G*x1E<Bg+$saLrcQCy?>R~Siur-#`ry!y)^D}kBv;q6|{juDth%_i~o?_<1t zGs<W>e-^<*d8U^zPatkn$2m;>zWrUZekv2;%B@xQPH_5|o-{+Shne`BblskPN}-Kj z8RcrKd8XJ*dnO)K#&4m}Yv|r;#u8ZH^Ow&Tm=0(YYr=+T!SI!#!1Ba_phyktr@R|& zcbcGWJJU*36>|NNRuyGyaNV`?9tLs3=ONq<tN7(*&4uJ*7OM2Q8^f^_smeCZTwLk~ z=@<Mi^_9Adf1Rp6`yNi5Lyv(<J6P~k=sj$83K)^Q@o+&~nSYLHe?7eM_z@4Pikm+Z zy_)T|&ni{Ihn<iR*?3?gHP?p_(~k<Ws#QShPDJd|y8V()e+tbXpD~G=kVt|Q?114J z8YSAo)Ky=oS6=)3&<U|=D5;=Jn@R_I)Zy0{D?p7l{$%W=q1E>>fAKcz9?1fz31RgZ zwBlv<m>@jkRVGX0)J-+5ps-9w)*GuikEWr~AY3L&F^klWw~>QSV>_&TzU&yu@cx_M z`SfrPbksPw`m@xdZWC~EVOX=OSYjs;8ubh=mE?gSFVQkLF#|G2$znN@BJSTJv4*gk zrrQ11vNg4`H1m+qnp~Hr%`on`#CRQyOj3-kpr=>dM7svkI_+YuhrlG7TUjD0*RL_p z(bG^uQVA_!EKr*@?;2%sKKymm4PLG*O}F>Jr*N(Jac)h-r`9-RIZZSP!;8$LAV2Ym zQbeaB`F>F9<0imKOW3?@0X+Er*7{^)AJ;yug7<N+Kn1VHFWD_JzXqRyOt;rkHUI(S zv^$B9T<oZsqlqG{FR(h68nF_kW4ESLJIV7bMDVf5#QO&m!V)e1@d&Lkl97YXc;TaY zS8Q;b?*^HHeFvQCGWZGj0e5X780BRDH-4_51)zPONe(4#*k|q&T6El`%=`=^f&4M< z`ANJ|UEt2AnHOUDD~KPGa)m%XDwz9)Rq7v7%6RqTa1$;U4Ld;@Sqgl@d&whnDb*=; zvUZ;;D_&_PcN_f%1&G4W8w4t1T=GdaSE%`^SsTSFMOPHoVEwwPNu9VZUnwTIz?Em; zV7W_a&ILbqzkl2Q-cBK+IPc=a)GtFzB148OF|BL6H0C43CZa7fni(53W9VXIZjuj| zCI#GDZBkV6FZo?R3H4S}-`LpN;!2idp(7+orrXQpLqijU^Y>kKO2KUTY<3Tn9~i~i zWs{}7()?lRwP(a+X>XnPI9Y(tt&M6kzH_F_PTgE2LjMht4Qvi^tIYSssew)SGXBdi zxYED+!>0a23^p`mzi*KsdNNV7Jn>Cq<bY|#S-&P1Q{?aGaiqa)Q%yY%J=F+atu<s) z^h>huCN4bu+rElo3n@IUV&_+_xqgD2#^?+Vp(3OBr|DF<jn7C~m25z;i&_d6fg6Rq zTFue9)L(X|w-q^^gj|+`Yap^-#b}<dCw@H10w`jMf&fCw0#8h~)@;F5?8_XCmMxpr zCoW@!ft@jAxg=|q*?cn#Gs{e!SgF}LTxL~%*=^~aeY4N?aO`|nl-U4g8*hhcUeTwr z^1(nu73h9s$e=GY#B!&!omxW~vLJor%FKQ>#@uWeyK>~pP0i|jmQqDWTd|xQTPLJt zWTgZd_^em;lG4ySh=ZB^)vJ=feAX02KxcFJ+HT=8!jOkbZQR$%88+g<W*I`3#OsW3 z3f1Nbvz8ZBFPOCdV<peQ9w8Z}PjjhZqFhQSy_U--+e}|DS<=vJCNs-8B=g=Jn~I_J z+6B`Ntq46R3l9+xhzM5bIW~7OlbTGRbLhwrkSb5j_p>fMvI82XnG9>yC!58xHxdDr z@8}(vLmlnx?9u+jLQ+hk!@oGHyQr3UCkzo)jbEHhqVqvL5&TYwQ5W&>uZ=4<)RCbB zB3%ZJ9$(zV#^{@FdkxZas?Tw#j)S9%3sWH5<l>OEugwA!KIiJ0l#(3TONK|NF6q>7 zGVvpRf{)T2N{kOaD8%<a6gwVFR!}gZlyb<?dPCVN(+8Dnm<^CQhP)ON<NM3!kKQ$C z32TwVf!Wthf=G)uSCz@wfJ4am1Pv?!BW{RQqZz~Dnh&jCq4>r4hU-2s?Z-nTX1~#_ z=V%i>!-ek@NhMukeg7Lq_xc}tyOJlVaCC}kgLd7eX>@L`!!Td&^JmDXoK1$)2_WQr zS>NJtz(!H8LEoUGorDb;%*TRFurAfP{1LvpL`$WG^1|Ec4Xrd-ojx;#^PRZhe|$W6 zmbDdz?;_mTzr4Hwwz;y6$XP8VtPp%_-aM3w1BaD-&BI;<6Xugee#-4AbO<LN#Ev4% z53CgmOdi!Q#onwvdb1+!@`|Th$|FrfR;?ld?0NDz)2ipuE-VLnY%UlvWK`ZjTx?a! zvj8`*-r^spSAG{9{^G3Ut=vET*sOdK=q_V$;U{$5E|vynT_!8Ac0dN}BDWiaFY^#5 zN78(cI3BBs+Oh}mj$1til&`kaA3Na@E4XW9$Iu*Uk<R(BAAZNC3N4#UOZM@ro+^tw zeZV?786PxI618V<`&cGt83vX$X)r2?YhT&-lM(I52&bz{)ypd`15ZnhbUfnQX^}QB zWJQ4%nQlIW>LH^Z95KVId7LBHe9Uu>nB1aH&Yha-Ln!Tj62SN6U=6t3Bz53OX+`%S z>X_6zdx)n*%gI=jSWhet-0=%sl%189Gy=<xf#Py!X7grA5ew!k*-$L+fk_T@#usz% z_fJx0pZKudehuqX0;KNrjlBTW!-{?CG2??nWo|V+VTup=l(Z^Pdkg>DzoWwjTD^}S zyh;klWt+`5XfrpoHk@t2M6_X?w=w3liA<i~%SGs=%kcTVIUvrCN=7B|!1{-B{gQvm zF$_kat~VCR$EF@7i8t3!g4Itxc8so6C>=Wf2z89wnP5ie$GT5ce|!kc$;_xpVimL6 zjV^coh)kk?FBPr2CFQu#%XpUIB`eI%VRBR;D%q;@?C^8<*iqpol3&5uK@SL}$|nZd zMR^rOX^N=y-5xetyc)q181t!ah&W+h$Rx2~t1E`fQ)gVdAZ;Rl46RZWYTt;~h+k}P zUktkQHaaD9e^v%1)f?oG9-^M-hAZHdbGsp?2fB5G;!n2K4<|F|JD&g_-Y;U!?{aYQ zO*FR+l=kyg0TdLlNd3*P4aFhQ5!QeiRY?%+j#}d4`nINID61f@u_u)OHJ2K1a!x>M zoS6oU2h1hK+{!f<h9H1_aFOCx!NTER09t)wz!%PxWTKrdZ5$g(>OWh56<4PrmOi;G zJ1u?VvG7Dpdmho@mKHKn;*=>ibr-*=P09y1KMF2_^?9(5rKg(42zHY}srR%jtxGge zBHeiuE?Uq1!pFZYSsJ+_gWK{>(S@6%Y#+@aE&Y%obAQy=c@5_W5AghFPsXB~=n$Dn zCy}zQ!;k#!%~hw)Ip*S>Ap3><i#?x`CTuodI$GY@=a5#5!VWql57p^G)`O2!-=b@# z<#%+so2n|2-HOF@TGvXw;vgwIoEk*FtAO0oQ@xD>W8%|Vi87Oa^+#h0w2hdsVaueP zTRq0FBDID^gV+RoNt29&S!}g#EM;5M;mbm#oc<;XG$Z8E^e_`Z{C>I8pvEvDanw6S zjVW6r0WDRks1<ljYgHsn5B-LmKd6w@Danc|oykb!_>}E8)?r;nHAwd7{+m%Az9QK# zG*t3%a&hl_RJ)!JGw~?icO+Aaegy@C2OpZy$c7C$w}f|Gmq|%K9&o`-TXEqNps5di zfEMunEjW9bt1$5zCC(pKz>>O5Vn6Lsu1wgIBr$6+<W@EpSN^L$<3K^}=v>RbAg&dC z4%@Mgn^Vm{Qg2pgZ&LCv(M9dlC?xsR8-53Ub!2zVIANzcg>bLJ2Z+Yx3TwD&?XTBa zk0OI4Zp_Nisxnl6`FztM`<Gutty&Glp9k9LYuI+20XpjrTo_CtamIJH08!u!eWu&K zN32AqI03c|#(?$CL`FJohq3%_gPENwKoX-`CG%|}fr6$~9&wCl0&z#XFL1jG6KZ+r zBfLVUxeyM!U$`2|gi{?-C!U&6Xbz;i#Mhea^C&H60b#*Ia-GYqUwf=}Fl}QiKN7N< z2Mjkv)@~|R7kW(}GIm+J99=gB5x#tGupJb=^Nl!hf&GS^b_wV+xU839K#SY&Upl$> zW@yz?RE%Xq(LJ5V1`!=u)(T$+d&U>+66jTH$?<UJzWQ!rT4rw0qn)Ra<fh^DDDW#9 zi$hT%>@3ID_N$j)rWC(^k!VJCA?{4GsggwqufS-cZZP04pH;>C@8{wHGqSsOj*y;? zi}%UJvTr?!KczvJD>ARV=JPm-gDQCi9%(KF_H^hvJHUIz%eUM>&rupBDVl)QMr%p} z!@F{&buG5(x_DgOppIST@opbwR;g)5{c-_ut`5nXx&Q(C$+GR%QNKFz@}Q-#2OD%O zyeE<P$%Z?HO0=-pbI0Yz-=Iy>?QwF|M>dU3m>3-RSRRCH6j13_(n#;jDk3rArDo-# zdSNiUO`$c@>eEE&NBS+BAV6;CQzq~H62BE~bfXr-ZZ<Gtuw;EAQGia07WFyKnS+Ap z$fN3%X&=Iwun6}-i&w19K0svf(IxR7NPf6@g|<=OK<VQ9buqQHlu_-YdZh>O{Xl*H zh0S==Iuc8O|MK~v<?z4wA<p8eb=zz`n&5jf%L6h_;>i}my*b8Q@JE*tckziFESv%v zOSgP=feU4ZWfOl_e?tHNvR)uF^7X#j@R-}GLTaY5-UtUuikl%s479KCUCiGwM^9*e z?vPcbDe-vL0*Q+-?|LQuhBN!uS=}#=q4SCY!1hizRmO-9l(EuPyKEOvYL=P=t~*~; z){NfVcIM}mqe$18B*;|MBP*g|QBkU+CN1?hk7J4QZ64NH-*=>!tiFCuy)%F3X6ee* z+W!+8oNKN$l)`XX*JyWSYDwy@qWIrqPV4LX#_8N{Y0C_k^`FV^J)JC{2NS`gZ(|Yb zIo(??E0fj!?Pna3GY0+2h%looI2A4}(`jR~Ikh?c{isD5ME&B#0a&6)Ut|k(s_9GZ ziqfJUih%R}B<968q|V?rXeh};l=gX83e&?Z&^g}&+g9C@Lkly>&8=UNpTR4QS<u|C zYYnXhcp?{-3ChIW3}gXQM{XR#7!>#@I5D?x2u2(%W9p>$07$Hr%jPOs;5COwG{J}E z<;X7Jp5rH-dKXWu=gvnXv=7$+E)OruD{koGUfA$gonYIwW^qq?5M+qO83t&Ws3*h9 z^~88Eu~RN?XCg~oqE~u~?zEPQ322)@o`tb*J2rjK?1<#3%7`1rj9ocOV!ATAKmt~a zM?`U2x=oiEp>l%eyDcpOG6*FHL~VhYU7oN=ob-b7|L!l~4xmrXlN@w4Y|CP)<dht8 zLWOM)N}hKHJOewy0s3}=d#$uK+-lpVq{KrHfL)yT!~JS@w^OnlBsdC9qHu>`4@K<Q zjuP-{itPN%O!yPerZmZAzC84C)(x&gkl80x6`X|+;?g05F*+Fl#|XSf3-F_LV^Ola zxf)6!QdfVa-1%y^JSjE{-Uaj}>XqXP%L1{UDeFk{5-)GRj5xIsGb^~dB{1nR1W3w1 zZ16%M(*vPaSI9f&?eApW(R;rhJ4Y<3*-s?KkfXu6R(Hs7lN&wtCW{`NTuq06argVS za{!DMLKS>qCWDgHbEq0kXbYH>6^I<9`*KH{ZJMzcLi=NI!-JK`9uV{orODIQzLBMY z+_Il6{>`s5?WlkGMOBvF_Vd9Wnsdx;!`0Ia#CXW3GvT^GCd&m3(kCL*uGp>R8y=Tm zx!r*@a|jPk8~5nUW#hx<9OVy@WsEabfR6nCOF1td<1ho(j6DHXYbgjrJ)IpQfREPf z3^1GC8C~<VQUM$>Yb_Nc>^lh#B01P_y&-s_413AxIBTI&0phe~G;b=E*<Dy1o@Jw~ zI)ILX(h|7GyK$hiYiyQM#xeZG#}$}y!S>GU3U1Uz?r|b?(9bcGYxCoh081j@7~-X@ zbz(}Fb{g9t`H+lP?|9JVTh?Hy-xXE4T5h7gQw>9Y?dr3?8%_`A2d;_BMlAftl6py; zb;WFc-+S(!{CsCTx5BxS#96LwWOMqL&-ep2(Dl-N|5Y~C^`kCh7OCFCJ}*`IK2Lst zwoSDFx7JORyf-=JOwSstp-m`&nQpKCLjcQ9*Q|#rbTPzEq^g4zVRi;TAr#pdkvPH6 z4&uaksKz?gd2?i6Bb7*8*h)%z!mz>cGdphd1v%G?mXam;B#cLX71i>jmfx0F!o`27 zzXf!wZSUmumd9fXe1&GDR$Ul*xY}z35zW=>Nr2xmrO@xix@!sX%X5Z8P@m}d+imeU zfBii`Ky66Q1HDMeVBrH=>ajEfi!xgP5bX3~nHZ5ac-=J`h2nHxFW%J>7(83>5^?q^ zf!ulRC3_onYVvL<xp-^EMp+KId#BYHlVp-qHv%S*BH*8_l<A8ijDx?DV$J>j`9uEE z?g6b9FLHd<V=jFQHXz;q`6IX}#7D9u!gjO3e>luWNRNOgc{pywvjqq1{}J{UUQxE; z*Qip`-Ce^lLzggw5<?9=bV@f!3@Iq!P(#BE-91AiAYi=GFoeXQfaps%N-FpoANo7r z`PNxyopqi+VXu8%&sBThW%RBa&IJX@sVSqK|C=rUFWm*@iO4bT>wGZd7QZnJ>73m| zUjdcU&Tg~><_gDO^VGyU)djxC9v)JW54#S4d<SKugfcnat<;0>b$-WrbgoaCfHyYp z1w}UgcAEPd7+H&-t7uC+8)WcbYj@4?HSK27%{J4wpeZm;pUq(DLU>G1dCR2M8??R} zqbv39TskF0WHpz4W0SevKi(2)-t+y!XhU0BZ!2d+j5X?PbnPj_@}*ZyH@8Iwh0!>{ z^BY>B=}wdF0gu^pHO{9vx5tGe|GtV6mf?H-3jK#actr=u3GRkJ04~Obnh2W$LaEe7 zYXh|MU^~oGsFSQK$k9a9Z4H5;N0sw2-2x#(oL^U)NYdIausW#}zJ0nrpj7UWly7G) zQ%*~Q=j1GPk!ABk%7_>hy_;QR<P4K!NWU=6tNQ@j+IaeSJ`CiQgw*lDz%DHsA2%3a zOW2^Nos`o1_t~;Wd}D9d84Lp|&t;bx8qH-B(+_;rbaSHbTcy2t3S*8a$N8|(879L3 zOupM9ZI&hDo|e9vB;!G{{ftbn615ZFVCC-iKYqf<^y7DC7DTY8S!vnkMwyus9e=Lh zcY8W%&-QniUA!HV54$ztVx}JaAmzarF~zSkU70u1eCa7(l<^2WQXRxXme%QFH7X`) znGyI0pWk}vH}ln-{LFJXy{#&@?lcCDU(tAV(fBRfj4~2`=AS2BVi0RdOZHv30_Q+6 zP*$Rz9k*Yrr)5+`agP1uD9+p|$gn%~-iTsbY`4*F`?Ycr3gI_8G<_h;knFBH=Zd$M z78DaPm4MBZh>I?ABbpV3&;6UfYzGBoW@s^|DZ<*tve8@Gh@=mU3r{KRx@zL;En1WW zko(pielFvk5vpb3rH)OdsBeL$S$QohALzg8d<HvDX1@v4>uhowGZT8y1<Lze4qT=J zB>J;cV7A@$E$6K13oVP3gK8a1z7(>6wdIfX@|DiSys8I&f?E<)b#}0{$wD1l5nQET z=BVbHyS@tShhO#Hf4}f{X>QtUD?E}q#n<t$?hig+w0=OgC~xM^h;Xg%OcyPj^AgzJ z_g?<W%ZuartX7ua@b~^9?f3w?+7=2ice)S*@nwxA9w(;=4?ADAsDB@zm7ghLUE)oh zfK$LmcJKl|v7%W7cwe_hw?VymAA6B6B1qH~XIow^mS`TJFMf9^@0q}4I7IwsQay&< zlG@M^j}c<ZBLWb@@^+3dpIvYw9<{V9dnfHZ*K$)TrJKkK3EasT%DtDdOUe6Hj;O=o zCt{HWJJJ{Oh`UR3>1|jyIJI#|r~8>C{pUw79ec#+3W0dui&ut8V$42EBYg;@(87_J z&ZE-1Op+0^QyyUMW9bZi(bXag2Bx%;?>Z#S2T5I<0=EKn1|!=70+m;3zcI~)-p)7& zLu!}*;B%(!|7SnQ%N<vq!@yITXZ(Oi&|RZ}xt&H7Ke9?j2E`#)P%E8zid<R9jXybu z!ES)l2P-vW*=7KLTpph9%av4@6J_x_zXXo!LKhlq6?o)!e+YDu+JN<Y8o0a_KW-Ns zL$20Fuk(`JNk^s7WHfA)eIHy8$#Mw_$hf<l<MqmJ#saQD<DZZU(O=)IwrCr5GUMGd zd-6D0Ak)~Fssi5~^=5R0<3Ab$Yn;-^!ljhaU9p889URqn8K=S#OwGU%5yR5=Crnt1 z1eb|r=REd0Y7;(oMgUF9ILfXCy|)lz9@KmGA27@;HaaO|@^m!l^aZWK6LL)~Du1=5 z(Fu)be0g^6(NL;x8{MX4ndL9RBIG{*nY;fFK8N?wkfY9_Wo|?0hlxACAKQdx*^5ah zxpp#d_2Pg`^iQoT>O|N~<M!jTxtYuJN9arIS8|vKg_9|r|5da0z1d>yrbbB8Q;v(8 zULzkBG%%4CHu2RRjd~+dP6ELTc(jaCHSgy@jM$Vf(7RTGq~5qntTXye)R1I43Qch( z@sNs_17+#*FlkB4jsd^1&wXYBRmGslES2et`NT@tQKT4|6}i&<2URd)EVL9<CDCX$ zH0g^ty>O|^3_$C>7p4+@#=0371eG!q=$5&0E0J#s@rX-Sfnw7J+$5R`7QM4|>CQXq z+7BN$>Pi+l%)lJ2!KsO`5LlvIPZ_x8%{=3gPIQzD@6H3`5JW|Is}h}ufP#gt5_;@! z{xLxRPyKVD<PXxCJ8Gqfl?Ih&kVrHxsFjV}|3EuRY?~wO{j8dL8?NqNvF9UeRi&rb z&~)(s`TzRgIzlUAi%RNb8>1E?SOY6w2a!@LKeuK!&}rr}-ukMA)!h=qW<(_;WpnO; zZ;fybhL`A$KlD*tz~nI&N?jUL7c^JxLKrz05~kOb(%Wk$MCeLMn8jN(oqLQCN0}=? z@-OJIi)0CA!YjOPUT;k6JvLZ-1s~zMhq>)OT7=!h=3Mtv;%$ORrwWj8{2!AJ8&|8- zz<hUCU(mPm_<n%m(Acu!3P~8Qd|@mr_WkEYys4piL}1Owc*+;wUKDRDHviZ72cJO& zL&y~&wUFCXYKd<Yl-4%wxMOmVcrrZd7raPc^)NA58wBmrDvHFw9iS@la~yM`YQph@ zrs*;A{FTMhYHwWa%n%U6k|OYGO29!G*EJFw^E~ubyzJ@O{t#97jT)s#gh^ndgW0F1 zT&brG(pOApduITTw#F_BU(yQK?b}ova%nF`8!;K>Xe!<r51|%P)tN;3n`gx>i4|pC zEPon&A#&VS#ddS-ea&ksjdRALy@S~U!(%{(C4vmC+5>kR?mHLh(9fDe>eok&Z8?KE zzsws=EPAF=0%$$wDI75Ovv$HRli2GuvE0Z3TT^ZeK(IO9UB=8_a^6sqGK`2pS0wNS z`Y6O|wCHo08U~-b$JDE>Wn1#sKkB#cOUO3-CjUHaY-Z@eTI2Y(#jzzYw}A4H$DgnP zIfcJdY96KO+!tpj%Y(;V6?)iH(ZYr`l%`T9L{ZHOXH}@PgD*i-cWRUvvW(M>_1GQ$ zNf><yb^?5ql6K^LovW)mG7GUobnwLMRVg!#T+;BTpm7ppgc`MU(VcU1kmRGMEL0+6 z8`-}MDrW}<bCm6<R$}HX8;!P9bw`UiAKX}287-1<9M<;Z_n8~$yyr3LN4iDs+l|4U zg3L*?s}4Qf$)`D%jZZs7le(vXAoGtGPo>D2U+7zut9=wRu9ufk_rfkO>J3GCjM((D zdRh%j>aIxiNx=~R`R%3bUkqkDXHb5YddJK_VSb+LC9lwF{Mb%}S90?%|6l%`>()ZH z5jXgJ>RkO9e_{0=zvHKkT`GATEX+~_$1)upwPwBqI~s=d)B)7Pzg|1A=TR2Z+YYFC zSl7zn$eZQZF+=ig$MUZ&8OqUm*lLW3ARjxySF9w}V|qk@)~9%Bt#5E1tJGpKvOtxd z!TCh)_Tb5>m*LdU0r)c-!{0$1!O3alc9lQfl(PufC@D$ZO>JYLh2DzZ08iBQt2>%& zg>DevCjyfPC3?fxdKY7;#OGXe4jhw=gIM@AHD7Zh_$9Y+zKs}pl740y4sB*bZ5+}} zpd3_9-g5PBCtTois=kuFdz9qzNS|(9&6j1zQeF7p@k`-Gr!C>o`EAx_dAiu|9iQ8b zMNJ~CYORut8TX0y=&oIE@38!duU!ob$ljm)-22}?)s-@v#fzaT0pEqHM`AUpxtoQW z$OeWL1<(|j9D20y0rO9fOP}M^C$>*=zWKwX`gC2AL%Y@0hl05WvVt%kXpFN7;zy%3 zf39&Uk0FY<W(zUFI(r|Gk9^hmwWIq$RnV_5s>6Aj+(ZG`8eX@^+si|4ZdIB(kQ*Yn zF5w8>O6=6quA;`h=zpkRMGd%AzD^yW35KJ`a2gqURhZQEXb(&eXR5g6GX?J8a@krI zYSIyyKVT`774Dx1vU6bgBA*b`VcFEt0&`g7-QCdeFig?rq2CyQN^9C_SsJ~4e+NF# zMm;nyLJEv~Q(0(NNcUCvtY1*)){%CboGh?&@H$mD^gn7TS7DZxCj9Ond@i6rAZv}2 zyh}|}dXExbS^csUOfHpeW9|1oG!`NH2YNZsatK=u`Md~X=WLbX50$DScUvZ3Bg42b z4iDaz2k_=8Gm#GpEFtT)3G)I6ZXEE{u8A4k_RDrj+5p+t32^>RzJA@sGUF^=Mo%%h zXR>TUs&h&u+AVNGD9FqU5?8NGAh!$doz<FN)EEgDV&J}nMKq+#Qln|*>P4%Rms5NS zP0|Ix;=TzR4Z59qr@lt$M;yN7$!EyyH;=1Q4A@y-JdUW?S@Dm~O9!N?KLIjj`d0qb z?WozL8$3W671Z_C24qi{=?cc%boJ_&ChSPt70!-gnVxo(9y+pq)xwrmdrOW@ZW`uS zy)FHeH=>ja-1gL#^bQo#{e#cBp)h2Le1Vre7$j|eQ>~3LawSr6ysX=mCyu4xas@5V zZIQodn8>{;@b~ICW6KcrtaYuq#@Hg7P^d~}_4K=^!@nHz6@bm2Xmv<)NttF}l>F4h zISWv$YOi$JcKGHwhlw$F53Pxd@J)sdqJ^X^Hdm)ej{~}S6%vxv3>qbrT!CcOLN)gL zvloPo15=7jt;{#uA3KM+`##LH>A?Yl%64&9D);Q^b2wmix@!`f8rLh^wU}?Irs9Pw zjj+W^kk-4(zFUQepnpu*fwS^VrHMkj$yPpOBSS<7c#KmR0{FuZ;LZbUe;IpNW)p_b zXL(;hTO+6wmb8_(cKkG5SF==fjAXj9(=rA9h#|k$RaLy<ISBiGq2>=hi>mTB`-_3X zTXoQdqL_MnK}J(J^&a9$1Ij{GtsMOp&c77t0(z$8tHD?RdpIJnM@x}u0@n&=w}246 z$(TCHZilf6(-7~G6i}sTsmn2p>QheVW{Q$ht=KYX+cB0jQd_lA=A{7ZuAz~&HW0DK z4R>Ao_msSxslc`Kom&GW`|-1HOoX~L)NqJHa}_R>Y2E-h<PBoH&k{|-62OyIiZb)l zy7Sb|g|_Hkv*h6&vjy&Kf$gisaQwAlo2#a6#$8Rfa3&o87t@Ni3c+&Lv$H2}tZOX+ zJ@RhHDOWi<SOlb!cg7bu9-MS4La4WfthQDc-!9Ft;?=vvn?)w*vhJd^5pq@HXB>if zE^Q)!pP&Aa`?g$>1M?qzzUkHe?Y~w}xvdI}dK|b9b#2jhRpV}Hi}TEl5ZVNz+b%`7 z`jp6b8RnZIP6Z3q4WV`Hq$Q;CDf2e<DR1@M<bzgQUk68a`*rT|YPq5)(faP<sJ#IH zR#+r1`4R4epM!jt<@0GN^DJAN3R&@2PxbKQ<D<kmiPHW>p4D|QH>}`u{3$YT2;Nt_ zY)#vo9W&U{F^$<qo5SN7T}CW{l%(^jd!Z0?qs^XUJ-zl(Ejg-k+Eles(#ikc0go{f zlMp!)&3>Sfb3hKQ8#MGecw-76HoJDYJ6Mt*H^4vJqaV*v2N`K-K&no{!KueWaEE@v zWL{_c4rSI?LtpIobW;nE)#22VaYr7;Cs-l+tU7^A^~0FXAAEK-(joisoBI|69g|CP zvbVIT;s>?59#MLMzGs^m7+JgEC`&Ud#wTD9<porI{t_D3Qzl7E20`Hwf`-R^LOD3$ z^<E8qq39h%e2B932R2DFjL0KD%I?Hz_x!5kgG7C&nv`J|{;A9P*aSJ5Go0F#BIC=i zCiR>%B32eS<OH0*@NFWuR@|UM1#s2gVB!aG`zu5CY4%3WU8l?~1>SZHes4X&(e&$c z;&}E?SeY88MDQMxY4@$k)`8xh=%`HQlNt;0IjU-CrH(9f2RS3Qt627qS%03XI!i<D z5M$8KKJ;}sK8a4jPgw9#%4RTfRdd?IoUy%wccS{>i6qC|K`4)ou^o$F(ihU&5!GQI ze#fOLxsS_$zxVU!|JQ!!mLTAu7gT*!Yf?_0Sd)I_E@zWHhEGItc06$1Yn9BNm9lqq z{laPxcP<V%<?NN$*Tk_;WR_W%x1h3@%`J5`-%;)-`Y{y0y~JLBz_`(W-ae(!R=dlD z5oA_$dso?C`eZxZdRPGp^Lg!sT4Y(<Q1N*8Q7r=UTY{Z=Uh~sOK&93LhZeJ<vwYM- za8PiU&6bT0AC9^DEJ^P6%bn)P*$h`HZ<n<f0*zOc$PV-`3fKF2?U}-9kk*Zr;TZfY zAqp*rkK?C$1L>=et1MH=0Kxt6GbG@O$g-r0r;z6ky*AhAUE}G{)hfO2dW0iI6J=(p zo1K=Ytcnh2QcRMqY0?AlQ@!?MpZ~xIoiMwDtjJ5wzxn6hD&%PX=Kn5fmmXQim-1Qc zkCj`Aao5jKsu>C+4EZ8zGS)93nT9YYi_{eb3y~kZ1}aD1XGCTr^ug&Bq75FhtTKW; zIvu^8osXAY0$z4#E1PZg{tgiCk{Fx!Q4T3UD3py<ahCzSHYwQ~YZYMI9q^~2E2WX3 z5=N1zo#EH7ms%}2lcFLuupi9uVS*kLbH*8(MU`cMo>s8uT`!d2(O`Xs>zl~bE%&_O zRz0^HXI!lfrDL;as&Mj+A9rhAGkU{i`1C1e^BUvDjL)Lj^{31)l6f^w9#{sNX2y;x z?kJ!e+%*$BECRQ_S~uP~Q`(PyD3nWSWyL~lo0)3?;>Z$BW$lu2x~6!s4T2^W5`T^~ zI59t9`h(Arf#Lu3U%$zaZlqXvRbYB}U!hTrfIJG8FBBH?gmT0_Wb*d#j(ZcKKw0~A zCDn>1fj3_@ZwFi?@{n)%CoHB4v<TNT5wrwN`-YNoVyTEye&kZ)WM7a4BnQH=x{o#E zg**f`H_*NniZ&?&kF*9(V@|dyC*QK>F?}}-)|Uj!+wwR_wz<3icf^*9fh^7yW1s0$ zrkH^l@7SpMs99wKpLTf!*X^O}J$!6Az7<RAjQ7`UJWG1tEXughuGcHN;)T+ycCh1y z>|DVI?PaMts?7#KQOgXMb7i@PANsm<jRC2qSU`GX?+;wi>$~Qmzsfu8DGYl;cSy#b zwg?EQSc+uO3#dlcKInI6+z5x6_HW=ugF^qlPd%sN@IU)^q;aed$AUEizbzrgw1gX{ zx31*p6bBco4{6rIw5cpAbw;lJ`hckMvLccT4lzd9h&)ry6<UaPWW>2D&rdf`lrABe z<lM}K<OjPO(0A16sbJ3Dl8iX<E$7B|Ef@I|NeQ4Pz@Fk18CJk_vLm%;HLg*wo^-%S zb7sTm+pC)z!<zSI2I(@Y$Z!W-xR_aIg7)=LSFYnu9lFMds2SB>)Y@p%Z#I{b+jvar zp8=`I)_=NnIEA)1X~I#!NhF|h)+G07Z?6D(NH@{UV3Kf>%V-(2RoC!#M3%)u4MXFQ zFm{1~Tzjh8BY@Qo$b2KJ=JNNb0le8Z>CD;Fm~Yiwm&9MmNXg>kAePU=C=#sy`b&H@ zKtR?AH}|V*n7utk2ML~oMnv|Om4d>E|IL(E?0NW;++Y^H*-ivoVmA?Yrkh!1wcK4G zi~DzqcO6LzN%DgKk3OEip~X^EPNjxRi=D@@rdeQk24a597tAbD++d}?<*@1%Q1*`k z?7#Nr*a?ToZM4{v?X;<uE)$`<;gqt@_BPfi>|(7!bZ0MiR#F}NGwpK`OLddi9xZLK zMf=N6v=y**uye^+6KI_M(=A(^r^5Z4-j-{LrH&_xoR$?NMuBgPU%xo?>d5X*f2F_v zhzfjRpr93b8b~Q*6B8L;xNkqn{OW|N+;V%7_QTH$A$#g>HTb=OO!_Jxxdy`rbyudi z;jjc?W<^b!R1)I8>!0|VQHp_FBTzQn#7kyT!JLJVue6!7$rUDyBX0}72*JMS6A9|e zkBjDdz}2kQ><usHLV`U+Qlc}g!Wfd%hqt`Te7ho7*+jZF^m^1Gm}6Y<o;lgPgs~=H z%FrYI**@CN#Rn9O8(Es^xw{lo(P>dMl3x2gqbFt3)tT_EI;}ac#&)IL@w$)9)wa@U z3H-kx7GgN8;6K8+wLSswC>xZNpRn+;<nbL%NUHH20P+A@R7Zaq^djW|Z+W09&jZTd z+byRB*7ROjrfpuy<s=eovD<mX+3c`VF;hU7HnnqEK%E(-;k>ErSZ%kI?q<F*CyN<C zdoPpg*F;!adKiu=P0E#LE2nP4`Bf7KbPgU|02H=kbQaaoA^gGTxAD>6`>|VInzraV z3+l1P1wFl@_1?C;ZV_s0)RwdcYnFLmNE9}M$f+7=oI}jM^8h$T$?MGu=!i3UKRasE z@JhE>sxto{oCc_{*f~Bi0^`V$)5=u(<q<Sne1I&X-6mvkm*ewNVf)aBhI60r>+qH? z_O=go@x-RUq@T${X^BXS$gF9vsqT~+v?|?%b<hgSVL|r#pqXf1pmmU&W4mN2JN-L3 z7-vVvMm0E^8+B#5a3d4~z5mz_)yjgI7)bQ^zIDAj_JRp&>6TEoHFt4iC>+G>pD{gG z^R;?NG$$pxIHSW%JXfc~8jo$IW;nG)`hX0P(Zf;z@4NBz0Y@S_k<c*mjO<^GZu*8* zvLyg39}NA@!5@6yDW@y^L_b<kWfI$DxVseFNN2C>%hZ#z^7&~Xy`{6zfWsa099Y)E zf*om)0C;Ag_Bd?WK7{iQ=93$LN@MJugdPonCH6##f_%o?C2<A@+G_V$<8F<YLP2<m zCQBm{%~+?>`*~@fY0TNi1}`DKt$AC&y@QKr*z=@@+5Z7&v1lqC!OQBfw2B^sF+UXN zIW?2}zO(5D#gzDJsS{i@wTqSaHUb&81GHkPg$k)UA^n+yKdDn9OA_?5SY3mRv!AZo zvQn+n=Yw4+J`}-TqDcJLLliS*5oMxN+DjE&BOK$77Qr-Oa=|s0NcB(YD9gqryr3*0 zHIYMj_AzA-$r-X5&ThDeJ{T}{Vp+HF;F>M9@{FSP0OtMePk$NT^k4V|-YJ>Ei?cw! zJ+p%~S?1)_{}_BGj5rJ%?7ek)Htm!_4lVAU?xx(yMXs{6P*)FDE#`zed6+SIfIh>q zHoFa|&Q23-MKnIiznh8q6eJ|ZB^7IsRvNTaJM7iY)4Mt5!*4&j2hO(AaorGX0*(x! zOUxH#2jFCsDZW!6%7(YppG)^1x;CzE<!Xt7C9PP<fI3OzM$qhj^Sk#VIeGL2k=vhc zBuvh$vqv0)LKUPVi_p2kWf?0rlAN{H4U2kVg*sOs{+3`uggt}zU4}Ga&*XV50(HH^ z&e10QW++_X9pJVYsZj6ba2r5P+d2(DooVz*MD{*{cG3Sf<#-04Hp_)KK+;aj207sa z(I1|4v>71(!=EL!myo@i{TF%H_byHuq{mZIgn97ED2s>dqYMMD%<>74qodD%_u`yO zB~%$XBp^n0ra#>y%WPE`L~BYXl{2+_!l4J*(awT1mM$Bitjzk-`aPW9Rcgb~e$mpt z%=uIHuzOZ~87O&t->Yu^FwpWU3vOd45xhiuTESb7jky1wXqWj2qf}+eJazCq+sgRH z<y2eQ!@9_D*yoHwRCQ?<d8lc6#V&rlM;xyl!XfBH;<kLQNx?uNnIu=Od-d+28w$bW zGps_k4r`EsKXq{!H{u#9Y6-75NIIP&G)2Rl3U<&(>1lck7gyq-MKh962kt<!EI8Ya zh{6OCYgNBO2bv+Qm%9~Q3dBaYxZHt@7Pb82W!32Lx4*bB=(^dT&O6=8lX>K&Wy?Xs zOdxW?v5cWU0%#`6%lEAOO$yTD?_{Q_Sf+yjiM0d9zZm*+75F4NIa)Kmr5`;bDf0zR z!OJ@I6Ya!P20{l-#~am`DT$Ne3{d}~oC0+!g2>+70kmD{o;f+%(ylIkKpS0^my31i zaFrV^aZYGz?_d7a?DW8U_~bfY0VzA?WI|`B)OUJh2mlL-Bk40+-rS?x+9a*(o{U`D z_SWgDpNJXh`5}?<)$m5bbk6L1)c>jO^cPG!Db0%?$MLgV^-OaNBX66Lmyb^aSME8u znm|DOtEaXL%?@kFzH`Oq&_>^PXaO0k_6-4~f5jr_8Px54f8_SH^Hs_mM@Y{j_l8id z4-$TR@W1>~75`iRa9fnN8LVHBy7`r1QulE>G3t7bra`ueK9`_dBYw$aBd$A{Iz^|( zECw9B&FNL4yonQ6$rUt-d-)^k>lv!(K4t&RB_$)-RfKnb5A78=!fbs6^1%$~8hh`j zhjMZ~v;N)9GFujEIERNE!@<gGW=Ad($KHvU@v15C+G=1B0qT$V4l<tXI6+Ht2t;a@ zjIX5caikM}RBcj9I%?SaptRt7<{DM=c2r~ObR^N?0U`cWV6azL0C3{ZPuDqnWLPdI z0ILia@A)TCyUftA$<uCa%I}_qJ#V3p(1fO4cQ%@1Dbv%qs+B-abTM~(pzuIlu4yq9 zKqNC<A>zVsn!rYlOAo8dS|~%tsfo+b4VC@X$9*>-{H>pFu`rUI26E9*l3DD35F4~h z0$YGcq6-O*V+Joo#-i{WW^7$&r}s7HTBes(l&L#6@?4FIX&0F(FYWjoRO81fXp5as z#zz<=bm+3^XVRW-lY}zfo?l2;!X2t+(b3T6^a43(19=NDAjA{`*4VxZGtj9WX=iVS zts}0`2AWJ2AXbyx9m{J6OUImm%`50*N3GmxUd3h;F$fjGrZ{yoPt+~CW-jiG1wd{u zogQVQ$24h`a^bCJI=G>wbyj`e)`?ImqHXCC=)MbKQ4L?87!;7Wxck`-j)eho0m8g2 zJc=RLLJRc9EqiaNxj|r%4tl1YE_DdU0(3}y_w=c}4Kf1Z?x`nI=aDsBxFJtK{Nc~X zdnAxO%A5SNepc6;0oaTh7D_bYJI-Qg!e&@RU*4HnBiR1mC7|WaM+*@A-Jei3A1*?{ z9$W`QIj2N>&$G0$tD}~vMN;n$=cj}n7Ry9L|N0i;+wZOevf&$^Y6Ty;nC=pJzV}_I zW_ow=36sive&wr}^8C4tpXoak8^KwzNLb$1BjL#$#!{fNURzSKHNZv5)<(Q0DCwz_ z)@D5y@7<54+xjJFu{wJdw6?gag1BZNawGPW&}KZHRI|UwHoD5FmzR!1^j%ImcFaOf zNfW&CukNVZ-Pggk<y`iZ%0gP{9F;XK1_PMv(|~+`)!;<x!DJrzyV8(Qt$JrukodG( z0X5pPiRG8S83!55y1bIuAAD~7qi*iQQBJDM789Pml#M2(R<>`GM1F#tI}I4TfA9(A zem~ozgD-Q>RB(tBa;Hc+wj{Z`nPUaUhrT<P3+<%pHMjp5pmDK04;7lHJU#aL{u*Q@ znED}`2cXg+naz`hPe&~R?FUIV@&D1kS${AFy&WNTkB^v9bjRF8vt!P-gD~u@4)~#0 z!6Qn0*1p`^D`v#-Tu>l@`OVbmNdc(P!9G2FF!6hl&P@CKBXLm*vB6vONvy01mzaQq z8Y>|NykPeM$OHYsk?lXOR&PzxWo=TM8h6V<V@HQdKli|BEp*D=q1cgwXlCt4w>Ozv zjfE|zSN!4K1@0v*CH5r({XRx5wU{Y7Ct%}XGDNOjv}lFqTXVn{`uxB1vEMiP(=FU2 z?+@2)y?jlJ`L0eZ_cG1WbQ?RHZd5`hIXNE^Vc>Fd3L(v)bS%oXO4j>wKlQ9TR=Ccp zVan}fSG6=(Li}5h8Ba%bn^_o7F(}f3NhH#ba$&%9h-dhf#Pb&xV_x?wIx1SHlOjmW zz6<JaFgv9+ajcyD<k~)IFQ&|yBY8chm^#obP5r7&VEh4>5m{o?nxQ^e+e3XTRBXE1 zsa9CkOQC%2AIp+fAU4XRl2BD_Q{B2m<~j%pW(>bj-DXNquaoj!pPGZ7@lvzcasH|r ze^`~#7+i1*o7rd4sOzeMMUl>F@j?(%6L%)&oefz`@+MR`$FFn56Tf+U<l-SWZDP|& zjWloT7|X)Qr6SU+9PS}I{@(ZgyxE_A^qu6rRSaV4pc40(=$CIo*X-WiY(G#osFBsy zJf*V_UvoscBT{$nl`H3M7Ofbe_x(_aI?VqGJO111WpdUUvJ(K0^w-U&W_c0NAfvwd z^N3a>r?e`ef^meyf88MFjG2#0fPQrGvB#t4H%IJ<iHV3T+$Htfs{THB55OI+U8Zpf z9P=o&=V!ueDsZ#1d8XJQd@Iqfgd}+HS(IvtvD$;u^DAKQD<L+VT=ftsIuDn*^%3M$ zjk0#=DAi|iM|#4MSmUoU83fBd2JpN9(VIwRWq(+SeElDLxShCsH^vl5Ai?W(ZkkGf zS(ZSL>I&YE0vRUh$_tJL7kB9}Slk@jqNbl>O~Je$=GCA0dawKvatynfKd0$WSy%Br z&Rz};N&bwPlxek5)!~{ir;}u-=K*{4+`%E>G)bdTzX;fg^IYi{=Dfix6&hM_-LrUF zNKx8CNReGBG>>^ms3=OAoX~u1GJ)H#pau-@d*^Ht*;4qpy-xeDC`379=;DW)foP7W zpn@pQEy!BT7|0%EOGx`tAzD4e+dUEXcmoZU+{mrnudj|yk1@o13cDGYD{qH5<{gl| z8=0G`xzC7_i&uZZct^$sLVsZ^laS-;&lr(fplatbGdtS<KyI0B^&$%!9^{>4p;$;W z{NWL|;9+62vC-5VO}D^f{2AWoPB;QmxSr_(p7E2ZNZ%2vCT0F@E-PyFv9c)rThd0$ zLD-S`OwxaU=NrHE{Qv4JZ|&`jbS=9&7BbK<=M1}pHzXvKlz{A21t>;?YS%FUO-(<| z{~^pxo;I`d(Cp78Ce7$WYi9*A07><i`5cL=0ohu0jC)Lhm%8EgyDhK6jQFa9iuKdG z9K@?+QE;%{5Y-(L*%EC*Y{1|^Kr##Y;aEu<Yk3-T8TT{#Y%out61R9v1Q4ySy}l;j zyLTNdquL(bVZQg$i=ps``aJ-*j<P_{kuYK8JP~V99|@Gv5^~e{xnLd_w-$Uqrqhvg z112sHzYZH(KXuzP&8Le0*KCKe2GkBLuE(z4nfPduM&){r6m6}`Mz%kWb59Mfpn>-d z$&W9~g3FbhP<s_FlO>C}!SMaDVSlGT{Ml5ig`9Nd+Hm7oEjMz0nq(V_kmCUI*7iAz zc`W?u&+)cTDwhqp8V$RD*5yLi$ESD{ZyEn{+;vF};i_<?BcFe<!b-kHNJz0$v0>X3 zx;hbV<TZ-y3UarooHCc;g&J!;$|jI`ow3&5H?R&ev9kOcGP+j|;t)~Q58XXLYuMFu zfNKZ3EUQ1T($_K6n!QkDC5Y&qXlY$S?@hSHI~$hPKnAH8dV-|XOw;m&Ma8sihP3<Y zw5JVi?@QAo`P<vssq_Zl5IAA!ijAYsO7|=P#1zAI(&kflIBwoTA~sqte2^x5BO6zw z$l0f^Xp_dl{2|pQc^aj=p|KGx1p?*o#>fxtI|2%=v-;*?!!p8o?m0ST(pej4vw!gU z4U2)S%}?^4JTeSblB{Snt=`*6;yF(IXxBcTb~j^KI_i}ZB>cy$b`Y}dxf1v8S27r< z0BK1bVyDklS8^s3BGELqlRq3v)%ms*?>gKHAT?1a>y>lFeWoc};3`2j!`?k7wPWc0 z!UnF_Sr$;$6?22Fy~8ckFHYv1O$&r+)^Jknei2W3XysA#>q7v`_ucM5_*K3wm#G0I zUv&>8#7dt3<E!ryQt6H83m+Wn#3_6*?{s!s+i=ot-G{odUgk$Xkke57w&hkRjy5qU zBrRi;KE|h2KuUal-4FRJcRlApiCFYlp_P$%#)kUEb7a+RJjYO~UUAnUpPkFCqDb_^ ze1AX9fLzT>TNOUb8(*C?5M8galZMJ)eZk&+dC2jf{RQjfMCOu&<B9*Pr}Hvbv-5n= zc<KM2D|uxB?C;O<V!geHcc;I~dz(BzU(`*tRx0R3Nn*PF0&y+avU6r{TDg}`Zw~=g zd(&*!6kg_c*fY-_%T}E~lC#S_>l`v_Y|I<V+nS%!GYrZ*tu)|F^~MHXDm`Oy)NP(R z9$6ka7pphq%f2alb*I>c?-41F9jZz(0C@7{m>ko$5zOTw^9=aV(SquLZPy0a@1DMe zw2a0ar14TzhKthd@&e1|Je?C871^jxfw~<)oPY1c9ics?;_%4j^RX22r%H%JOy~9F z-Rk$V<;ss|mQ`YGOM0?<_VPRmlr;YEXHE6u=6?RsGB@<m?V!vUWN1?pw_ACG_~7-M zwb!6g`#X3?EYz*Th9drTyO?s41I3sdlDJZAH!*jKPMla>+|~lg$W<oAJG@MlPFxSC zW1#aQUbdO)R8ltNo&LlnE<*L)npGDnaKv%vJZA=F4qlwXIY>I>TlnnC&HhTLfweB| zduX-@@X(l6=k4}R28YVqzJEX2$$9ONk;A`XUL~B*m{aByrwHrCQ2`h3_`1n9FnclT zl?%BFd_h_=BU*GABhfi<XJEz^^$9zL9o|^xml3*Cj9WaB{A`L;HYXnn1Lf%bt5Wi; z^^kHzuo17in(J#Jed0*7TOT~$)nd^mlLugkG*eJ$T4Nstg_*LOEWj2&T8jNoeCf;o z-4B*>(0RI}8*N>!Ai$XjR86$NxYH*dO6{m80SfjORQSiOvJ9g{NrG;cFk^#`_z6)5 zyNM&Dj?1%6G9>f@rcQ+*f=YWhyAlo+o?4TM9*X-M^7wAwxcp86R?B@olOZJxuMGOo z>CzQoCQ%^cNx37KAtgEvYy8@(&O)~Nwe{q!Pf2SShi8T{72`gJPZ&vMqfB2oW4;rQ zYNnOQk9G!|xW-GL{0?;<#Pn$z*sz(*zm>KBmphr@em1OKbE(%UL2eeD-sL`^_-@XN z*gT>80Mntx^5LmKfJqJ-7gf5gWoN|FrMm9v(6qrnc~&tvl%SRa@xxlDHn)GW@_c?b zG&uDv_<((?&D!{}D08--xaA-I97DMwM}$EqZehR5eWJ<cb@E_SQCll+!Ktm-o<8Ho zluA>%st?MahGuLQEbrbqj<1?4c`nXk;Hu9~lO^IwQ75$6X`H52!98ut>_d38Eu`g3 zKf5vYvMsJo>%9aJ%ynz~kw-8xPytBoZ7QVUVp3b67~TddtTihXfy5YL6VdG~B4+ir zN@Z>`hSpl{<~A=orE-M3PULGO@Yk}g68EjatPJBUxk)y=sl8>I1n1}^d&XUvl#oG7 z^0UO!t$uswO96x0|B9j>N+l4*@nRxT+;|QB%04#Ytxaqus_-398$9?z>_uJfA&3<r z;FgsNht+Am;_NkOVxf-6C9<2;KfjAxW~S^07rq`6nNELb!}gHfIj8RL`$S)0%#c;e zNnUFD-FhO%caC7Ae7wl$w@I`)YM!|*X@nB*a%jyKHkFiGjL)=SfBQht!uxVF<0=Q2 z0~(ew{3N}+$e;O4p3U1w{BZ~h*111Ey(~OUBOI9fz|Ph{S3A7ek#SU8Mc*hIAl=$y zmNL|5_p|Ck=i6I>mXYX{<4jfWv;s2o>!e;61L-&Eb=9v1C%)x@0sh6-DDTXNe7dQ9 zxQ|Yl$LP$<Dmd;#9YTba1zR-v@%`%b#pZvup{d>O0z*bK55x9y=Zk#eu3j)v!?nmL zS6*q{7QVDd{HWsQi9BM`reJg*klo`flG9Gg$&bEk=dd7caUJALs6|#)-qN`lv|W={ z(u(<jPd^BYcyyvP!;Oy_EC2SV%WA4O`mdpdd$*v_U1pB#Hy?Bs#qZe>({M%lzS(lV z-V~v$(h2gd)1NR9A5{qaQJ(v<+*}+pSqs0OF!&ItcRf^Ku4g%zIR>tbrgYW)IZk!T zVkG|Z>u<&+6L3-V9N^ts8`B5U3IQATu6jWp^zEk<KBp|Azww6oDWe*>n(?VSL40F_ z^tJa&_4A^BtYu3iow{%GIF+mIG`{zgG^+O@BuX>6#$(E8+yT3?7(_%5_YwHKcMB0B zi-qURM93(4n)EwMY{$KjkpjFI`KI3qsdsjnaQZCExlF`uBMor1Hl83lH9Hvo;kKfi zVq9S8KXypO+Y1Mm);=LcPlYZ}Gna(q`n5rw2B%n&3O{t=-LC^fA3Irp_!D2>poWm6 zt|8u4heCn@IQ%Q1j<YWSx&BC7=GU7F+Yld@`+Sz<ByqksC+|if+-SRJMzqidei^u+ z8G{OdSRg(pj)R9}m5u^KOac;*t(HFYP95hG5Rc^&Vcy7}eX7VRE{yB?8Wm2CyvwvT zh&<E$*v@K@=!%7+jE18}C;(P>JNwEQBdS`-e0pK_aLRSbAL+f|wA7eT7jW%E)yeqM z*%}%JUl~}fX34*u9Db64HM|*a`!?m8EW2Id0IKt{<`*>>EMWG(loxR6su?n0(g1=5 zD(+*<zGRZ3g6yEbVtG5P%`W_L6JJNujMq2SwTchE1G8>J^6xA79mvl1R^!&H{Ixho z=CH^MEAVn|ie2RO#7T?sOYcAUT<K0h_R)h*G46IT4f&1Hd1Eq#RWh#}ZF88Ll?rs` z*(Z4!B0Y>`i-GZHge4boFkk%x2413??mg5g);K4agfegL<gqw6{Y0@NSXC+C=)#w` z!P}xQLf0%<k#nlv&425@W$uVxBmISIm0cr*)%`wGHajLQv0GJJ*@!Aco;!Se%i}DG z5jqi&RA(sa2M^{k?zeEgeN|`DUaD8s8v@2;wCCV0i2Z!tRX7Ohtm=ddx-<ke2Z7H? zz5~Ru7YFgG80E#})A?<VR0Cc)f<|Oaoo7sBlX2mfQ9<<{2v_rrXH$Kc=Joec`A=9z zZ;yYmySB1+><)x+__1IjXd&fxPZC%<TakO@Q1h;5o}l}(KfkW+RK{O_{zKiZzyB}x z6esIE&-t}Bx#|}di>mc|0A{NlvT|S1e|f|ctAg!awes^V^^RrxgC5AF3w}q6HJAfu zSDd%5S6dSzB#kZ7=e@+!C#p=gYS6*tzFZ+m`(F*;IF+U5in&{*dgyfk?c3`@lNuc# zf^?f|Ijfm@0vv{jj=x$;ScS_;*h^wgTqOA4dX6=uBR_Uz0NfIL4^!GV2FQP&Kf4@r zRi=-!VAAY8Z^>tXHtCpDqgbg-q+>IOErsq=lRotr8`svBX5TwW2(szT2wnVg^Gi3* zo$#=#%H_Y0I9Yog;;pV%#EDiZQpIAhA_s4c@(XP5OLW?sZ|M!m29<Zicno@I6i90p zGQ-B%%tAlC#koYEcYUt^!=F$0Zsx0vLrbgajgM^-CG=$a=b>Cap2T@>S9f^S;I_K) z(L8(x%ms`jm|t>8X<14_d^Y$A+py*~{0H20BOW<m=I)#|dix2ved_n1Nm;71wQYHf zZu~4={rs6LE-xu<JH>U_xk?he$RN1_-}6Nmh*D$hEE*oK`f(hKb*kIv?fJ6%S}Z=a zWZKyRmKDJHbl4&_X`c<N&_AuUpVwS#k;#OnW;U7P=r5vTX?7Sx+J)VAIAX%@nSM<; zd6GEzauUwIV|z$H2jv8x?!41W6rhycvk|p5-(8Mn9c5TbV%?xEmo^{%c~C{EIrVP0 z7A~LH4^Hh5(+e(}q}&dPEGov`mLjRO!Wt`(V<V&Uud{~>8B+-dMStVVr1|fDkeWOA zMkvy*D4G@OgKH&SvNKs2;l0NkBF3^M(XDL6y_8XX75fsXd#xh%h2>bF^oavrvVF!i zu+21$<2#HsHBAc?srDYo31l@RavOjAKv61dIc3hiJbAqz?6tvDm<bhsYj}OYu=lcg zk|yVBgnIoUuZzTax3Jr{nLTUisNlTgWd?^4)AGpZB156h>Johxhr(zJ9nWa@S37Hw z>2+J<K2vZ$CdzF<eY}k0COb*;SQZ#;%_o5oIbA);GaDbGkY1+ghx|@(@q9PBW8Ki% zouRS(wxz{C#Fy*+m<llyczHKeQ!sx9%q0gdvdY;_=;Kdz4C$Q0z9?zPq)zpsY;^*a ziIj^KHFnVN<_l)Y{K02Rg9Ng5qrdo3jrCCD?GJ@_EVge4XotxmKFhnf%uiW^mP)@y zj`t^w`pI@43-=5s0~dpMu?@=e8O<FoHWyxo8ld_*N!ti!>A2G6oKw+eE93^#?Fe?( z*^-BX`s)c=`q>pPfI5kyrX<9`S+4k`@7p)J-@>4akD!DJ8Qgor>9^^t0sv*ZDsx2{ zWUYg+TGChOF!3R9u3%0rV!&!P!!bVhQqn$A-j{Sd*=B}n?kv0P(9bPlVQd0AHfaSA zy%(VVE;V{@s4KY+$D{rsvwwN{sdvnMOV`mb(<bseCih<8?3@J3M2wqC?fQa)Lxwww zA?)bVO{`Y063i)xK(%O7bK6M6U;(};>-^K+P`brt_29i-WZ55lmQ|?#XMd46GAOeW zFYxB4ONN8@Ka}58m4z4Sx30SPU07UFQ=B>y))8|?HA_WCZYLX_svH!Uz5V?k_#S0u z2`?0Ta=9*1hw?^P72dV<O3Q?}>$|e6;0G32_LkuC8V&+*!A)@6fP9O`v!C6*3d_Ms z#a_(B=Hv18=_q&&2{wHyKbYNY(K<Ii`$gH{>M#yQJ2o+@nF{l-YEY>cw8C$wE}5Ni z-8sOsHfz!<_{QHduwz1muUmVaMY*~a5iR{=c^mD-jC}{PW6!jZuyPXJz*{ufP-8kr z*Q%rOW_7gCN1KVg-yYAjIPxAh&KtM;Du^vooc46KPYn*13v^iciBG4aYT4(#JOkpq z<Ok%7vqmkR{`E)t4HJi~-rV<+`vT}u?#(CFGR=qs-<&KKec_r%2}OK}&X&#iS!@WO zf%{LQmWMiQ7nJJ1K#b*R4-Y-_TeX6&^1umt6sUb4@+~Ru#xbPTL0vDyG^E_{tfNKb zg|LXFMCndswYzhUP4GW4-@!XXMP&yYnqN`YHb0d=SA=`j@mJ|zoC|5*4qD)FOk5wt zSvR<f*AzeZ>|UNgOPG$3ahmS)i@mJRp`yz;>k}z;s=j@cPL|v8p!!p$r1q;9q3vKU z{oL-Um$+n)I!Q^PRK}H`UFRliqbgmk-P0GtZc{(h{uQEI4C(>T8|x3>jEZYI<2P^D z=7JVmP10U`V;OAeo;EC-t@gXW{u(O8(v~JtH}4AO#^LFo{Nd03jlbv??#5q2Ac#&I z`en%&YhzQkNYLiiL4Y3c9Ae?=-pI3z_*Vgj<@U?{4U<qPym_enW>Q9RTOy1?JK`az z93PZF2J$st{?w>AB{sc6sBVFsJt#2Q4)NTo9@YXa^{m(Ld(30DdE0xcOcm%t<m}B3 z+N`xrUpw^8%v{6XlBy>PoFHn$lRT6gVm@_^FF!ZQ-iA!IK;Dw*gDk}uQlCpSH#VXz zR6;V{BtXm4-!gm~Ml-ncEr9Wx&trY8S|8izWPR){8AGC!hPHbSB*fU~jRNpj0gZ!O z8F4wZdZN%Z3F%sPNg+~F=o>=L`Nke2`|B61gA{HPoQ}^Zd=2lUp4)UO1BNBC!C%Ig zEKcRw%<hinO8voSQ>__ti5OZ?MHqiLzNNQFG{w2KRk?+cdl(N<6QcapJzHtGWKjw~ zz2yjhok=e9#KV{^_vnex<VKx;v@Jtp)O<iktJ#eHrcRsw$#Kp;l;dELlhk4?VV7Zx ztScePt_HYs=x2_AZa%iB?tE3m8L1YI5unW1^$w1Wr-8jN6Hc?*w%50Vi`d#SCHoqU zXli85>*c`Nv}&JgPU@!{udWvv6eyQ8DpBe(AVRGbxsp2R+jqQYzdeCNI}6-HCB0lC zG=eHc3%tK`22yT|y9GcES#+NSL#xJJ2zi>h$*H52mZSIQ<}i~!?uSB*@{{RRo)XBt z6EP3z9KTMxaklSj6|bJqTzBaewI5ejwDFFCMhQ<EX^@aV`205Tzu6DYJCR$wGd3tO zlSlRY-s2P4+Is!0ts4Jrzrby+4U(h1x^3RX@%F~YD@UTil_J}k>i@`dK_k~u^qL6* zf(1N<VD)<f-#3P|mD#E^qFe!EjaKn8^YSSl&vT#m!^iN<{H7x##1G}1nzt^#$b{Y+ zsr{;DRTdl>1aq-qgg4NL$8@##;<73}DuMn>R<L1Mg;d|Oc^s)`v054g6mU>n&@rU9 zcz*lU&!q_%N+<IjaSDC9X<G9g(a%<T0;f(2Zn?5AlKk>mR#jB|9|xVfp;#%i;A)_M zrFu5~-s^5YqSKqnq|a$bVX0Y=W7<HB6C3SVz3B`p^D~!A*xW(;h)w1!{vkKO;qhZL z|5LZ1KluEQzs*1)3By}f-t;WA_-LyQJnmqA;`=mr8M83)eIO%1U!>&ON(VcER3cAn z{@F8zrBisTx}i3oAuETF93NHfIVo*LR=xi6`S!*R-N$esN^^a^*F5IVD9T_`Xmq^` z!dq`|G4_4yXQbBbqR-U51><zIZEeu2dqq=%R)n=sB^CXKg<U%(J<+Lfs8+phBiFnx z<QLj1*(t5~<%OqII=LD5R#kK`C(so$;1RQ~J^omdmAU~>h^~_F(Wk$RPNPH%zkZIT zsF6eUg_P>#>4mxq0B%OcBZDnD)NYWanMKO|@Fr&isEdD0hzLFmUA>KWKMBMMP8X0e z{+c4I7xsBKL6JUcTrr)v!oq;c--IQXO8)rEAKjzB{&l6FbTy<XceYavhIM+N1H)^B zKXWi~r=C)?)?O&UtG<k5bq}x`zo9V3Riv50Fx`Y!eTd`gO$BHHUpG;iVI^#fIivuH zxl|$t`-AUYeT(VrYE-nFCit7<^<c&|4`cG2;quQrfbS!81*WDAfp#6wo8Gbt%?P~o z*OH(oo<j2)_tR^V+fI|`%9hp_54sr1UX2AxX81|7(ky(OTGaG(gJ$n2Qi>Ki6wXtM z_4fR8cZIO8?N^+EB4WoAVxdsLA}UcpBs%z6<Am&o{eK1MO<RCF#vCEt*z&gW5zVqx z)|r_%%@PwJy9Ej^+!@L^|GgQ<hxOi<RyT)T1-@9XP|ff_!_l36{rU@8SvF7d?|<+) z(-r=!PoaK*W6>7djx92AKWp#%s6~_VQ_FPt93E&sr#v;jS_9{9I@%R%ap;#ey5xW` zcP;7r!dgr%2tV#HkHUZA#b+P*$bf`@C@S<QW|b~+RaDHBa+Zi#Hhpw>A=_jLmamCE ztg%7U`_2qM@0FA=@($6+RdsM8QQZ5=q>(BaqG{@)KDxiM&jGE0vBx9oJyjTH$213E zO{M}WGUcr^A|5n`HB1BO=sh+Wu~LvggU5)QGa|O1;ipNK!7y~)cEYTX)?{FDOP*yk zlPJY~^K#GxJ>Q@?2-C@FxA(;VYvYLs$=iHEM{2Dq-t!PXWAS;YR^4UiQwRHnUJ9>6 zi0!ZpA=`Jq%D*w>Wd+y8HYMkckKqkIdm8?btv~+8=<%(CP){lk*V_|sesn+}v6(PH zFatywp`==crv&lz@WTkE|6S_rRg`9ejc#V93c^9saaQSZP@8+KZ?!hqtrQrwr&7HV zD8uOt^UH}3^rYByKGB`$Zl6A;j2c-#B4pN*swA=L`o1iaZqoj!NoZ!i1DWuM?omEg zt-jH~HI?~_JxgSt>9QZixD*Ig*&yoli6XUmwdDV;XrF=e{-iZ1X_=mIvS*Rl#r6J9 zmrR4L$Q0NZQqg(}Z8)V{6?CaeaA~a;rG7W-*Ae15I74K&3UTB5XzQ6nCUUFpGtZ_T zY{&DDhRUkGxUE=S=3lK@U?5Q#YRuEkeGpzJ+s8+vykS_Q9o}NtdsnIhoU8o@pUXe~ zqU4jj4vbX-b!bD~R129@OlMfGxvDK;BxT>lYFOdz)_F|s<ZSh)@rR`h%=lmjT}x*2 z;WjnC?ynx^vU-m%?PiOu#=uW`m$be}WRC;4&zkFIf0_5k=I{FX(`1ml7U=~l$W~GN zHGtNn3sWAciXzg32ds3gnk~EgjJMv*mz$e)@)E|Mj}h4dm@fk1xKdWr0=bSNU(Q&Y zVVr466D^GQ7$ZEOy!`!TBzMhp@0<b0&CAT<=CKcIYwJ3?4odE%=e-PSyuqW;1QHa% z>}fD=&Alp6p+iyaG~vME;=MLp*iNO&&wBX?D3Uy@^eWbEgo-kYLcgkJ4CAe%g;IIh zqUC&ksw2cvRm;)!@_!L_Rsn5xQI|%7OL4d0PH?9X9D)URcPRuZP}<@S#UZ%6JG8X8 zyL)kmB85Ur%fR=~%*EWydy~6dJlW@*oOkcF)?TRg;HytAFMJNbM9=#7u@$P`t2-fd z#Uaa1HM8S+6sJ$fUN*h5sHa2~oZPakq}dMs_`0-K`%&`c{dO|R=^-JsNuyg11c`T^ zr8%QD(S~GcvY$#;g-5I1)W{+Zm>O68xK@s`vH!m2TP_+QkDdCKW`!oiM-fd?BN_}Y zzsyi)t{Ngp3Qbq!>tVW!D%uuo?x&M02jayHF>?pVtcd*N$fr{mF<LR0@ssi2nPzs$ zCLg|_L5*CjWePzq48}LXFL0e*sN@jonco7NF0scc2ZuECPHrvWnVH+rX$tjXlHZ@S zi0$s8_UVr!??7i)Oj^C7ls_y{kA*jINpZ&Jczb$KBPI^0gxD+FzY3m=QYhAv{$a^B z&B{teR%xx>DE6Pe;8HI8KY#3i_hYtNkAM5W@Rl(&kYhzz&<+TYZYbl2ku^(I=D}mU zbPJ-pEfigT3aLo}Ym)KzirIAP>!qTxhj=m2M}cg+jmATJH(KS@!>|F_`RYSYcBIkf z5m)*8Y^-8YyhuF`9B+Rvu_|}dg7|k9vXO1agDmByxCSHlO7FzQt(}Mh)fiI6ROeUU zxO(G<wkh=d%i~uBn_|HzBZB5rMU_^$9Ffsem?>q<{<?>y+gN>LEewd@%#8Fk<q98z zCF+g8f6b(hE#H5}jN!_lYH<>7Zp{*tsVr?fXy`lG`mU^0b$P>@8`Dt@l`&YN`lDQu zk<)+~qyqCez8TK1P5@tD%OX|8loMXUY6h;oOMDL=loS6iANErDQFNXCS$~>Pc+C6T zS2w5JH1QR)lzPc4MX+^b{{<VG>}1O=Foqgo)fAUe5U)U=Egj8pRMd1dQ-c<p(N&ja z;5B$87!KHECd=~TE|p*@6=wY6YhdOztuavajvC@4Vc_*F<D$|t>Y~5AUa$-G895y~ z$PpJ^K+$Fw8DpY_+*C$i47!B&gyBiPD_314=4~lQMPuAM9OTJv%%0KgXd>{pUYfbO z?||w>*VgcPCH0OPSVf~R%0gEywPcJi6OSS@f1nAdp2ncrpuLKp#NbI;bksaroOGgc zOU2Xg!syW|Y?|ntg>^1hb|g98w)FMkUF(-C`HajV6c!NyHo)EYkpnwF1!WzZLQ>Ow zw-Ro#qDac*{?iYxiD&<(j{|^ZZwIJcY68s4Yblrl?pnp^qMZJ3!~Oqg+W-HbOm$N_ z`8v#TB7bm6G8Y}l_Il(|1$NQ^M|AlDDMXT^%)iA(otwDJPWoGnyfq7I8$4o*bkp)} zQ;@_O%O4*`&Q%T&X84fpm_j4$Yb*`E?o?)wIO?LRk{0T@zn$VKD)hYEp!h39oudPH zKD7?jIiV&YT!+zDp`l64W?*B<za@ZcI=q&6E%jPN2k!R;2kIwgpc<3qH(y6eT;0S2 zjvQsxmM6caV8&Pu3V5$ojOLppVzqj?_sC|%w!oq=7nRcfi^c7fHvCNOzxkhW5y12O z{ImYOHTpij#jCyMLA^Bd&)D6S7NtCd2-6{=U>A|KT#vR-2T_GA(w|`1bfhU;2TBqf zStg&qglAo-g%Rqha$~hMtI%eKa4J*w>a7!zasNB9KSd>hMev#EMqimXZownOJ5fH8 z4n(3#xw>DV?exj~N#V%uF^-C?gC6{%8b2G=?hN6OOseyN7M-^m0&KLj>Pr)QN&X?C z!4%;`e41mfucwA5nU^<VYRO^#beBA%NH%eHaw{qUg+?j8llAE^^){+Qe;WVJc=NM} z4nAsCb;btnIG?w!cn6!6uKM*<Zrm*36H(lRdu@JTsa-z%3VgHbm0i-vm8*i3Q~AZ5 zSU`J)VLH;FEq=<?UxYH-rl6&l{rO4R<bV1*V!sP(8VslgelkpdH!asiS%D0j)GCV0 z2})8lsLQJwR(=!~CQ`d4h|^`4&vdkYz^jw3Y#cQrce(u|kan4yk)~$U#F1}iX1g0s z&}4VbgqKDSnOHMEi8rV$!R5l!C$_X031WSGe0%^gmy7Y5XoJM1$n!$-ZKe&Cp_(j# z^&Ji6tlVkIlw2l|z=1*}3~5PW>aejT!yEc`8@g0%VrpW{vJ(0VOMLIpZlX#VUQRrs zG(+47LX-h{EQ$(HhS!&mHW_MIE+<H{D%awQ!KtOr81l08$|w8pjMI$J^hot{Xib$$ zjC2(=b8{dS26Xi^SKdvbPJnTTp{bb4ks)Uyfg0?0>K03tHs_2jMJN|9e9oj}MK2c& zED#blc{@G~05{&_8Vc>{(Ue;iz_hC}E9sA_)Df)yAbddpu<MTg5vB`^2#vl~Dna}> zkz7!mK*(h0R6U+5?|^_z(o)^rp3Fy1F}G6T-90qH2mX;!F#di+-Ofx7Lu>|Tfe0a^ zw8uh31jAYwnGCvgpW$@=W|69#s+^8MZWIxX5M9A7GPZy~6M&6r#02CSYi(Hu5d3aK z7UD#27o-2~ijA&qz=lE$*+Una>H*@Q%k;1Vz7*ERd_xczOqznslxA|uk%@F5BoxUm z8Do&RV~SXXBFoeV*{dxehI2-z*YDZHwXp>C$|fHy(^d}G_I3w?QE(8cQ3a?~#?e!( zxpy5BWd@*vqx9^Y4R7KxEME9LI{Xw}Mtsf}9>I(2KERC1R^1T({rSXP5#GU49TeXE zBa0zi)5I4M&xfK|TeL1rAsI7;l^!i05#6eXS|%3zy7#nv)RGR=ej<gJZE%xq*<EsP z(WV>*8Cz@2OP7r0s%U8sUbdcU7RWzl>?EN+x4}q<0_<giiExq$5xD$3g8|~WYfWT% z1}OM;r&@l(XAc%xunAD4lOO`$07t%ZtJ49A<)A$ln8q_Kq{;qkk}_wb%7$BEw*`m{ z4@Gt_KP!De*&0!#BW*iaD%_wzSfL%<OHcP_j10Dii!9Q^F{2CsA>l(mPwZkDO|YX{ zsp{r+gw3nzPpJu6zFoGTrjOz;+$pzoo89E#@y^b;br@??SnZ41WIlS~<E%3;W@rAX z8_Dj`d&P0C{{8*h{rOn+P;u=B<=6Ri6kiw%;#fn3)g--%$frIyMG#q5^S`#*(+Yft z3a?zmQPq^Yx4YP>n{Nn%hJ4uVttf19Tv>^<dK7g2M65rk2_nCCIQ}Zl#ETOx&W+-J z_{D{5K8Vc%)nJlwiPn!ulrrK}g=2Y6z$JDz_XBI~c2q{Se>7ProAZan^BIv1#$Vb( z*&WHe=!tjC2RsxQ4~u#9KI{Xwvyl{rLf2{&n^mtdiwnNJnppj$`;F+k+rS@dnyKsJ z&R-6+_tf+L#|-tH^w_bl<4wfHcKc@UX$by~RI)+ND#!>{tyuL$Fl7=z2yvmINevoT zRxM#F@~s*GUqZPr{?_g^s=it;eAs1N#C){IeG%AbCSclnH7$^L4f8)@_w|$cNl~Bk z|4yAXWvwmaP)~+YWhUQ%D8mqF2eKnrh_PYg753l-b%zPz0!(kuIeK~+0bO8$&ugfB zL&_s9OH3Q7Ye|1JL4gZX^!BBA96ll|4r&9)lY8b+o5N@Qn*W2BTH?hvxAKh#Si_2C z%yl8QZm|$3;79H={kK8gTii;UJ5?k;Sruf96B3eIK($S72;uEEd=vU~5u--=CJvck zVNznr`bSGUg&WK*ldlO|*QA9t%P8ZNde1K72i@(_?D&`f8l|dg-$p6aD@hO;T_yw< z(4_9x^v;ryY&L{4(NjZ*pJ7;r-m9nnivgdsWe7rkH^q$z4sbV067NhV<%N&1!cQ@u zqtm&50o3q=fuhNM6kGzVH;{PCUmxgUt*uY9{1`ftN~8OiqHYx9HQ~a^Z3ndXOy4^+ zD1v~_s3VJbU4i+0m=4%U$i*jTVfB1;_|!{qhdDn{<UkY@2jlR6FrZr(vGNK7WP$@G zZx?;xO;x@$Q!o_dAXe#zYb~0``ia5DDvdPcVWfDUUIw4;jdJ`ahUlt#X-+mGpomME zjRqHCY(3<4B;`?Qr2}?<15#X;U}dysk&qz1Dr<fV@1kqlsJGF=$Q-?+LJ=PcX(qK^ zp+V-1dc(6zS(2YbN^x}w$B5}(3D;k{h7deLR0PMsdPXu-#*XkvJv>AV1W&SC<ctK` zB41WajKB@Z7y=;1YX^af1~L7G54XJPj$7az&AKqD+2ZCsN#Bom2cn--^gg7UWCi*D z`I;G5i^1^th3a;7ob+t&eqtLg%R9(;_U9C((a<WZHUd3`j;Gqr1*E{859%cvChg#z zod<+8>B?|Ks$~HK(_66Z)=Sw-OsS7d-@bl&O)_r&E6@DX7u(KpNW<i{K<Oxrw}LQ! z@onU)o$Op~uDmulRi{F$!}6=x*n&S@VjYWWr<=S%SnVqT*|H-oUnVQ5cfh&ykoPL> z@5tq9z2#eFE9kGpyj#k2KUXVF@onS!nk}avd^>0~<9@3;l&t1fbSo$HOX8c&V!q7N ztMFHqgr*P%l+Zbi=WsqJ%V8m}Wr!s@r>Uk=_sBd3d0s`o9U9hKzBep+{apX~k8#L) zZradY&}^PkW>z05c{BQbA70~5kWDyuK6zF;^OCJ%D~Ai9?}|9!8|>Kf`W;4_hQd(e zq@B`|#7vjc3K@~Jx4YQyG!XM$k+Hd)g|zvz2t+HwCaE&(afDaW2iG!R3)5rHdoGc3 zIlqK&K5e#rTGmF>s$O241VxN2n=4<P8AEMSwmg0IiKFh+B2iji#j3;#y5I7@Dk&Ri z_13cMB&0v27HceftyOm76H5HxAx=~pGoYT3J^|y`YR{39#ZRue?Nq4joUv3ntb2+0 z{NlzTnP&1Gh_78Jbt|Irhwv^UzVxXzg9$N1)W(}ZIV4CLz2lY(5gUA-C0Qyhf|s8# zk%EGC_d_^B9V3BhH7l5C4F%`F{ufBx3)KYSz)Xp?RJW%{*v+`lBh;NuI+Gs9C}F^& z0e2Dh=!3bf$IWX?U9LQUH>>H`!GKTfJxZy!fUToESA4xv=!p(nZCl!g6!Huo>R9<n zknwsy-T&%(diwtKbid)<x`b+1_QutYwDWRv;P0985B5@r(FIq^livc~2RGrI^iJcW z=6O<Pp_`|=3;E-P`U)XmSMRWv$c*fkVtxg<IUD=GaDBQ#Wg^jjl|A+_ZJx3h<<G7h z&b(olG}NE^5H4Oxw}m1EKpeMgm2`#xtU!px+kXfJks^ZaASC%@MD)N{;so1doB-6+ z!IkF20_JZf?2Am}t);Mw07VHp$_<ya;~Wc3p(IzW<tS#^_tDegjugh+FY8NJlo3;h zbH{upeesQDm6~UBqB!YR^%KYH)6?GzsV2Jv3rNStzA?9oQA_5pzjnjYP)55421Wpx z+pj+0(?9k{F`tUa*<Yel#Ys$7J3ky*W%SurUu_2!BLptXr9>I`IzR1cr!B5qYou_A z$Vx1YytaG>SNHUEt1lKNV#p=IQY<&Ex$I>d5;2qi{eqw5II^#M46lwIgpI+u5>~3b z<m?d+v`klsYG*8nu|Ox%;0+)k!5j!AK)|6uLlC6q9PJOGcjtIK7hiB$30tZ4QA8m& zyx`No55VB2j0_wngGVBaU~grMqA1yHHZVC?)!6x+d~R;jFf8p>dWV_%y((Rzt)WUV zFPY!&EAnoNs%(CRh@e#HAk)+fpAaz!yBwma1>(oPcu->w|Id-*O^4xI-V~T8Y0m`r zzb0POU)G}Dv)5vIK?LKkd<O1R&UOx4(rQ}Zo55X<xh*w@HO~C};>x1MlSVKp75{+B z5=|6ZWyLAJmbxzI2}LBPO<jW=>&1fEy((oCT?CX!Fg9H<0s>!H_5ROY0U|YZS1Kv8 z%UmzsPIjc>18y8jY!?)C#T;E$<%k+e$D4W#5*y`kN|5E+43Mg>i$uB!gRbOwa3#3g zOye^Ytu%k97%UUOCTC%|lLc>h<3kAAAD^l9JL3Zf_K-fuDX0^Jjgz&Dfg81M>;EK8 zj?hBJK&JikL2G?vh!|yv1#)<#Aw(ST8=6B;1Hq1W_35#RPwwB_Jo1~GU-(p;e-V>O z&7MZ6G&=Xj8A9wh+Wk4FIoI{voaOXXKTNOv(A)o(1-reO<FwscHUgDiiU<!ElTTzG zos7;2j39%9%o_xHI=G#*{C4q;${Jtpqv>TU5L-G0?^(Lo8-$FCN(602f=`!b<A>v- zy4e{9C1Zh*u{g01@Q9_Y(8&A_fJP!8pZ>mCY+LiYJLt&synuJOu1L1xekCn&aHA_Z zyRs5ygg77~(Xu-tBSUXU4E*SZgO`F85EGe{=``CJAas+3ppjU3qT7LiXqXVE?i+X? zqDB4zJ0AiH$rx*dha^uSCO9S;25qpa=PXrD9NnlEYLt&q(bk5VEfTxvWc>Yj^Ksly zqq3Rrq204yE{}We3*Oo3@Shhx(&8q3{=lhj1ZW|9H($n>ndK)hC~U$o0G71jzyvI@ zpzrYTC84cg%ZSujFq%&ZSYrS6?Uy}l6W?vtDJk>mwC#<S&g3Fd;~9T%mad?-=Degm zhK?Y|`H1fC1#Y7;oSV0T6zSH^X}}}QTZ4&AD(CmgA9UJ{mpm`3Z(98LStBZHjGlLw zfA&*P`(q~0smHfDe(>)bJl-`cB~|jCx{kH8D}5Br=ImvCQr3Fu{*Mi*+TMaHfDP2Z z6Jr6@?O_x3+)PiOcKBS5g<;E(z$Z*xyTL0dre^G+Ao7N}<*cd)qE9P^z>Vj-w)j$C z7gZ5>YCB0iX2gjFDG)jz?bUSS$C&MXV_Cm0WeSTG8OMjWekvwjqXOf;@Uazg!FAvI zKv<A>Gy|}WDo+d^$#pa+_5LhKfWai%IA_gK3}s8K4-KLa46jRbiDpDs(yBv+f42BX z`*uIxOR||qDTly;L{HLeRrUL+1{A6ly$1l49`YMLWgViBgiB6`3yT~1&Ni8fG&a6v z<MRELsj}NJU>F_MXQaU=h-{4&`W)v<beW+>HAau&L#fN$$4EqAs8)IP*;B_nMdRX7 zu|jcz!p~(QZE<mZ_G+zyd}2iN%EXu$a1O|ME$FR0nrzs~hw+{fJM{iRf#ca#<)B2p z>B<pQYs1N8WX1$N9aW5KwONmy&S*RRD9#vrt?+%q!&&Ovz^O}5D!7i@CeEweorHxr z*9P>xd4_&Gx_mawM!9g@2lItbt_YQwJKfkE75Dlm=Wc_c`!@<Jxd<jM2C*m|A5apb zXC3sU_wUna3A8^si_dD>)|F#BWwJ$Aj>HV4);OIa@f)g}`a0&9-kY7&d@=1bG$9g` zrc8h`woacI@0@Aj1_gt~oujWM7W&kx7a9G}p!o@Bfz$}FS?=vtI(EgfO4u3_ADtG~ zJvx*qZFpK%hIE)<u#akpUCpj?G^kr|@LH+b)eXs!XKnKnE*Aq9=Vx*Au^^(0GFZE} z%~{Pw)VEJxzS0J*_Vr#h{`s+9()RJ>d&=Bl;%v%~DfYPQZ%<F_{)|kxc(U76H24a* zI8i}2Ee(n4#=W0BWKeq%%iS|f#6@~~x+?}EP@khfSv^iPw;r`L<XG-sj?CizX>kB% zw9+EpMrTmlfZyAhe{s6rUz_yqGBz2bV~9W@QsV<bJ+xoIh#qH&bP39SgZ&iBAd&UC z<oE?83L*<|q(i;}0B_<T-D*2d1S5DhZO-t0jy^rI1D4j3oYod8HG5#bPLO7>G$R{G z%NiT=dAC9&&kf2#@8K)R3yEPyP~p`Ys#PR%LNoy@dD_8IbYz?(+>zSF5XlD1m6S}C znmeiGkJ86(Te#+d9qE~k2-v{}njR>)!G#L_<;rR5t(%K~pXvaZX{}c$%`Qa`@c(%R z7K$EpePU}>6sFI~Alw?IdS2D<&&-5#s!tZ;Td)q8#Y)zy*=nR@xtn<EIz}qeQngIs zP8tTq25t7jAI1;X&Yl0;pMS|`{r08XG=v0vb#@*2E?Y_qAMyDhnEE-np4;ko=#6Cc z%3;W}Pr&H&yF@s$4_gol%4TD*I!nGbK4u+AW*nqVr0Mnh+4NRt%~oHUV{FK*1rx9{ z^%*hQBIC}c^2F~u5OdNpL9>;jr~orm2Esa*AV~xtiN=i9kg=#z_Rz`)pns8(*>B~p zXYy|&xtFUj&?W}Nu9QGHN_9rrEJ!^Tqa-*Cg9Jv77&E}1EsJEebJFS^0hU3piJ%A) z<gR;z2~^A~2|u2H?&s&17JHzSitAu77;1Tg9%a5DU*`Iy^{=FmAcJ0>DX33{HddoH zleHp`rKY7`-JKhTK2K*|pUJFWf+yXFV@C<4eg2YF0e445(OlG*`ni;TMDi`3HJ?@% zk~J&kwk*_**n98%F8J1*_8yp(8X2pQ2KcH##GR8NH=zhQP(~cW%(vzbB=NAIpvMyy z@TL%!okSNQR%Ad#;_l10Sh5T5E{1B-8A@bu`kP$Ad+BA<m7DL#j{13oMTsuJ>_MDs z$==LN5`4VQJj`C49mb&!GCc_pA_42b8pZBNDp5$udN{cWFQli&Tx*071};~rB2l4B z=$@WFQ#MXgFk;8&q%O?IQ2jS}Y}0Bi<U9PvC;5fU%j4^wnySEy4o$#g5J^naWF;e# zSPf2Lmq98gp>G;iLsdyNMz)CtOaTSa$HP$32Q38HnQ-teI>DOL<*xui#ZL4(oIC-! zHo?X0ZO;eA3!f*2YHkOFgQQ1Zg85m3GIfl|9!tJ*5=B%foh5SH$nzh$CpD)QS}HXR zDJ5w;R0$dbW-?AJoSr#>69sJ)l!E9|cH(;PElUJ}MkxzvvGmc@-}Q<?LFNy4jA6K2 z!}PZ;yyzBNY%V|xkENh=@(&Ts%hPQJ!#HloDFR%p9}|db@sA=2>ZO>paFc94aZ%`n zb|1)glM}G1&2{i`K#w+jnqV-*(1W9vp^=n^k$3X4#wn2`KihP=9I=CBk4;E!lRG&u z|Ejyap_F)f`rnHSo)+5yODT?k7dU<w6@}igDdCj+?j?aG3{I>1N|R{5R#zHvio6!` zZ@+K-tm?MBor*!Gm`F|(eex<%((}R!Ikkki_-gBg&o2e!0~>^cjKAK@h%$`xt)=|& zqeX5EO-prVt$!wxF=GOIcA8#uF9^b79d#khfs?~J01nm<PsM`{N`gqJsKtW_bNh)X zCioobxCv;;X$oV1RS^RNKR=`oWF142b{K<!KxcFm5!dN(TxtVCGYSqQ0Au92Bce`S zRrK0Y9Lv1gt@)>dcQF_Dq|3u7TvU3L(Y(B(tyM*C^@ZbWo#nRrtZTGJvlap24#xn4 zE~VI{f8NQ$@toY;{8F3z8eJd%eSF?X-|yCg=Lq8{ih2ib%XKUFqXp;}t63P<NEXUj zmza+{7|0{2=pWa_60tJ13s1CXsfZZ=yeH4C`_S<12O)8~nrn1lswzT-0xt6XhG~UW zSM9eDit!gd*RuUc7D$`9xmxT0t~;HBM?YjZpUmzV+euooXQ>gExVMq4Y0IP2A@of; zhIT8KkcA?TQ@Hcn`y!#@2dU-K33O|vRpPQ<q9`B@@aYy41M%NOiyZ)h1Q7d<JK2;v z47|$>wr{-1U>s7`qf!AtU<kxL5$NsoZFXVS%=#9G>BuoDSDz)(xj|4A;fQqjU;&e2 zxXr$;-)Vx0=ZiwCfyF?W;;2sR#O1R<DiB7YaM}0l62br1_}$dQ&C8lH-&QPIZm=x3 z?K|)3I{g-FDpvkRF1g>Sg^+=u!%(4Ea!-gDI!jP15N|7fv$r1bxjFj4N7+gP)#2yo zJIq<<+Qk`ESPOj+_n{+RzL&|0Zs>!p=5j=b{G!1NpF276YaNn<T$8u1PPqJ#HO`B8 zhlmq`zh`Tc#`aeTmTc?QObCc{&-ed+s`ZEjY`hF3P;O~zraLkoLO3^+sng0bNSXl~ zLk$6ecM7+FDX8v1XLigEPxed#9TR?j62fzA8i|Uf*o}#p5{9DCTWfa-9fk;$0Lhl| z5Yen+`a=opXmV`s_HU6CkSKUifsaU0ddtIq|2clV{ri6B0>%1bZIEyMT|aLGq7bR3 z1;Uq5yqQTJ4TaS5Ur`V@rSEeuke;5r#+|yVqgj7AMk?=lB41lY<l3>Z(KMwf%RN2` zKn4)kaORAt@>e=zc&bBdz>sGnB3sskP}YCql%#grsovg}3!?N2z%ey>@t}FgWO_-m zhtz-mWA}=x*AfU9n<jLmipNZ1XweU|%OCQLA0BFVH|u|0b+++KzR?@V^7d2S<bdC@ zcN1B3gCs`65FVIPhUlgT?)P?NB>IDFq|xA!0u4>52>Frn@zKGP27<q>P@~7AUTNvN z7x@QQ>kbDp<9zgBk#r(Eqd@tD>j?IsN0IBmI_6kBJ$oF*J@C3h!YaY<Otnf#sill* zZ^erIDcr&W|2Fjd>5p|!Nt4ETCcPSwG8-=-0433u3KQrmK~rv%xl;4(e!Tt3Ymjc% z|8TXV*hr6?-`U)!>C|FY_j#^{OaUua&IoRlt65Dormu5Pz+z}csvUOBE5RgQ+1vdv z{AkygJXPsue_z7v>YcWrGee%sYMQT3PNZGm_Fw+Rxir?D0N}#R_vs%8#4ht+N|6mJ zTb=37>QIcg?(>-rz{W+A1ITOYfyhi`@$w%)IfxAS{WH8jF|7d>m7ouR1~N4C?xUV! zqF79618F1_1cha+*}eQ55`QA{J~c*|H8-PWkmM6v+Be=}TTM%F<od_!B%zFN(@qDP z6dUVK6wY(2*;6eg3njCM%GnL12Y=xai_yiEx>ob8Ord(be>R_PtA)oK3!?i=_Y+cg zaiy=}hnT|cQl8YmXx_IuO{zVVye8yjcGPio4$bazrCn{DUu-v97}C`-<HM_LS<H3x zPKO;W^GUH01l~9xkE?O$wl4W)`#zkncDVV$elVsvwP6nM=8um^pc-$8e{K?LT2|&Y zYGwG(U-VISo{#Fw9ob)o(sEA)EsZj2Z#Q1-sh>X&Oj{Q~%BaY%==g+xF(7l%sq!g` zhZyiu3nXxQa`hqd1|xf-TJV9oFnjBTL3D`qtmt%Vo3`~>D#7LvCzugTiw9)R<@*Yb zy!-Yl(5XpMF(Tw`9?V3M;2y`Bwrjt05BsB%>kN7pHMGwl<R5G<F0+CR;RgD?ekX%} zBpY^r`gyDlr_dDZ?uShihL#apx~GKiD)6MeN~@2(GW<!%`rYWd?~fOnYmJnFO~cLd zDnGkSEQ2sj1($%a+WxgFtJW74UOq7?Nk71ZI7fz*I9q&Oi0;;kmxZ552!0mYB@N+x z3z3xn9}dok=sTkfV-=}S(uu4sAHA-xBY#vNM^v!>&-s4FdyjBOmh+*D5x1?v90Fxu zcqK}0KwDtuO_@A)^v67`@X`oEY21f07+fgjbm0F1k;<}w-nrA?haO<NOhtk!FWrP3 z=<Xn}J+YLcal*yZYa7f+u~2cdQnE^|ozY%9i@ZpXl5lPw97exF%$X$eRhMJgt2_rs zN8Q{YBVivh;d)wAwrIxl7F7?${{({(3L2e-3745zFwxrPG9LrkjQmlN<dA#MLVF(u zH4=tZ62npA;w+(;)UTA@6;J_9oE(!y)?zUYxE&Qd0>lX!p|+usf>tp(^8AVzk;)Uf z#2H9<h$OK%h1h%vkp;a3HZb){XMA2~PKBt}>Eh>jNx|vr@nP2Kf=ZD}`I#cV%8+tR zL_rUh7e2So^DXy$r?cx8Q5XbD;Y$`*_1toMxQi)9sKb^2Z~q-=3J@i8qR8ZG&*4mi z*J{CauCtHSGjgdgkdGI$nGpgLDijEbmM50eRjTHk?9vPxN9@A&>#vB7j_G?h#KMu2 zv8c#qzeLag82Km(f>a*B%bNueD2cDC2r8Kk;8|v!!m$iL`}vj=kLOY?IqfNI*{P`$ z&03d`6qpDp{E_GI=FDvEv`Gbn2ZBohb+TzePzrYI3?Z)s9Tg>sW|p#~j6S7Q+bT0R zy@|?3juGan#S(ZjJBg`{+=B=Z1g0C1tLvB7rDLNP!Yj2UzwWig(40?1k91p6wmwJE z7PW<NmzRXGAR9yqK=v5zU-<lxP7(e2%;z!HWQx<n#8zj-px8ExAhxl+B<(*w&m#%w zB+o`52nd-vqGc%Ll@-VDaxBS{q7aa$S5&oq52;^yHzD<G`N9l22{BlPQEq^f{gH43 z!q1!}^Kw%3f!<*cQSN_(m2GFJ*;V145Q01w(((@hv47Fan@?58Y;=5;X|ow4%48M_ z3CC4RWcD2RG!2A2wwfQ5e)H<R22v%CjuJJLv7C&>cgC)sM%RMtmorcL@wbj@8N!ff zwl2&pwwkx@{RNUl6vWI%0z)m-@K+^)BR4UkP!Up&fJWCh@_X9yb>P*Ai@t{S+yITr z5=GTew52Z^E;KqyoJ``T9@~zZGwex8f5Cowdq}DWs**+nu_iGueD1-|{@5yO%NEBw zmUUbiLgt=CaF9xrtLkBO>Ny~aBr$MRuyI}Bs0r#2r-BridyIoq_FHIeQg&(p;BOjp zjQ3VwliKFyLyb-ukUt-HF39BJ#C<A)kVo4^gSb+^QkU=jWQU1h)%(na>eKS-v&MzF zBh0zjqSxv6Dpc{QHr*nr%+7Zx*CrRd@BFLP8|d+V38TuYdC1R++{@=B&y=v<r&OO+ zrffUR;|L8}!X2`MAw`OydMH(V*4lDAjEYmQ0%iAR59!W2tWk#{BOOx~gK9`H{*#=? z%&x2e3jkJsA4C>gjFyzaj^Rv5fy-p;FwDut`6<|=Tu=^^yeO8H8fxkwfI0?658dTO zEpufDX<UH5$Y|^Qr;mG*mk{0i`K*6ubtFw5K)%ur!NE^$j^jYybtcTCmSAa+)}cVq zW|pgdq$pMK2{5BI$5Y+DsSD95EIH}hcaWFe2ZVuVm7|?&m~kfvZAAU7MEzv-q4kwU zxTy?_dK9@hjtiFm0{g4W9=76nCWewj&m!N!q8LXf(C4&4U)tkpi4T*-k?LDP)M>qf z0U0?gSH5bl6bb6W&8*+y4C~jnIs@uL)XvAfcaw3NWl>3fI+>#BPTGG!B=pn=7@Mst z%<>aGzf(^}0~5P?E6g^#W86|^Ka%~{W6V~;_2;UVlCnHI?**O+4)4n4s446aOztB& zjNJN%wsA~`AtfqQ>w!fvW^-x+H?%lZ9<zSX$;H+Nn&~K`dosM#&tJv$|MYPpI*Qj+ zA734kx}{(ap&ktIT0zK1#p#I-!FX<bWBhwAd?DV9O-NsrSdZ!yt(pu~D|Hf}1KSzJ zl#YjE&<7Xj`d%p$SL7H2Bvv)@SJRO@m60j4cA_!iV5~3Z-P@y^f>K*J9V5<w^}qz_ zR=HlbE(Y;_$cj$CHRYkRR&;ni4VdR4P$Tx1A4|Jc4koBA)}Cr|<Y8LzX)k(o8(m+R zIy#lm@L(BI)CXmP1pmG@Y}}o8oVSX={E_}iMZUU9zy~o_EziTMFnvw16YvT)r`>XO zRW1FenakjIty^EhY;!PRW=zTYc!V~Q9aL?3bm&8TO;K2&EIFIBRLhbEXMRr7GduPJ zx?ev2rHo^T2syv-xs(YNJ>q-z?@iW<;}>Pb9Zk<J4oGNecqrRfb)_t~(O|I3^g)dK zjI>B1mLKayYT1jxsW$`4-rj-I(S(ZDD7p9Ph>#BcJiQNF#MXBbuzX-A^Q1NiOX@D` zr5eo*pC0qtVP+fjW=xH><-?G&eRU^L{uCI^XGIzndA(Psmv>NAJetVr3M$F4V!?U{ za1&l$j*S?vMH?S2v`EymWUjLm7AJJ-7%<9xM=nsnk?{$x%YHP19&R-n1Rmup`QToc zPy)QpbvF9k-#DA<RS7m(I}kXtY;9HlYG?C}A$#h<(lKh(_MlX|lFv{tS%=%8s7t@x zlq<e0!tZ{8MvUUV+&E6QpprnIb%J{MVy*Oqqp`}-Q>g>|U%t#=g?`bk;ZZYzo+%W~ z2Uk5uJ}EIgCjiW~1>H*Hn_Qi!q_JZMzdKN+sd*OjC;o`8!Nl8A8KuWj=HeMc8zXV( z{)3L6!*xfI_0Z%{xr@83%3A_iAl3-O2|4Io8;}rN&ls5Gc*<UF?yM2IKZVO)M4e8c zYG;S4xF5O$yzcSvcJ%F@miwSZRVqtNX#vvV;wK_2$YKj6uQo~+$mOrho?1G57u{e| zC2G-ML{ad2w1T8VZ;I$%ngx2!eC4joxQm7@D25^C#g1m9JbPtABVzkorsTZVqk-VI zgq{XA&~Vw?CvoP!FHxp0XKk!qq7&!i@(J$)ea5nlk?#cFUF~G(b;5puYBLsmI1%2} zqTYa`bbc*qrE~mJKeuuMqFZ!hH2*T{lzTmMo3hKc)zTy?KPfS<0d@J(=soP-%5^E( zoDJAB%-WymqCF}k-X<@`LBx1m{#2-vMbJ8>xHEoWAa%QP)G6gfR5%~DNS(vH-7K8t z7vw7cX}oSQmlo<bhHtVwB4%V=C^-2uQ#9qc7zOhhsbrSXvp_EKC?&u~RDH6*@HJum zr|^A`^}#%D#S^~*tj~c~>~k)swylI7!U4?@#cwbMGCK9ju0`Wwmrb}jgbFJ8SF}pN zU(&1DQ(QxuY8NiAtHsky=@JibF{(~P(g?vKNzsAxtw7b0Wlr==@m!F*4t63ugE(ZT zJ~~iE*1bQJ!wq0_7n;i4ePt>^kA8PTf~Qzg02CW!?eR>*`p=)_m%_7uOZnJ}#Hf$* z*aatdrKp!3bD~^QzMK&kd3Gg70vV!x%mha9{x+QxfeaRig^<mQ+AjD54_jy&ETtL} zmDCiO9zEM{XyBr4yXDkES3tIvbUHdPgpIoa@*O|YZ_rTme|Bl}anLFrlUT?IG=o<B zrEpebw_g4ioN<Xo=)aH7Q>rDV#~oh?aUq#Kh#-nM-8WoawPw%2N#(u5P>Q$bR=zJI z()LKq_TwH^@Jv*Ynka>mY}Sl6?>)baQ<q3BQS#wzU~@5D|7{d<?w%*LugH`4BhTtE zV8p3MM%K;56ndX$NoQ26mmHPKT)Vh|W>1L)&A77`&)BiOE8wS^sz8h@@U)LC^GIvq z<S3uT=4M8rDroqxe|jmm{-6HBM7J&edlk)Ct0$XZgHG>O1SOF=xsUq~2H||-7@iC} z_bF|P765aI00Xit2@{gMw;><ni2#8q;%t9;JGCZ~t5UWVIHe5KZ_9wfS(I?ja)D+= z^AT7+pyi#g^M=xH`?nWu*!F;+XAdmL#`ou*zYEi<1^ZSW`%T7_KHICYQsR+4Lplme z2FW$~Kv6I_SB}*4sFzL$m;TbOB`d#=Vu+ksF|@vtC3LGbQ{iD&0`2$KM=Y_5`za<Q zp)V}TH!}$)VMvD3$4EHyLGLU*{cOdx9#cY-JhNQclk>gUKBc7rcPc2@?g({j@EK&{ zsN9(>$Z?qVDJa>&>k=s?R$UUc7C@%yTbWdP)LdEP8ZY#qm-Y2SlTma9Hb;X{OB~P9 zsZNg;9J?wnnof!wYsorsgg2|^;P+*ZA)-*(ET1Q=dDbT!t_Y(hyUacp!EUvN8)bSu zhT!acSzvZ9vN*EAL!4Jl9=SiB+jlpfBqk%Od7g(0m~9{gMD(}HQj$r7LS4@@!FZA? zbId!YY0#0`dvX)2<5i@clgeC+#|~Iw!iB9pe89j{g-4jG(o4A_2=X3p;?+b#f9Mlo zNSD$`GGfvyrVZkKb7tr=8fba?Nj2!0O*zucr#;3-F%^3ex&uWjaZs2N^7VJQp99~v z&#)FC${ar+35KrWtjLBy(laQF<>sby&g{>4EbW(l_Za;A9{tMUaW#sBkGf9Z6f1kv zh7KsbJq2P2|MM5^dG;T>g7q2@y?0Y)Qy0{F&>AG{_SV<uu?>r^eQSMxc{gfovBvhP z-~B5x<<%L%eXb7%T99Nb9iM*>Xs>ZAKX|n<tlX4^-0970Q8h-HS%bNeL6M`dcNp_w z<*)1x4uvDKA|DOqRX#RSo+KAydp$Ul$MNO6t8}#MDupK%L$3<e2b-d}1AJO@DnyiT z3hLKwxzQ-X;uteb`C7UJ>4AgFfi}sWsb=L~UX(u|#V|rS&@mI!h|ND5MS};R{FmSU z#_>J2@m8c^D~42i1jk-uMzp{TUxD&fNI!~Ybu6(shLtT97K)s`kgeh8b%I||PBy(H z`N(U&&Fn$3(uC<jG{9<?RDGS;Bu*`H#d(!Bad>9<jw)H)OZ^<HJkN)ZKKs}G;b$06 zNp_^rxLyWlq`V^dq^(lqHvqZ%BPIy-VyBgRO+0B!6n5KV>xyCbJS28f85tq!Q($Gu zpyDO)rOZU1)h``G?aidhsjP?nM|l6i9zKPeX{=gTQ<5q{<QIHkX7t>(q7RITd%@6^ zeY+&AkHw`bz3Fq2i(dfC^uBwPsTxLF*G(qZs8p8zR%1DxNLAqyHfLk~aFJUhzBu~_ z`{Cls_TyQ&mk*Qn=};1kQ%4fAy51UAwJ^rEJY9qEv)DHUye~!r>~M_!3)wqPJQHr1 z9cN00=KgUG&UMqT(efuTsrN)QIQC{>T!tx5WRkP?1r+({d)`AZo#i9m*^No)oAkKV z)<^gC)4xv`%yoR>Gc5%XJwoqWkpN?ng$`XWT-0y>xtJ-oMP7)fj!B~D71605Jv^GP zQ)0+wl3=H<V5DVd8JH<ckS4T6cr2C^#zPO8?Iu7me3iCH#3v)Yp;Z+j8yQGlN`cKk zc*uUAU1lguTu)T#%_}8ed7?(BqGyZ0d8<-iX~i9h_QRa^FquKD5~Bq8bBG#l%evXz z{gEYeahCD%kF{onW9zztB4@J5uvLxTG%Q1IwoLm*Y1@I)rQRGEeJi`b_R+G*TC8&z z2CD=pr|kLAmNo8!ESkRPGGhE@`fYl{m)+3FxTWOL&#ww=!PP4)WJqmgP`u$~x6=}# zIaWN@(Hi$71s=q?nP#nl)2wO=M&*AF*NL#Evg?_!5cOmF|5rc%J3mFDEOAti{nf89 z<ES7+ol4Q93ZFsUH2Jw;xVknI#xCAN7}<p<&(w^g{4auoY7vd1z<X!^>1=KIFvZI) z|M~ijGg~0%I-VFt3>id9h}=O6$IyDH2f;>rm^esaX{uK_P*SQ&d9?F7tNsZ)p(4i^ zs#fFO$A&>>MZ^#okfPdmVJ<i_mM#^iH@TZ$feTp%Wv1I*q>mn+WL-9$a_OL$Ql%?& zOsuTxLwLRLEMaA!=aG_l&61{Sg$XtqXf}`OKdCL0iYKm(5%~)3RhjMZ`U@7U%T5DE z&L0%dXPbK$@iVpl+AwZ$4EylCuEPUHeeA)cne={0H9;^dCH+^o(gGew<hkcBjWL`R zQ+*<%|L$M&Vt{9TZm$7ZAx=$BhLee({EBJk5T&?5@ek5!O5dxN?oR&jSkC-KJWLc& zv0@T4#Q|5amB7r-j49)c&HyJSMiN<~owN)d8B1nu&N$T=RV)D0?yJY9=b(G7eRvLX z5ZoclCz~$JSZITPUvz;4=1n{!DWt0m?2nQ4IU*JqqjF$C?91GZ45GvGb*OYr3luoh zz&N_892*1}0iwv+Qu4L(Sv#ZBjOE<9dl9jNEge5#kf*WO(z!^%F;`JVoyJO!NU9Y; zgbUM=foGgbjjjBs?Mtc(hiLe5B^}x9HC%5f*l%o(FGtzThHOpq|MZUDs1A0oG5tO@ z>6Rvo3f8IxpO!~=BcKPzPN;u>+v3ToWb~?v^8VXP{XAJ=i%ugfx@VLAvA3$a@~Id4 zQh&O??3C1UR#8Ef_6~<jaD1!Ydp$c*+5e^_Gs2AI6j9JNj#%5k4J}=23Y(iM#LO$& z{MwJp?F*MI*`8Wnf8Ju}PeVuHVt3;yW)JiB(5QbAiy@xwW&>{_^CrT_J$_B;QgMaH zcB65hg;{>Thqbbhw|eQGl10!@Z77Cy=qd*5<d-Y>lIOqz`m@mo$tzOHJl8T9qX=}! z8Fl^pAy){4Y(*uJ(zu`{Dp5}W5k83wBG#3D0qmXpY?mTg$mY<BhwnJvB#ADeh;S@A z8=$tHnBRqv>XNl43=hm2F%It=6i8zvBf8d#BsR!S??@X;&RGp7r>yNGY++rIJ=<^e z32x3t4S3-bWy&e~_{=9D?qrdO0GZS&r192hJro%sl-;ACbk{~U+O)Pb4H3f=L(dvZ zoa<2<WiTIy-Mf%-y`XT&`Jy+zczv(s$~nOk@_yRla}?ee{DMLmuOH8TX<c#v*-wS7 zJfYp@Tz+1BLpc4xCkmET9>#T@KU3t~6qW+i%D%=*5!1e1l@+jZDvyqn%Edk{?WF(+ z>=`+`w=ZyEeB9GK1x%v9*Apjn--Qw@%TtFfxgY1YUGSniUSW%yS-sZZKy8rQQlM7W zCNw%hxN5xf;9K9klj>oZ(cieVNTSfl%Lt;wuxUJP!{$QaG`C%n7uu}hd;J&eqz95V z7(fV7{5(NmCUOTl282k$Aw=wai1*ljysUd8HiS4Yd|2eig>=JAd=Ub%bwHqLP+uQ? z&~Yfja6-OIm+mwTJpvPEIvnr&rcy3)?fhuo>#6HK5K2$nHJLhL@tN8{yci@Wx$&SV zi8gO^@bGdZ_lf+gU|VL1;7vI`Tc;D%JLw>P>p{ToqBr%~=HL(o&278u44o?FYv0lL zH+RFFn1c&)O5!FREz&v?5wD|H(D>@bLUsBaBiTn>TxUcA}y$tSIVt#v9)e~`0I z?|UFB%~YREQq0lM-zBf!3bwQk3x4G2czfgGB-PZV4-$MoqO5&(rS4sZ0?Z%Ys?!v1 zgH(?@ykgOl4ZYDMCyZALI+6f>(`s6v15s8?!L&N4T;Kk(3c^0M>_(U`D!{In)+;H8 zXhj9Ze0||VFTXBUYMR-N6kTl?F-nd&f#?m(S*Fo<{O+qOZi$%#OwO>uAvXV})nLFI z0rMm`HJIC9;cWB~By_|H){KO7WE!W=?*t48i>lgmv0ocBte6PDnmUzm-8B}QjagaF zD+@erLz@XXd<t!(E&DFIMyC9o9m2iTx~Bj-bpQUZVM<cBICaYY@o8W3-S6`Np8eII zFt|-a-RCR+tiE=gSS^joH&Q3LHIF}L)$LPo1U25I+=>2jQYFhWf4gd<oz6s<@aU$9 z3$+ZV{komf4^<|by8ZTeaTxJD$SV1_YW{gjbhQ6#R|nv0?nd;_D^(%ajyLO8N&Uhc zc?&`+2nZah-%Q<DFr%C~>sH(&<*~Rc8Bq|CuE=T~%3t_U%T+(e%EP;n$Cr1SMNndp z$qGy1%_Y(i^49$DVO01dG_AWC%o^TmXwCJ(8rtcvr)jPWCXqah`WGkZO_zS4n<-nV zkFqMa2Muu2>(fmm5T<MJ`{%Kr6u$M3-NtbAn=rpPf2goqCIf4+CJ|mUI`Koe`O14k z{%<5h*tZ|I?>tBbm)`wpij$C(zF3GHEYh6kFkk;QE<Rp2_x5$H_U7S_4n3djigY`e z4F5;3+w+qoUg$-In{kz03)hKDjp93>TN-Z$-_7&NCgO@W|3nN9jEAU%ji|%z+rDlL zb&j!{c>H_&ua?x_*}C@E6VGN#F6EN5?pV&04^6hMNx6q$d<aB`H?1hd8xN3C(NT?+ zk)VyBRBm}5_#YqeJi9OAnlB<<@mjb^+tqMs!naQ)Cnt9-XgW)ij`~zqifdYu)ryy^ zu#aurlSN?_@W$%+tRI4;D%I5;6DUFDSA^a77|C=*_9ND~jb`R1gf{n+JPHOG1%Q$I zlGm^1$Zn*HjRly>!U*UyKR^A2%x@5I(2JE)-1Kuvl09u^m~nClBLSSHN=(SIN%`t2 zqQRhc5j9kX>;qVI_~0y?Cr;`qmG^qU=)q^}#CZN&%4RNXaYHx`BRDKgqVW*4yOF2C z)T}DP-wiu$|95iQr6><==OQc(9R=%Wb|!mJnTe{QB2P&7ok_oU5P(0fPPE|q>joPH z#p{NPM`fmfiGlEpQU^TDC8J7$ejpb<<FdgphD^3k#|s}uIcz>(gxxt)@{|@)3E*@a zX9DzxKHrZIwWr#qgS4eBuSUs@R&^AE&cJe8DxVew$AbG+#6HyN@G0u&6xR+MHIR0@ zG6X|8Wr5qjKYrr`&VJ0=>yVnJ9j;GE$0Hd?g_I*k#dZ;tVD4}X<8d!Ur1!n9wVk-U z3g$3Uiyi}P4QYSV?<nIS*YV^Zc!$#Q?hWB!%y|LBHYTe+eHYb^C$9l{QR6p{iS6#l zy9aOQx0mFidTF)-YecRpi%a0~(9@m^W<h$U28J^sg|TXCK9BAshW^6RzU}>4X`SUz z&`AUX*pKOuJs3N_I%r69_S#j!f*p$yvd4`?M@c2OCsB@zkip7qiQteBegRvo-89v- zz1uLV9)p>_@L`sk7YdZ#U4A^blfjC>>cjdq-t4D(@3A{4)cp2}Q025spGj|_e5TzN z#nSU@!Y$%u922@#kb-pqW~BTru3~(sTRg+9MO)>v6{jHSO-*rL+HJ(Z0-Y&3fn=hi zUZGzpW{vZj;3nriOpGQ67WQx!f`_e-z{nTF%^lz9bDZONs#e<ygaP-!-V6#gxh$zy z)s<j1Rka*0N$2y#>*dE{7ne|r*Z4BU+y;x3D^u`E&(blIUt7gmi=>MqqYjm^We)V> zA2z!N$@aYrCQ7TdNg=&00s`9hTO+?t=nOSCEtzetK4SMo)cwv10w)q!!RS+gNO=3i z88q0@9vdZ5#x@JD7h0db#r*m{adbmIufhX1@I&;2{nx*c6d@9e?OvPxCkqJJ@f~h= zRx1ZxX$FAMw{p-#Zg9f|Bxc3%emQ2KH+PXUyw-56#W=T4RIgGHlbxT4VtoU=Nc|`^ zUW8vt$=ZuSK<e+>(k7(umViO7C0f_m>|m5x9FfI9npEYolXyTEXLn+A#N0IR+Dy+E z$3tYNcRYd_hRZodD!$?Ncn8gybmUDp%&|++5?G%+D-%}XOzpZoSM2$=wR7{ujPEn_ z1RGFjo=YC7p2z42%L<Z?oIu>{ODp03-1n=qI+0dy>fPen@1ZX)tDhri5~|)f^^EIZ zc&k_7!71l{dHUYXn06I9N=FQE?JUn*)<z&HC!UBJ&7oYnNmkfe^~q2L<DlE^hcT6a z$QK$DgCiN+U+U+LXbN9J{@ij`d-<`ciSmd-gUo)?baj0A@=@W5%8xvP5gUam;%p3O z%or0(oc9D23SBz|#~TxqL{c6sXcWJ%jf8~BUz=%&nZ31j5ldpTECr4ZDF*Z`ZdUod z6WDKg_vWLh)WCK2$L1!U?#8&!eqp5-hqI6CLp~S#<|QU8l^Bj=o|;$hRVY%Yu@^>s z9-JS>&80E}24eD<9SuBw{D}N6xKI=K@m#`y$HTS0nJu?qwn5&i#QjNi2<sYD^$Eo^ z-0He0@llzWpl|zUe+Ds;nyvE?AhmoFV#Puo)KmK$L0Ig?lN;h-)OheI*W8&pqZn7^ zFxW*NT7Joa;S{PJ<rqz1y+dG~+~|i6V=m@-wV@esvb!_0{K6+oTt_Tpt8e+sAYX&X z0R!*=Ss|TN!zJ8gC!_hB2~Bzh3(cn!fT?IzR6*E*-vEc+=QKW3JN=4IeN)T(2ESq- zyXcrJSqB>4vcIJ41-I4L+DncfGIWgL-piFLjo`er&v~KQ8hCvx9!jF0-=%$T=-$|o zw}4;dbN1%pH+-Zp(o4vnqgh;sEHd2B80*d~&s?;vk&xv{DD(3*GA<h(`q{cTW)%CD zg?`p67`^Xv&#Gou4VkH=upX59(u#1KsMO*dAeIXsbE?4}=hVHK8>8qn5A*P#u3^pX z*k(0JV|mJ?dAz8Yrev=K^b*^og(k9a>Wk81I^qvlZI6=eHE(0r``JY#jU@;Kw1!V} z`{~JZw$<*AcX#}!&+Qie$sAAj4rd0g{dJ(Sg`JVr+;crf1x-wPMWHBS-c}%oBz;9P zj_dgr8j_+M|JDhmo7<Z;JQ*+1+knB%s@Vw@j~fp9;2KfCy9!myI$?Vo2W-g^broya zB`7dC*t$CzRd8S~TZMeL5G|nmte9<&VZqJvn`ZOF|0?e+zoPo1wof<6&@prkFx1dP z$j~j#(A}W4lu8ZVF*MQ)DcvgFU6M+dpoAbGKYh^qS<hPc-|)<f^M0;%?Q_m|pS?d< zNjJfOX~15h&}U41OlHUXlpK%&Mt`?a>5d_3v77PfkfbUB;^8H5B;dP1&X;2DT~YpM zh8Xn%qqTSLp=B=_6c`-T19*UX_Dh<zujavO4u{_+()^G@i<5hWh3KQvsB|ZTv1o^D zWi67(Z~0`oYSb+?!TWc{PIPPgU6@k5^k4abPCAX#+qq80sl(3a`8I1nI-$=drX}T{ zKk+r%NdDuiU%<f7aI}6)bD(&w%rKDV880{^F=$PA>Kie)XvBH>2UdXDH`xXXAr(OF z?Ob0Dq}{HbvV;4pqp#j=opgQsg3A8On&q6N_5P`G<Zpkb_ogb8k!t~8OeOqMd-dI6 z?>b|?a3;VK`#dK_vkecFPo%uHdZ(K|Z++AdbrPUNnhn1qVLp=o)HSbfM+rySCEdVP zPC5o3Q(_%e3%IIm`5jK@jZDeY|1+s)aucF6ofwl4PxG^en806Bsz$N6<I>y3(fxOi zXq8b={mB)Zh1f90t(_8{BXYQ~-iJ~;?4=b7(dIAJwJF@Y!0SUN`y<OpRo#;`DWVdc zo2rN*WK~wqPeS&Ok~X_z1W{7&Kf`nIParZ>1qwP7f9vay5)cwnG_X1|gDSaaSi(1( zsxJdnucREWeIOJlt0u)GhQ#Iy<~wI|=Cu~S3r!{ncf(3V_tIyWfQz50km9AekmZwT z2$)|I%wXB`X*VhhXB_+18mPQJ%3!8h+H35S_bJ{*<)eopVf1lW9Z4kNE%=WS;uw?g zappXxsPhOcR0kge52rj)Ops67{F)C1R>p_7xL)C$a!r6@PDoyTP-l5AE#+~*DbCz{ zK4S!E6`)6?ksFO&e!OBlTpzI-+wZgR79!4D!l)iSI+&sA#Y9hOfCqzIk>5!_HJikn zSA+6N!z2YI$&%H)1As<jGc(oo$H(IoOnG%{u>-4exgJ0UYyiXCg%>844+55-ABGLR z-^p(9{^Mu<r}P6Fu=B{bVaqLCjbm>W8-r$jEQNh-_y3MGA-j|Inw$wDb>LgU4&AR6 z+m{>F!pG#nntTE|q-wM`A*ji+HTo1u$S%*-nY0VDCza2y^*8#sPc{aA1*L2?U@FVD z*e}J%B<m~20D-*}D+T4vIagJmdQ^Y(Sdb~_k~czizwvXH_QaI)GehmM=O?SL85{w* zkVEkUCC<I|G!UvRZLBtP9%@=dRIArmpM`q=)-`?r4>pFA{9IY~{le(fTb)TO(c`x@ zURF*8EyiUQfuqdH7eHm*aoHr?q7hr=?@Y>E3|ZtC_S%4v(xuc)d3KDxjAz50jNH5B zflSYO<}RiZf~{Okc^gUh$;t^2q=U<<O*jOyCd&Tp58f&~KtoxVX$s4mSXF!*458r6 z`m@^OD-%<p6wT+0)vqSU85zxROoA!VDnvMFy|?%$u<d6e#BDPcE5Kf)<pC+m%pI2z z255b;Dwk*qM%WYtq@a0^+>gpw*yv9%AP~LiP)&S7O$Z|+fAdZ1#4xt!GB2-=iRM=m z-!$$w-0^&2x=WID{ZF);{V&>m*)zGzyitt!(QIjzQ5<CHX1C7~88GmU^lrPW1o~iM zC=;jH1d7@RJKDb5b=o#Dh%T@hUE#U&V)FUjKK;JlDyw4kkWS-k0yAfAb@3kF5XZ>@ zc88M^L%cF1?Ll!sF)7+$c0P=BoP0DT0~*kYsY(~W=sPV^%`|_ps&QAx%~QRQs|C<< zW}ScB@_VeWUy2V<UyNn42RcLh39;O0XW2e;4hI7CpL+TwhgnDF`&<8O2#Ab=NTeiE zj-#)k*iD;7@6~r@JnzNwt#YHikMIU5Kp0Zc>)>K)3j{gEphb6gAZuUVaic|hXJ4j= z;1x9I%Z7mw73;C1rQp_Gs9LUct@gyF2?KT-RE*9~HK_&l{4LBn>79cOSJ{&0>l&R; ztu`hCwUx&YZzA#DL<x#vN}zN1z;q&o%I^_zK<8)eROz2Xo}pi5y5;t_7R$E2l^Rr? zHqNQb(Mx?sLM3V!0&G?==E-%VlS(H7P0f9nG7)J>sY&%>*MaWzq^(ymeK0drls@K( zX4@9X<Xty$%gRW1T9gseYm{QiPp$r|&zYPe)VX$I0d6tVl1e#v7Rw*COAzP1S3+sN zPL@JNG&ueQXz-0HEQ8Y&gawue>V51QI3J_I^mu4ubmP;K`4fn;RY|?b>|%ODnHA@7 zKtOlGLmG(dsoZ+=sibiE0EIXRP8wI^VqiWP7Kf%OSPP7^@|tx)2*GG|NK>$wiBDDb z4s;bqE!=X(9h0Czy-lcG&DIqUA-L;VjieU0lVDH|zq3EjvSn5Yu)HvsgZ%L&GU92` z))q*ABQb$b-GVgK1`pt;6lz@0-%26f`AOm1P5Vz2y)fJ_*Vpiy-+_adz(<xi4GyyC zOJl=s*+**FrhxtU`y?=Bz_}m>T@shsWd1nOw1p}oc0x~602MEew65VgsO}&C=Z{4Q zR03eOg7!AJc44@x%F&u;l2DhQHNJ7OoguMZZ1AMReMd$8RC>Y!|C9_bc$so6$HU`e z^NV%uxB>4ww_kjFr;f3o<DnP+my-D<oWBPfvR#NXHB9nurweJkpYpo+RcxW?EVvxn zSDrAn<*|5K{wMpSRe4`V#>9-~0h<Pmhzo0_4@OtP5@oB&Vx%A&fevI43dKMFnxxr# z7Lr{Y=U=o%lL4$Jcq$~i`y%^sYM)R@$R>JjS+4V_NWwPk_1w}IMoOc$6dDW?Z2sMy zci3{Uejgux4B{qtmMkSAA{qkRH;n%AI1}4iYj~A!EZW;lyUg<1<<I3Vvws1iwkjPy zzmFu^m%GF_WJlIM_s9pA{ZvRduzdE{Klv7eP-#Hk3R=8>dgrX6u)k&ds+)_YH%;{F z{c5~+zl-&9{*@xDH`m=x!bp*+TRbnf!GxjZYzWY5Pd^&iLBK0>P9$%_t)w`mT=LLS z2g~U8t{B{|!`kyOqYs}0Dm{0&1gZ3qlVp@P>PpQ%22P}k{LBT0m1+}SNIemHgu7Mf zZDv96RM60ih8kT2YAb}e9EpnO0!)8>7~5z=jeia>poDCi(+fU`{2oa8wOFd3NS72$ z<BIjF*z$MBsmYwt^^XtP&~Sle@R&Y@0X~nR;ZusbJRu|V<vhGCj2B=jM}MVoVjj|4 z=SeGfuY5FL=gi&%PjXkJ;03d2!d_9v$09j{;j}VP3Wx^V241d8+Fd$f+o+sk{8t|d zRcWZmV}ApUup^J#z?JbW)4BE~k#m+vRbjCJpNc8t=z-6Oay4T;gQ%V()eWdI_cQ8f zTTv3);i|!7LRD$o)Jb3<j9e1VGb9-XDV+eZx|gpHR-z|hugst94qo{LWt<x%xL)4d zDOkwHL%pWPxY3n6tgY>>L%exuGPL+TPRb94%63`{DeItBV@imtz)abuXztautlae+ zL%g<1^_;hC1{$Ls*OD)=q2)fI1?rDU$q89;*Yr_T2Uru*XNk#0jXl#(oM-5_gsN6n zzcU)YTS}q7yjQs}Tr}TVwXgLDsxIfep(3h2Vr)Q7((F|aCj46XF^U0ZoqJ0~^4VjH zQohW9No+blrnF+i1XC;ca2fj{VfFQ2eRvc!q0*xZt3bV_N`>i-IC*XT=zB!U$G6em zVTqoqbI!r)REhpQ^&I2ZZ4pf~K-L4gfrv^fX>kc&rs|U$Bk`}gw4Ck}^z`P~Qa>+% z#lp>?nUT(}Ujn*SOW7q}!-*t>Wt}@2V0oTVL_ulCo=8SL{(&hfZ*zQ!?K@*J(;9-C zzt)_qAf&lQF}zQGYpO^4y$_CCj^57D;Y=X~i*jtKcVG@bbz_^(%%_gVqXJFuOmgB9 zDVGVgH-<pz${3vJXRGN;(a#GwI!)W33pX!B<(Zm?eo%M%#HqP@>M}E*#;Rj&z(WBY z(pVx?oAZ8ROC~*Nse=VROv>JtX)O?vx6PyvQOYm+HWA0m@!NG}8}B(;fn7_QjmBSn z1Qfwgoua_Pd(lsb%%%j#)$Vpgck8te+sWAj5$Nj6N6)DL9{j~6MIyy(mYi+F;NA>4 zra=U?5AwiG2`&nN{;>~?D13hS^9Wg|QcBZsx5zRwo2B@SFGHLVbD(gv(Z>)g5nD~; zPKy2r996`Ia7LtEH(86w4fB`a2;yQWe#XaWC`b>-^JJHobs!A8lvpTc<JhvIXqd#= zW(RILi?D5tES9F!VQr5OCTR1}G73nhVUR|#!nCy_!q7TA6c+l3sBpw=x7vF9HI`hL zW`9s&@rm^#6<9{Z>1HE(+M{X%)3BJkqfOD&ik;JFBGD#6aBOrv%zkkswfP(z9z@F} zRN~Li9=5Y{xL0^%^W8`M%cS5}K1T3geS{wC%T-4~XfXao`F4E*r=~osO(xzje_2Ja zZ6Yg=&%yozgM=^%esuKU;8u2iPEd&sP>g(SZ$y1kvn1xV&4AzMj`h%Gxb(zZ?l%l^ z%3K|+AdCJTc14_Ag6CaKc$@?Zm!CRyFuO=zL5@sO_Uyf$vjiaKs)ZPNeUROLD0YG? zrjDWBQl2ZR_c?PHW%(~^r!)8AjIUaICw%Cn<kzFPx@Hs+4J>Nqw*A_=v~Vv^;LUUl z5ve`Sjwri4ojTeIosqynTO5iT<a%b4Dx^}*nA)dAvs;*w6rtwLnwnu~GI91a8yZq~ zeY4j_%N8wGO+GV`t^~nCMZlm6xdY(+z}Z83&)t6Z5<MO<)bF?xqZWn{i0Xs(Uwzo* zPocr2eQ#-;=))|XST^1qt}0b6gvzsxpe*7yqxqbh5)3k~W$oIB&vM?d<mA?TwbYh@ zb-gln4SR(LV;0^aP1xEQo`@!|t7c6TUgypvByiBZz_*-5QIy6Ubrbx~-JQEPI1$%c zIRBDR5JMtzSsbl3a#wL?A6W~mrin9j&P^sGW6{Ll{=g8$lHwK0&(DHl4YRjQKwqQn zKVgjSfETG9i4#YzSQR^1m%R<eH7r|&%kV4R-UyCynoh1DjyxM!wGAQ`GCojNFP<l5 zzf)vK@)}R{1CdZ|YT<in$Fdu4VYW|r5_&yOUM8^+Mb7B*a=|yV=HdG<cJGu!J3NG= zM{zvyo$&=U&if1;RSO$~uhIyTrJMfUf1tIH@bdvC8Jw8m&&X#gdRb}sKv3-*&xA2d zsnY2k6yEjoSVfBK#p>7ZR%;4$X7h^`BfnfufBg!WHa7kkZh@-n@G(*$?%91S7pVG+ zV9xqLQ{kyH#qte@<d_y4`zFtxM4|XgraU`kr^?b|hI=LC<}XYK>weDyMBn9!wEI^9 z$S9JTM9Ux~J;14AeQ0n$XUw<IxW}lyV0`~It(l#RNz~YZgH6E?)z+!9SF(FcR;Cso z7K5h<0>9Hyd0=2)2MG8PtR}o4j7&k`XzEmj^;bT-`x%D=v<@RfK;)67p$B}pLX4!e z*dt5tt=@B6E*5k!;9zzB4&X4I-h>?2QS?_|yr6Scbh*p=c=Mu`ic@tdYTNW_y2)RC z^q{`1;dl%4{}pmLjkHxdy@bZ<f|JNJM4o+dgljd?mN>#_*OyJ{O-uP{S&Ao~wD$Is z8qz|JEh#bvEz-^6;;074bWmYA!8+X>n=!Ejg~X3ZG|Z{~d^Ao*G2@sg_$3{I7{WXZ zkLxQIxK!XsBdB8beK4q;jN)UN>cNT2Ihzoz`SWeARx11EY$-ai;evP*5FgO>Q0isp zwa|y;%qcWhx6!se^edl$nIH|pBjCrPB5t(&smM}dG-k()FVrG%CNoMVEX;_|giJHT z$X@2YZEHNDu*vaKu{Xk<j%M8{X9+%s&!=qeS`K!@ZjrJFKHpZ|5}NCu-c+5z_YBGW z%5xX+{CD2R<A`t*`Ub{&591z8o5md9{M9E|bdVLXwTo+E{((QI)ou32<nF6vQ-_vk zm%LKK!jMnBts^(lgewC=0o$Vf5?SwOEXptX6JxHQ2|prPKKz<F0UA*7;Nr2*P{}rz zsE*<Qp4N>0_>#!pl2E(y-lXcxASPfk7oC8GJ4=dJRZduv3?xW_k1kCBw?1!BWMOAV zV-B6;k|rCh-y<OtJspUjSoICNH_)R~^W*6+TwcUa{PgL)`%{xvo4EP^p7LLQoskWn z|IJ6x68gHq6F`}+=j<7K-S%3>pDMa|_Jc{nuiT_sss)P~0qIv(u2;e>Wj?}>zr^;} zWwn9%8ZLMFdB2X|?);GzjYXg9r}<`yQL(RgH#c`*3f`+O2z&#X(*=V|p2U_b9&%@9 z|JA2mOg}g5=pfq*CG?u_PxY6vQ&;HZIK{k+`m|kP{#++C!Xx=oNn*2Y<!*@x(@}9& z9&L);bA?f^55I6fXBQS3lov)QD=$ZS@j)+bYljmCXbQ!u7wljSe9MYK<)E03W8lVM zJu$pQ?PG^=Mt0{a9pE;V7T_28TS{8l(Y!ybvNiG=TKW1C+Q@u6Y|9?)^;=)CBp(Qf zX{&6OjXY*#M<>KFVJEs_y~g3-7PS`o_~vDroK<4<<Xh_C#|6TZl{W<M^6Z3!1Q_+# zz$y%bn{1;!EC2P*!gU$J2V-)9OcDEi1|{)i{4^^|=jH&nQJ>s`s@AH6EnUWwz2cRs zkTB>v(#qnOc~ys3ZXO_Q_zg~0jopx&8}GmV>bZO^ov+~MLa%3fMA-wfZWfkIfz{8k z`7Cz2|E}c?l<`;2Z-^{w8dXWsJHs<<NIMauoNxGZ9YFT;5L9-Gy>-el9G-z?%`%5T zY<tq6F_V(h3Da7?HUQDu)7g8WXW%I_;blgC-WLr^w@}<l@hn-ecsqg=9ura(5`dqZ z7y2w7=)%O8n&>09{^OqCc4Hjn=+7~I%|PNtz^bka;68o2P7ip9tTZES^ih-utOfa> z;$e|Hf6Tif-`qc~eWyY_Ibrl`em^62!r4|;kjYqxK4qc(HoR|jv|1eZFoQ%z$$GIR zTM-jq{W~vbUUc7>+V)_XZO*)BTHytcsg^2JK{^)ZJ{^?#g3=|8l61MFuvt)LVl#E_ zuYYb8ckg}Cc2_B=#Z(S810y1aHz#P{nS9sUuuOHmKI4gB48b^|U~%QQ(ZDrfRrI#S zk#kj$fij>;n2U0Yq2q?~LHJ|J1!Kf@32w>Z@NMVG_0XlN(L|Vqx&kW*e7@wUqpFG% zYL7Og*9T`gB8@6=@k1`wimc~L#FfHK*D68>16n$f;#h*3^58lkTWwFi>dg;#;cL}8 zVWrTn0XDa#N{<QsMDx6|q#M_7>OwtH{k5>~#!KtWj}v<Sm+VeCY3wEs64}eUO7U#* z=7inX)10@>(l>)LL+$!AXnXFqDhw#sPXBu2+D>s&N`i*3F76824wwYC{7gF-S7Il< zsT1BjQdp@)y*7cgL+PZv?z){<*MpBaC4cq#spQTR!1LJ>-r2=V*-Sn5J-fQ=EOwkX zm{ORKkUsv^j+@)T&%%_U|JUXCo55^|AxA<OiO4vjhDGC9W8+W<6P{w@Gs)u+QoCu- z&pS+B8)WA2g%{`6j87utNIIOWCrDYlY@*nUG*eqO42%?e`^&-zk618rSbG5+ocRc; zw^>poK(b$rP@I5o0%HdUU*;uN%#7ZL80e}9#bv05R&A*=0}IGr>GdF9==S5GniU6B z1vY*WkmJq9D)$=Lm!xK5-)Naig$a&3RNT?7TeW^`=AeCu#3NP5NvbUyOC;y(=?kB# zZWOE)tTMJv2PCG$oNJKbWQ!Xa@OZsMt(aZ9wh=eS%|%x|8y8ZlRKNVZe|&%g*${pN za)BIS3*K}Y)-1P2#duGDV>@GPiqV<HN}-c$*}$TVlIH-th_`QL=EC9i7>ZOms)07q zIE*ETJ1-X0J-z(7)F?XKw7dRzL}&d2mxD|ZwX>Fq5(|bZKCXY-xN-!?xJZ^A#Jn{{ zDD#uGkl9N`i`d|E_GHZ3gv<G`jXQpo)-w<`Vzp)tvaAaF%2#BLvez{DqL2LUrQbU0 zkTr+|?AemP`zoXGa&y3EMa^@LAx06Nw>-EWA(7N}F%p!z@M7o5K>ewOFN?%@!AL!> zwxWU8?y_a&jwFdHujBRr>7Mq+!`_E>fsZ;9y#o%5v%l)=LDtbg%ORck-A+vnUx_Ev z0ND#$g1JfjsXpwmRrl?a^s&>w{`sSM3Jo)urj_O^NF)w%`O>-8N@bII>>d?9cR7^8 z0ee0;7z5~j`H>v8n;<8Jq!M}~Wdk&!ySbFmlf+>7->xF;3~eLB<S6a;6Z*2!E<*^A z`Egqvc?CTfTvw;B;*&RE0j;5f7n9u3XJEuG!I*M`gd)=Zla^b%a^o~J@fkq8u7r0> zOzw5lsofJRbd?US%d-xrQ|bo8SOEnq`7#VvsWD&gB44x?2o!>?0JD_dRb#0pQDiSz zLkr>M^OEGIGfVkCjykTzy9*<EP`lHvO0YMNTV=IzmG1#2!d7o0zqUoRYp|!IBo}F8 zo4xFl3Om}&3*q6vQAl~$_UD(j;E;FDb)5Tt6Y35BFQdQu%t~zV<j6VFYSlGEmwtLL zeQq&d`hH#U-?e6U-2d#y;aX^M{_0rpMk;?07MM9cUCmr+7c~nU<mT`}o~|#ZsXaAq zd9Fp7+{M41&K_W8<h%^0#|#CW3s#l95D~LHuYq542a~o)x*m0mc(HYQ&i^p43V}xH zK~q?eL#)aP1tjAzl5Ye{jze>Fm?stczryKVc&aNk7l;k$2#dnvj%v7=t=(l3$vpQ^ zISJ6owhZ_H+!#~@bhwfH4b33(bz0XTYxRhIYAkj$q(6Yq*n{BB$%p(FsdXOm4vb~P z4lCc`&SejDJ<ddeJ&gPftFi=FZTf0!MucyMeC#wKun2^wBzEIyZ^K}+MBu7{m7<!x z|9+V%@~=Mk3ht~3xq+!E%LHz$m-!l;-tG7Kb)rm7M`LYE|Jxphh8mWyA~5SrJLzB$ z(KDc-JEbOj3lb-}*T4uQ-%{=X?2yMSZ7FaUY=5s<WX$oh>C+V4W=~u}$Ju&_Vugs= z9`C414P9Bs$G$(xfCSFLj2}iyP7<oPp@l_&kwO5(_R^K(4h6{XHRq&kW%Jf&hGW1* zeBUP{Ajheam&$s3tEPe+$h)U0d>RytyKn|>Wed)i#5^ovje=1s)(?wES~8X#{W2>J ztH+XfI#L;k-A3K)l*N%+ep$UM5+<e?)dj|3BXGUCtA=QB3<xXpEq<T#kejo&Z>Gt( zn%>ymNzK)@+2lIyQlk{TuHnD^_wRDuO<{75kB!+xWL{kTyLXe{Iz=OIUp^dyHAT*1 zX(WX4di$C5z9QZG(XcQk9b7+KLUyZylPcUDG6&9oy`$Sx0TGWR?NUU$?9^>my}M1< zmnTQV9vxrWL1RnK)0THOS5$!Pkpg)Ghz)2ZS2V60Y!T!<xjxD+WSyE3a_ID5$*DFB zMuAN{4cNRw(Yj?(FOb**4uHi#!7!`(W~F$U22^~&^1`{qs2e;oEF+S24x!qHy+Wrn z<V(g5rjfrzmIH9tVNed0)%m*sEyH@^Pue{eDr=d<Zv28YrTNwR%3!!|$(-5N@(13- z<!Agw=698qs$)uMDUqWtTQTreBSPs+yCYhq2^by6vIWMa8vf4Y2ck;$fA=3%X&zs+ ziG?wFm$FIFmr_I87oFF6S$|d)5Av?oBs|_umpH=<47O_VcF&9YR<^cB9Kzxxg4lBC zw^%|Wl?hDBlxg;z+oJI2d+;{o0#WCbP3>}M6hPA=0*)P}22z&zicrrzbTSMBxp8z2 zRVoP@iSOc$l2NkynCf1#scN5%$c2q4g?P~+gXj=uDvnZN0vsOQ74F6F>LUfo`ol(= zu$Meeck24s4<^qp5B>D0tYyVsiDE`B%5|X%8vS7RKU}H0Le#r<TuuOtll@;C*(;e| z%Cr^KzA>9Ss**|=KsIlR*EBka{7A46jlt$13Couw;1Sg{w0fD<K$b(Ov=tfvmG`{= ze0B7xt&t_QZ?zB2R;rMPtl;1L&xM*f)E;da-or}v3l~?_I5hYZQdF6@-KAUU+O#}U zi!l#nnakD)?vu%2NfiL%#?o09o0zo5th|nQ4?FpP1z5zc=&V4~q%4YOqmWNDhDJb2 z!<boX-m1tEm5dQq4rn5?{v54~&aTvC>me7wmZV73fQ@y0l$I1r%0@s+AVEenQiRDR z6tNVEC0ZfqT&7N!WoWM%M;NtT`JYXCwj#EiA~Ud!Ioy8rrBN#-_Nd=aFZQ%C17fsp z2!WZm^&TT$+ycU_T(vt#*&<cDdLT@bOQ~f1OD+-^;7xJAC=-5<3x1B_u?F%hZo& zpWI#N6%E3=@uLc?UgvDJ^A=ua|AJQhJ68V}`UMv3ubUS?G$R%N`ez9WhPtE86W6kT zs*qPZs$o@bQQiEVk>}22viK^4g_Dg0d%B1RKO2|R20>3_WOt>5=WlC`ABLu8nOKz4 zAeRkVU0pp#VF>4vHYn%Bye8lXU=>OjPsYd<MZiial-$rrP^5w7Aam$~JNK<a&%21C zxx(_8(bWB*s`#i0!i>!&FfAskm7L^_ols5@&;o>363Q4b){3L_!^DT$Sf-j9<R5K< zjyn5ZjFQmM16ZTp6R?p8H0pUrCdp7H;9{2UWtwrFa;BYXJvGf=;^3wl&={xXfx>D` zW$$~FQ0gZgQT7F6<?-AJ(fR|;D$$0D?f6kj`i3s*Z8f4%H70Le*0w>vE*;J4Enc`T zeX0-PHE+Syy7Gdw|En)01E?z20X(0aR#r{1)BCW_+$Jf^YmtOMAooene@o2B)ljU} zNZ!sU_T%Q};zS4_Sq%_|h882D<%zBWLzENAXwlsNR-)Iq9&-^~Cv^qOX{OZm;eg2n z9r$v-lsndbAT?jEqsPE$S}9RhNR>!YqmFr!U{f>E1O$)io=`1Fm?x2ghDrAUi*B3K z^n4Umd+`TO^Phyi-)E0JHAp#>E{ycObfnV&g&Xx!4h4dJ>HBI(Ly7U`AdJ?!jB>PS z+32?#2>c9-Y0+`A$*Lq2-%(IqSRqN#N${HUJ7HSJjr%VTkF=u*2#XmuVVR*O?I%(u zu?DcAV1bjBH4?vQIIuze8hMf^+91@`I<Hyl23WEE^XI$cm%sWvs6IeN2)<dG-&OBB z@VTvj;s>`zqL!H(hW?mUzj_j_8Ek`O>uML6j_Q3fe-Ib4f(Bwo$*HT{)yKqE1afA3 zuUXF(Bf?<vu!ug;efpDKvV_hTnC;9i-`s6o*Tb+%&`DJzF=_WrPOmCk?&VMH@}F|N z+I=3vz#4DIQ<3#0@#C$b?;YbVt$BX|lE@Bx<ft!JpKYaJfyyFvJ4-IOb$bS<EgtcT z&7w|T%_mnE4Wg^`7M#aK4;|Kt@4oCypDGALR0N16yS%si{^u?2?yu?LZ>@KK2*y6% zy%%+JGFgI)%)m9r4tcmZLo<+~CNJS<Ye*A2DJTD{Ra+4UpN%)qE^59|{Nao5G%r(@ z{B(6${KlbsW<TS8<F7tnVMI{f!rhCI=bX#sGfVIOJJh0GhTLM9exKGxRZ)3NfFI*w z9Y2gk@}@UsC24(os(Cx!r=Pd0O;X1sA$+ONO^roKbKYxXlRc1s8ap~w7!leK8&8IZ zJ`BTA>fasiRDF>#5``3Gx3Q~|h^nf!K<qSb{lwN-iZy`Gp$Ho9Er`9$=GA1VP7S*U zo=p;r6{-A|m|*(J@DAlI#vVyPVKU{I6)oK_2$Iw1VV<-P(1N9uNiz^5&$6g5u$hfF z7yNjdaNwg>ZDdv@dpIVY4cMYOAR9hC!+ilfIJaqrQVHC@sg^yGP|_xSNyy3S=cU+X zFW)q|47PGn8|R-D#KitZ6;_kOmzPc@SC`2N>duE8KN*HQCKvwYFW*@`KqV*+&JCbD zx@|ZMTV*|_=}Gi;Sgp4V`CygSN1RW`xhj8~nK9W#MT37%{L8@2sE!XT_#}Ny%ZL04 z;ws*N*XbD-;~zn>o!)pHzv7hX1`QA2Y*7=BOwAgkXo$aBkIU=6D4%U`58|c^+lW)? z{OY)(wOr~()acT9$d?aU<ng!M1Ju4uiO@(Qm&ITvRM!<GQ-F*+4;2lBp-3JArbxm8 zQwTf?{v2ft2oU0A9i}gY--oKnA)=iSr&ptCKHjLxrl%or=f8J!R<=2Pfqej$CXr$! zCdFdIqbv53IJ&t8nW4K)p83~TUi|iBnq*R@#OkFgbDyUAulI%TLeZaLWc=dD*5HA0 zHLzBlan=ml(8~Ez{jYx#O!J{Sng{0+?-pF<($p6%@aALy69*flXp=xB&5?LKEKT6N zQ?3|B$M-?(3u1O`!~fguzOiJxEmr(uqK1b-Ub8E*Zr$hZkSRO6Suz={{{5%#^-oZ< z_vFW~3l1_YQS>C5LaTvn+74B`(p5~6ZL_=(3vO^7N2$x%Ypj>s9TDHYOD2~Q<mus+ zyp)M=5}W>fVELncv6Z(Q`83eRd;OT|uG3uNrv}{KvKbcNV%%5PzueW9VJO3#3ZTLw z!aB8&buRXJJurkm`bh>zGRLDtqY%@~U0y>=R6vC)t+(eVLCCb`Qh>Jdm6&lHLj=Pj z+2cqP=!%P6)uk)&aUf*a!q}QDSMDNT^dZ($umN@|IE~;09Tx1bJ|YT=tj=f)tD?^u zEAXg72k$K6Md#xiY8*QD^vQ9q3E2|4*$F$}d3dj^^%mGVI6XJ#NhgfF49wBD@p5C} zn0!tVqjMeMsjZQM2lBGkE&;@Y6F@&HI5bh*6F~vMEf$hqrFf3CnAV7mL$|m3!p*|G ze`J2}cat0QZ&EBpER&`_SGjwUJ1IRwCt#F}@2C(9kKeCMXM-~#WB1suIm8l-Aw8{h znO~`aEc{{eD}WF#!cYBQnx3!SZag?FN)#v*Ut5L!1SZp@my5=Hn`(B*;ZT$iAVi2w z26-XsFLIKz1%%La1?2{rVVH6Y#V)cn*K2NbV$BrgZdD<}vx@7*MF~Zbs?4<ZlX%R# z&iLp+K~{r~O=+biyubQD#od<O(H5p?IF9Z2xTc@kw!eB#WK!pxEb5maGhi-~_bDrB zZP}e^d;#k_XDy<m6TjAJd`Yc0T7nKoZEvd~vO_Lz8o~7a3;DdIX6*KId7|`GhW3Z( zMQZ)JJgd38&+5vS46k2$j4#Ft{0X!qHcD5};wVXehxbu%HyO}uXMYt6%KeI?|08=3 zH<R4MP-*w@tm(69GOwN_*r@a?y4E?8s=SNDdXi_$`JTw$aBqqhjUBs+6mM9MqxI$` zdrnH7pY&?QBSVtX{E<NEzFrKWVa71fz#?nS2}>3d@zAG*x!bDKEPhuqcU)$uoGzEC z^<45WifYVK_88)W*F^lHPuYD*y<cu-1vzv<v?ouT7EeB=GdCjty+6N@qQEi77*6>w z*VE^jC->XqV|HqEwjevvsMD0uXZA7b#^af<xNis5U!&Dwny{i6@CHc0YijVf5oOj1 zyczHqMX^moT5Lg7)p@_<WK9S~@*IiWPY%0+SlC9fg0e|DS@8)mm2K{0FY>nYV^kSP z!M#xzuaVSbR<^6*G0eBQ@g3>u01pv9zdMLlv3R_kRkCuOPp3sJw$>;wysbV{E?*Nr zuXuw(6{;`3Wp-%I$;_NWB%-Nrwety|=%!t~)m@N^S6YI4?74(GMAkf{`%_7f$y<Lq zu}i>>viB*gjyeTWB_>pjc`wS4JeE6Q0jIRYvu1sOJavRsk{@@NxxwCm8%OmMNH$7> zUU#RI_>NtMihIOrfKTMFe=;RQ_;vUv03s9tRbjXJFXv95e;UIjDcDN4Lvb6r<Xq^w zyjw-YIZ(oEYg0xaD*!8?+4}zR-{$>EGxqeQq#{(5AF_rA0|T)MItJoAT^Wl;_?46? z2J?a#g?V+1TpFNs33Kr{*gS-K*Xu;X+(%ZNn#^PlGc*mD%o-i&Xijt3v-H<dS~dis z=-y|{CsEL$+j+vBR52GiP7Uu@Sp;h)H?H@DYYT$sliyVM>?8`l0Y<8;(#nNPf*hZm zk^>f^n%2~nB7v0(Yutw1r?3rU@%@%L7M5Hv2=>rFJEh7<Vat`O!e~Vk+i2D{J9f-$ zYfU4PTh`LD0xoOg#HSLoPE$<Nu$(g3!b7Hw1io^=%058qnF;Uz)u&$)i<SMczG&Mv z*6s{{DYTchnf`fR65#Vy>ZwHA%g!Hn&010)M0r_Vd;T6Kh?to>SP=F&A8Ax+TJRfN z>7W3?`=|=SbRiXDsd-+0_4^kYlEn>rs}Nyanq(ThZ9H2a=H|%{aa^iiuc4I&5>=T- z+DnG$zkEa)B$lLJ3B3=gv0EP{O(|7R{GPAdALLA{6+5QOpQ-7If9xf)NR&+bS@^_3 z)3}0wE<m!zakb%E`i$5%%}F_SRph6doBT#zI=hbvul$R&A$*!J+OTG7y}?1nLGCM! zsVWjuRf8_)3PZzGRxYt-AM5;_Q9zRd4I=@{!KE?kokxU2eCkl7m$hClZi#G(D{D(~ zo#(s)HzbR?P-_lj=>N_~fuUHS^#vLq>60pvSc6OnzXL`&FVbFwr_K%<t6Vd4?^U^- z#0eWNFI55AI$x+~y1j!IvgS@$TvX|aoR0kmW0EcwIYUz_04{Tm-~+Ue;fQCio;Lq& zC&t9SFCdV@G&w_*XM$I~#1yBZboQj^8gtT!gQK;!)B&$LA7oudjvN~JA(yg<g|Jq9 z=oqY$k+li3T?b9ZCQ-$*t0%FO%U6*MU9L(@DY%i~Pi18lr%jX(sF2QPdiV9d_r}m0 zlQX}AcZg<xxZe|vjJU3Rmp!*EcuaZhmsncn^pz0Ne8o2KV`7mBL|Y>+P;9;z9MT`h z&51n_ZqgCnG}RkzL`17O*UyvIiMCx{!)PaIkt3YdT}g8D<MOY6{yg#}u65R#Lbj8j zu^Jpc=$*dD)l#VmYINFEt)}zegnsw;;h@n#(L-McHf0wN+i&LqNklA`dv8P!H}?`+ zD-T}aCA2YCE;`KaP|X&F*$c-u`Nab==H8amae2E1)hgK4JlkJvKQYcJR3h7;QPpcj zoKORP(5O2E`8_`SKV=b5vh5v*Z<$41a5~t8TE)(Hj+3UbhOsVG6CzlSol_*xOEB<4 z8K@ugngI?t%>eja8#0ZENq|<iqxZs2yCFf{)R&wvu11@d^?1ry3ZawX=fcgw{&vjd zP!q{13wh>sTQAo%ST>)dnGw)-XvFi=Wvq(5D3ER_qRHEh;sC&c660@ZOmn^F0%>eO z67~fjSXli^veW*3|GHKDA-Q+7u(I)RVFx<yD0f8?dI}~I(8S0E_F`bF$i)EcZ_h%> z#B5*SiZW)X?g{)TejmlT|3pcZ^(mIJt>Mm(`Dj~x%#Y0K&3-$6fvI6yBDfT_hRsP7 zl>HAeZKG4i!4Q~Xze`Y;T`muc;$>p>HAYT2>ce!T$9pl!&q*bC(ahM3XOjJ5l>qNG z@wi1YZW#0jrCKrGJVd>Jw<<E=!dZ08$qixG2}N8iLu|%{Y9G4n!`1m}*wfBOwVi*n z!e-OhdQA8zMs9rHDAdaRM^LFURW9j41Qa(UE%dDI&9H95`68hJ?DcMEdYrg64JZrI zQ7=_j8jjrI!y5rAMd^@Ec_O_-<@ici(i3J0!|8_X-sdSg|I3H{sW>RQCHOes>n$TF zuC$k=ORqAjz&>3g#T&E3x~DS4;qfGnk%f*(X(<|R@Ls6vF_}QVzfpEzd+p+LJiQfx z+*XAdJBjT|(&N#y_hA&8O#PN%sO6+zC}Urz%36$lLyH0`a=u~&&vh^+TY?bVkkv`V zXbc;(=J<EN$ZWzu9k}t0+;=}?>`~y7p6*;Y+<A-;p}-wr$Eg&_MYwb&X&q>Uu(yaR zPK_O4b5cAfoy_!ER&0r{tVz?UQ)J``WYNyA>YZto3i5k@^8Tv-^D*7<=O|vo>TA0} z*VPZ1I=n5%M2dBw3gf}2l=hvMhTidxaaoafa&=bjB9?-N>W^8oc@t%cNzd{YxuE#c zwf*Cwhye&_@sMKbZ+-n#dgPBi=2uE1u1Rt(hz9ic>@q~+ukc+Gr~G$UX|?({{A!$R z6r8Jrbjn#(3feCV#p&{MvJ1O4IHJKVvBJAgR5%HhY_{Q18AX@VJY}kV{9m6FD-3bS z^Q^@<d50Tuc<oyNQ<ewsj++Kn#`gDS(qfg-FT#jwsb{@qMmMF6>bxe3n_wo}CgnBU z`bTby+;mz?QI{@5I$O{4PnLpt>QHC3DddM&tYMB7X?{Zv#eBpk7Vv}mfUTIh(b~u& zi|Lge1g}ZvRqF>mCT|JR+Kyu>p2R8j4}(D~dojT;@NYfRI%owTbuXk>DZPt*gOWbK z)hhR*SgVW&6vK~wUx4zKj=4CU-2wAQK_{3#g`d=y+?z{W<NoazTq<Zv?mgz4Z|F0f zO}kzn@U6O3xh}_|KNmTC=x9+=+o)7+UBb68knC=hsFgRyP(mHPjY}q$s<ZhMZLu=u zyRk5tg>To$>?0Aojdv748I<~;YQPbyNhT7P$BMnnTN}!#lmIBLo8Uy~v+3!(6VA*; zLaJN7656d6#|lPaNN<`1436rI_sg@CjFWjWpq0w;WMB`z%~dAysgp}KhU&9hHZurl z{0jSQ*QMBFy$}oB+6jE><Ua*a?cIALX8{4j8d;)oAcbTNKEhQLKFEzTk0(zvPZ-Q} zNF)v$KQB3Jj4S#2{jO0Jt}6va4cHk-@>_9dWO78CbWQiZ=oe;A!Re`Dvt-jlw=8xS zrzBn9pjo9B^UzjQ{U84%{PWKSuL0Lpv^fJ{ohtKtayz*a;(@Yk4eTSys)M*rsxz!s z$JE5X0vDt#c0N)265M<tnf>Z_%*;@UPX5-!l^$JyV)=iq0v}}rs?hbHn~}8hF!I{* zuT%385w*k<s*NfuNE<%Vr+<y4cz4*EX(C0Ay{bCWy#xrBGS{TX^r-dE!rIwo^(X(+ z2jVF3Y{#OF7hAfT^KPk#vkTG)6H7UIt!dT1YT(!IsS{1d<uo#!tC)_{0Ikzli4`Zj zTcw-luIp6SWPWNqV)tBM_4Cv`uH4<xs}c_GQyfn+;(GHqyX=bq^4<{TA4<3gYo#^p z7}fCG=D9P3s)lnzt-T5)<`XzOAd_G)rtd=cU`bOh4oB_tUwy9RANgbRkNM`QOS$OE z%IJ-dsZ|(lhMYT6J{G@-kO41@%hvQyEh9<PR5r^84Nmh{dZVK$CXrgAp?yWQyo>;e zd+Orb3OB=5inWfeOqz<-`AEMC%2(QCx2Z;({weY$(|2<*V|3BKG$7RgUj_XoN1w=Y zolaEF3SDv9wYrj_S~^!_CNV-5>~zfij0Jc8<YX??o-GS1ULc%1Pm8<jlSddo@Cq4H zLx#8Dg@lqKxa(7N2KB2Z7rRYay}v_^fK~Y9hXe(kmrB+P8XzqP{j;`OO&?a`fr26a zdey$y*OwYD3m!(Bk{$WtX+JdB$p}e@Z$9A;LSZ`EFW9mOM~8^bl6X-oGYB_=VngMu zIHS8#!(V&raewuBP-=PPk3HslPLY9v3DOi$pM2zLjo{_nw4^aa)uXPs<biFtx|FN@ zt&)s!3lsLK)Ccs%K?k;J6L#TVG}6-6a^QG7b_V3Itc47ePbX$>&}p{|&-i4M3BMJZ zfC|-2A|Gs2{XQZerq0JpV)s%-UVn_e#6Y`jZERv8+P%IwzdCH??y~iXB0n|1dJFgi zCmH@|U1ih!nRl^mz&lT8&WQ}ZvWMc0{%UnKBqgk5)W2!8v-&muZDfy+UTeil4m?ho zgRrgixFe@eH7j<7QpJs+sK@jqO0tl)5xgH|qIE}ElZ6fSFfcE9cf~hs5X>5`<P#52 zZyVj(#CBNgX$mcaL*`P-g^F`V(`|Mde-nY_V6Cm@AOGbG-zu3)ZmUi(_9U2dv^-a= zEohNR6763H+}4KX%8&U32f3{KQ@D$AsbP+M8Ly8d=`~Z>UJmudi;njakvA`58ZCjz ze^cMcJV}?BHRhsw(sH%S5b)|FXC*M4%z>%2*gBmvGc{=^C#R0UywL4QhVxGy?Ixfd zw`Kf~ot|^P-8OtXNo^&wx!hi`H@Oc?&h?|<T-EN64eiYF9qk&P7@k*PbD&0cHg7Dx zuzo&!@$n7&4*VR9M_#4A<yB6?<(8)2O<7?Ya$;lWV9uj7o2HXnj834$nTRDK9)BXU z){`e^(U@WfxOgtFX3lzRz7*4BUZK|!%i5zB=VyC2?oG})cwF&@i}=d@yFzjL!nRzV z^y(tW^L-TFKYs7^vq8y4v;{`h!NlU{&cT5wtAu`_AvYNZ`iURvi9!`vHc@PUj!wLL z*TUSraKuMw#jmM!8fizx+8?tARw#AC{2_q?I&SGayWHyUuqd=4w37?P*B$P^h6!VW za2yp0vMtu}0Q|9o7dPUJB_~a`IYFZ<uf??s(<|n<0QDXFnycxQGaO{ktV7iNMEq)u z%Q$&UZD`{wxf)m@JhiU|c532H{K{uKE*H{L-AwG$_K_Kp&gU7wQ1-j&q3*TUWx@&z zk2DXDR#QXK8#e0#E(54#kRSascAP4va0${;Ojd83mFsP+A8M)tuIeLGY(h{?ebIoU z+gkTCcO!-*jnPe07)`9sa%wEt&sYKb!c$X=y~M)4vgU7njY*qJZh`vYAdR6slW$FX zA{s_jml{#VniWT(x?NeF+oP+?77mY;8Ockvuk5MpH{*ax7Mo@?`7f3719jNE`nY5` zIVnnSR|vQ$qHU_cC56L&B?7gyyjA`!kYi!0pmPP^l=bBKQ-hp}oy0V0jo#>xU?U1z zmKmzmAuDcOHI8*GN#=HedK%!t@3si`6dRe4{tEMYdbX*LxynKP8=o-XnxL{c2Z`ye zz41c5vMeGmuH~JYbX4Ync*<UQmz9;ZPj0}wJZnY&okwOObBQ!fiMJ$S5RId!F%NBx zU!+rol3wcSedgq9dhwuvDA`!tA`fR|FA56c$QR4*;(Frb%J{j3p9i8Ixhd+!ZotEm zN!^G)dnx*lzi|Rfko3fR<ku;z_TNK%rDOZOR(CoIO&7&uPuA)bu&9p<oB95NvXzAS Og#-ovO}G62a``_;?^F%| literal 0 HcmV?d00001 diff --git a/src/vs/workbench/contrib/agentsVoice/browser/voiceModeOnboarding.ts b/src/vs/workbench/contrib/agentsVoice/browser/voiceModeOnboarding.ts index e4345ca7945..ecc5fd0a2db 100644 --- a/src/vs/workbench/contrib/agentsVoice/browser/voiceModeOnboarding.ts +++ b/src/vs/workbench/contrib/agentsVoice/browser/voiceModeOnboarding.ts @@ -33,10 +33,13 @@ import './media/voiceModeOnboarding.css'; /** Setting the banner writes when a voice chip is picked. */ const VOICE_SETTING = 'agents.voice.voice'; +/** Setting that controls the language Voice Mode speaks. */ +const VOICE_LANGUAGE_SETTING = 'agents.voice.language'; + /** Where the first link sends anyone who wants to change their mind later. */ const VOICE_SETTINGS_COMMAND = 'agentsVoice.openSettings'; -type VoiceModeOnboardingAction = 'shown' | 'selectVoice' | 'selectMicrophone' | 'openSettings' | 'close' | 'escape'; +type VoiceModeOnboardingAction = 'shown' | 'selectVoice' | 'previewVoice' | 'selectMicrophone' | 'openSettings' | 'close' | 'escape'; type VoiceModeOnboardingActionClassification = { action: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The action taken in the Voice Mode onboarding card.' }; @@ -122,6 +125,42 @@ const VOICES: readonly IVoiceModeVoice[] = [ }, ]; +/** + * A language Voice Mode speaks natively, and the single voice its backend uses + * for that language. Choosing between voices is an English-only affordance, so + * for these languages the card previews this one voice rather than the four + * English options. + */ +interface ILocalizedVoice { + readonly id: string; + readonly label: string; +} + +const LOCALIZED_VOICES: Readonly<Record<string, ILocalizedVoice>> = { + de: { id: 'de_marc_neutral', label: localize('voiceMode.onboarding.voice.marc', "Marc") }, + es: { id: 'es-ES_maria_neutral', label: localize('voiceMode.onboarding.voice.maria', "Maria") }, + fr: { id: 'fr_david_neutral', label: localize('voiceMode.onboarding.voice.david', "David") }, + it: { id: 'it_eva_neutral', label: localize('voiceMode.onboarding.voice.eva', "Eva") }, + ja: { id: 'ja_aruha_neutral', label: localize('voiceMode.onboarding.voice.aruha', "Aruha") }, + ko: { id: 'ko_jiyon_neutral', label: localize('voiceMode.onboarding.voice.jiyon', "Jiyon") }, + pt: { id: 'pt-BR_gil_neutral', label: localize('voiceMode.onboarding.voice.gil', "Gil") }, + zh: { id: 'zh_wuzhi_neutral', label: localize('voiceMode.onboarding.voice.wuzhi', "Wuzhi") }, +}; + +/** + * The native voice for a spoken language, or `undefined` when the language has + * no native voice and the card should fall back to the English voice chooser. + */ +function localizedVoiceForLanguage(language: string): ILocalizedVoice | undefined { + try { + const canonical = Intl.getCanonicalLocales(language.trim())[0]; + const base = canonical?.split('-')[0].toLowerCase(); + return base ? LOCALIZED_VOICES[base] : undefined; + } catch { + return undefined; + } +} + /** * The trace before anyone has chosen: the four signatures averaged component by * component, so it belongs to no voice in particular rather than quietly being @@ -447,11 +486,11 @@ class VoiceSamplePlayer extends Disposable { return Math.min(1, Math.sqrt(sum / this.levels.length) * 3.2); } - play(voiceId: string): void { + play(sampleId: string): void { this.stop(); try { const audio = this.ensureAudio(); - audio.src = FileAccess.asBrowserUri(`vs/workbench/contrib/agentsVoice/browser/media/${voiceId}.mp3`).toString(true); + audio.src = FileAccess.asBrowserUri(`vs/workbench/contrib/agentsVoice/browser/media/${sampleId}.mp3`).toString(true); const store = new DisposableStore(); store.add(dom.addDisposableListener(audio, 'ended', () => this.stop())); @@ -459,7 +498,7 @@ class VoiceSamplePlayer extends Disposable { store.add(toDisposable(() => audio.pause())); this.playback.value = store; - this.setPlayingVoice(voiceId); + this.setPlayingVoice(sampleId); audio.play().catch(error => { this.logService.trace(`[voice] Voice Mode onboarding preview failed: ${error}`); this.stop(); @@ -525,6 +564,15 @@ export interface IVoiceModeOnboardingBannerOptions { readonly source: 'automatic' | 'manual'; /** Allows tests to provide a deterministic media element. */ readonly audioFactory?: () => HTMLAudioElement; + /** Allows tests to provide a deterministic spoken language. */ + readonly voiceLanguage?: string; +} + +/** A rendered voice option, with the strings its play state swaps between. */ +interface IVoiceElement { + readonly element: HTMLElement; + readonly label: string; + readonly restingAria: string; } /** @@ -547,7 +595,10 @@ export class VoiceModeOnboardingBanner extends Disposable { private microphoneOptions: IMicrophoneOption[] = []; private microphonePickerContainer: HTMLElement | undefined; - private readonly voiceElements = new Map<string, HTMLElement>(); + private readonly voiceElements = new Map<string, IVoiceElement>(); + + /** The native voice for the spoken language, when one exists. */ + private readonly localizedVoice: ILocalizedVoice | undefined; /** The voice being auditioned, and the one that will be committed. */ private selectedVoice: IVoiceModeVoice | undefined; @@ -577,6 +628,7 @@ export class VoiceModeOnboardingBanner extends Disposable { }, })); this.domNode = this.card.domNode; + this.localizedVoice = localizedVoiceForLanguage(this.resolveSpokenLanguage()); this.player = this._register(instantiationService.createInstance(VoiceSamplePlayer, this.domNode, options.audioFactory)); this._register(this.player.onDidChangePlayingVoice(voiceId => this.updatePlaying(voiceId))); @@ -705,14 +757,21 @@ export class VoiceModeOnboardingBanner extends Disposable { } /** - * The four voices as real buttons - border, hover lift, pressed feedback - - * because bare text gave no sign it could be clicked at all. + * The voices as real buttons - border, hover lift, pressed feedback - + * because bare text gave no sign it could be clicked at all. In a language + * Voice Mode speaks natively there is only one voice, so the card previews + * that voice instead of offering the English chooser. */ private renderVoices(container: HTMLElement): void { const labelText = localize('voiceMode.onboarding.voices', "Agent Voice:"); const label = dom.append(container, dom.$('.voice-mode-onboarding-voices-label')); label.textContent = labelText; + if (this.localizedVoice) { + this.renderLocalizedVoice(container, labelText, this.localizedVoice); + return; + } + const group = dom.append(container, dom.$('.voice-mode-onboarding-voices')); group.setAttribute('role', 'radiogroup'); group.setAttribute('aria-label', labelText); @@ -720,22 +779,14 @@ export class VoiceModeOnboardingBanner extends Disposable { for (const voice of VOICES) { const option = dom.append(group, dom.$('.voice-mode-onboarding-voice')); option.setAttribute('role', 'radio'); - option.setAttribute('aria-label', this.voiceAriaLabel(voice, false)); + const restingAria = localize('voiceMode.onboarding.voice.ariaLabel', "{0}. Hear this voice and use it for every conversation.", voice.label); + option.setAttribute('aria-label', restingAria); - // The icon is the affordance: it says "this will speak" before the - // click, and "this is yours" after it. - const icon = dom.append(option, dom.$('span.voice-mode-onboarding-voice-icon')); - dom.append(icon, dom.$(`span.codicon.codicon-${Codicon.play.id}.voice-mode-onboarding-voice-idle`)).setAttribute('aria-hidden', 'true'); - dom.append(icon, dom.$(`span.codicon.codicon-${Codicon.checkCompact.id}.voice-mode-onboarding-voice-chosen`)).setAttribute('aria-hidden', 'true'); - const bars = dom.append(icon, dom.$('span.voice-mode-onboarding-voice-bars')); - bars.setAttribute('aria-hidden', 'true'); - for (let bar = 0; bar < 3; bar++) { - dom.append(bars, dom.$('span.voice-mode-onboarding-voice-bar')); - } + this.appendVoiceIcon(option); const label = dom.append(option, dom.$('span.voice-mode-onboarding-voice-label')); label.textContent = voice.label; - this.voiceElements.set(voice.id, option); + this.voiceElements.set(voice.id, { element: option, label: voice.label, restingAria }); this._register(dom.addDisposableListener(option, dom.EventType.CLICK, () => this.selectVoice(voice))); this._register(dom.addDisposableListener(option, dom.EventType.KEY_DOWN, event => this.handleOptionKey(event, voice))); @@ -744,14 +795,53 @@ export class VoiceModeOnboardingBanner extends Disposable { this.updateSelection(); } - // --- Shared behaviour --- + /** + * The single native voice for the spoken language, as a preview button: + * there is nothing to choose, so it only ever plays and stops. + */ + private renderLocalizedVoice(container: HTMLElement, ariaLabel: string, voice: ILocalizedVoice): void { + const group = dom.append(container, dom.$('.voice-mode-onboarding-voices')); + group.setAttribute('aria-label', ariaLabel); - private voiceAriaLabel(voice: IVoiceModeVoice, playing: boolean): string { - return playing - ? localize('voiceMode.onboarding.voice.stopPreview', "Stop {0} preview.", voice.label) - : localize('voiceMode.onboarding.voice.ariaLabel', "{0}. Hear this voice and use it for every conversation.", voice.label); + const option = dom.append(group, dom.$('.voice-mode-onboarding-voice')); + option.setAttribute('role', 'button'); + option.tabIndex = 0; + const restingAria = localize('voiceMode.onboarding.voice.previewAriaLabel', "{0}. Hear how your agent will sound.", voice.label); + option.setAttribute('aria-label', restingAria); + + this.appendVoiceIcon(option); + + const label = dom.append(option, dom.$('span.voice-mode-onboarding-voice-label')); + label.textContent = voice.label; + this.voiceElements.set(voice.id, { element: option, label: voice.label, restingAria }); + + this._register(dom.addDisposableListener(option, dom.EventType.CLICK, () => this.previewLocalizedVoice(voice))); + this._register(dom.addDisposableListener(option, dom.EventType.KEY_DOWN, event => { + const keyboardEvent = new StandardKeyboardEvent(event); + if (keyboardEvent.equals(KeyCode.Enter) || keyboardEvent.equals(KeyCode.Space)) { + keyboardEvent.preventDefault(); + this.previewLocalizedVoice(voice); + } + })); } + /** + * The icon is the affordance: it says "this will speak" before the click, + * animating bars while it speaks, then a check once a voice is chosen. + */ + private appendVoiceIcon(option: HTMLElement): void { + const icon = dom.append(option, dom.$('span.voice-mode-onboarding-voice-icon')); + dom.append(icon, dom.$(`span.codicon.codicon-${Codicon.play.id}.voice-mode-onboarding-voice-idle`)).setAttribute('aria-hidden', 'true'); + dom.append(icon, dom.$(`span.codicon.codicon-${Codicon.checkCompact.id}.voice-mode-onboarding-voice-chosen`)).setAttribute('aria-hidden', 'true'); + const bars = dom.append(icon, dom.$('span.voice-mode-onboarding-voice-bars')); + bars.setAttribute('aria-hidden', 'true'); + for (let bar = 0; bar < 3; bar++) { + dom.append(bars, dom.$('span.voice-mode-onboarding-voice-bar')); + } + } + + // --- Shared behaviour --- + private handleOptionKey(event: KeyboardEvent, voice: IVoiceModeVoice): void { const keyboardEvent = new StandardKeyboardEvent(event); if (keyboardEvent.equals(KeyCode.Enter) || keyboardEvent.equals(KeyCode.Space)) { @@ -770,7 +860,7 @@ export class VoiceModeOnboardingBanner extends Disposable { const index = VOICES.indexOf(voice); const next = VOICES[(index + (forward ? 1 : VOICES.length - 1)) % VOICES.length]; this.selectVoice(next); - this.voiceElements.get(next.id)?.focus(); + this.voiceElements.get(next.id)?.element.focus(); } } @@ -857,11 +947,43 @@ export class VoiceModeOnboardingBanner extends Disposable { .catch(error => this.logService.error(`[voice] Failed to persist the Voice Mode voice: ${error}`)); } + /** + * The localized voice is not a choice - it is the only voice for the + * language - so previewing it just plays and stops, and never persists. + */ + private previewLocalizedVoice(voice: ILocalizedVoice): void { + if (this.player.playingVoice === voice.id) { + this.player.stop(); + status(localize('voiceMode.onboarding.voice.localizedStopped', "{0} preview stopped.", voice.label)); + return; + } + this.logAction('previewVoice'); + this.player.play(voice.id); + status(localize('voiceMode.onboarding.voice.localizedPlaying', "Playing {0} preview.", voice.label)); + } + + /** + * The spoken language, mirroring the resolution the voice client uses: an + * explicit test override, then the configured language (unless `auto`), then + * the window's language. + */ + private resolveSpokenLanguage(): string { + if (this.options.voiceLanguage) { + return this.options.voiceLanguage; + } + + const configuredLanguage = this.configurationService.getValue<string>(VOICE_LANGUAGE_SETTING)?.trim(); + if (configuredLanguage && configuredLanguage.toLowerCase() !== 'auto') { + return configuredLanguage; + } + return dom.getWindow(this.domNode).navigator.language; + } + private updateSelection(): void { - for (const [id, element] of this.voiceElements) { + for (const [id, entry] of this.voiceElements) { const selected = id === this.selectedVoice?.id; - element.classList.toggle('selected', selected); - element.setAttribute('aria-checked', String(selected)); + entry.element.classList.toggle('selected', selected); + entry.element.setAttribute('aria-checked', String(selected)); } this.updateTabStop(); } @@ -872,21 +994,20 @@ export class VoiceModeOnboardingBanner extends Disposable { */ private updateTabStop(): void { let first = true; - for (const [id, element] of this.voiceElements) { + for (const [id, entry] of this.voiceElements) { const isTabStop = this.selectedVoice === undefined ? first : id === this.selectedVoice.id; - element.tabIndex = isTabStop ? 0 : -1; + entry.element.tabIndex = isTabStop ? 0 : -1; first = false; } } private updatePlaying(playingVoice: string | undefined): void { - for (const [id, element] of this.voiceElements) { + for (const [id, entry] of this.voiceElements) { const playing = id === playingVoice; - element.classList.toggle('playing', playing); - const voice = VOICES.find(candidate => candidate.id === id); - if (voice) { - element.setAttribute('aria-label', this.voiceAriaLabel(voice, playing)); - } + entry.element.classList.toggle('playing', playing); + entry.element.setAttribute('aria-label', playing + ? localize('voiceMode.onboarding.voice.stopPreview', "Stop {0} preview.", entry.label) + : entry.restingAria); } this.domNode.classList.toggle('playing', playingVoice !== undefined); } diff --git a/src/vs/workbench/contrib/agentsVoice/test/browser/voiceModeOnboarding.test.ts b/src/vs/workbench/contrib/agentsVoice/test/browser/voiceModeOnboarding.test.ts index f6cf4dfb1fb..5edee20f9aa 100644 --- a/src/vs/workbench/contrib/agentsVoice/test/browser/voiceModeOnboarding.test.ts +++ b/src/vs/workbench/contrib/agentsVoice/test/browser/voiceModeOnboarding.test.ts @@ -188,6 +188,53 @@ suite('Voice Mode onboarding', () => { }); }); + test('previews the native voice per language and keeps the chooser only for English', () => { + const instantiationService = workbenchInstantiationService(undefined, disposables); + instantiationService.stub(IAccessibilityService, new class extends mock<IAccessibilityService>() { + override readonly onDidChangeScreenReaderOptimized = Event.None; + override readonly onDidChangeReducedMotion = Event.None; + override isScreenReaderOptimized(): boolean { return false; } + override isMotionReduced(): boolean { return false; } + }); + + // A language Voice Mode speaks natively shows its one voice with no + // chooser; English and languages without a native voice keep the four. + const cases = [ + { language: 'de-DE', options: 1, chooser: false, sample: 'de_marc_neutral.mp3' }, + { language: 'es-MX', options: 1, chooser: false, sample: 'es-ES_maria_neutral.mp3' }, + { language: 'fr-CA', options: 1, chooser: false, sample: 'fr_david_neutral.mp3' }, + { language: 'it-IT', options: 1, chooser: false, sample: 'it_eva_neutral.mp3' }, + { language: 'ja-JP', options: 1, chooser: false, sample: 'ja_aruha_neutral.mp3' }, + { language: 'ko-KR', options: 1, chooser: false, sample: 'ko_jiyon_neutral.mp3' }, + { language: 'pt-PT', options: 1, chooser: false, sample: 'pt-BR_gil_neutral.mp3' }, + { language: 'zh-TW', options: 1, chooser: false, sample: 'zh_wuzhi_neutral.mp3' }, + { language: 'en-GB', options: 4, chooser: true, sample: 'maya_neutral.mp3' }, + { language: 'is', options: 4, chooser: true, sample: 'maya_neutral.mp3' }, + ]; + const actual: { language: string; options: number; chooser: boolean; sample: string }[] = []; + + for (const { language } of cases) { + const host = createHost(disposables); + const audio = document.createElement('audio'); + audio.play = () => Promise.resolve(); + disposables.add(instantiationService.createInstance(VoiceModeOnboardingBanner, { + container: host.container, + onDismiss: () => undefined, + source: 'manual', + audioFactory: () => audio, + voiceLanguage: language, + })); + + const options = host.container.querySelectorAll('.voice-mode-onboarding-voice').length; + const chooser = !!host.container.querySelector('.voice-mode-onboarding-voices[role="radiogroup"]'); + host.container.querySelector<HTMLElement>('.voice-mode-onboarding-voice')!.click(); + const sample = audio.src.split(/[?#]/)[0].split('/').pop() ?? ''; + actual.push({ language, options, chooser, sample }); + } + + assert.deepStrictEqual(actual, cases); + }); + test('can be shown again manually', () => { const telemetryEvents: ITelemetryEvent[] = []; const service = createService(disposables, [], [], telemetryEvents); From b9397197d8bc39df343e434a24407ce087b12bb4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:44:43 +0000 Subject: [PATCH 79/86] Merge pull request #328261 from microsoft/copilot/fix-gate-chat-tips Gate chat tips on registered commands to prevent 'openPlan' command not found --- .../contrib/chat/browser/chatTipService.ts | 18 +++++++- .../chat/test/browser/chatTipService.test.ts | 45 ++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 0d6e5fd41d8..67f1cd5bb58 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -13,7 +13,7 @@ import { getSelectedModelIdentifier } from '../common/chatSelectedModel.js'; import { ChatAgentLocation, ChatConfiguration } from '../common/constants.js'; import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { localize } from '../../../../nls.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -767,10 +767,26 @@ export class ChatTipService extends Disposable implements IChatTipService { this._logService.debug('#ChatTips: tip excluded because thinking phrases setting was previously modified', tip.id); return false; } + if (!this._areTipCommandsRegistered(tip)) { + return false; + } this._logService.debug('#ChatTips: tip is eligible', tip.id); return true; } + private _areTipCommandsRegistered(tip: ITipDefinition): boolean { + const ctx: ITipBuildContext = { keybindingService: this._keybindingService, experimentalTipMessages: this._experimentalTipMessages }; + const rawMessage = tip.buildMessage(ctx); + const commandIds = extractCommandIds(rawMessage.value); + for (const commandId of commandIds) { + if (!CommandsRegistry.getCommand(commandId)) { + this._logService.debug('#ChatTips: tip excluded because command is not registered', tip.id, commandId); + return false; + } + } + return true; + } + private _isSettingModified(key: string): boolean { const inspected = this._configurationService.inspect(key); return inspected.userValue !== undefined diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index 756b69f1967..e7983cd0bfa 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { ICommandEvent, ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ICommandEvent, ICommandService, CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { ContextKeyExpression, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; @@ -22,6 +22,7 @@ import { NullWorkbenchAssignmentService } from '../../../../services/assignment/ import { ChatTipService, CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND, CREATE_AGENT_TRACKING_COMMAND, CREATE_PROMPT_TRACKING_COMMAND, CREATE_SKILL_TRACKING_COMMAND, FORK_CONVERSATION_TRACKING_COMMAND, IChatTip, ITipDefinition, TipEligibilityTracker } from '../../browser/chatTipService.js'; import { AgentInstructionFileType, IPromptPath, IPromptsService, IAgentInstructionFile, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { URI } from '../../../../../base/common/uri.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { IsSessionsWindowContext } from '../../../../common/contextkeys.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { storeSelectedModel } from '../../common/chatSelectedModel.js'; @@ -29,7 +30,7 @@ import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; import { MockLanguageModelToolsService } from '../common/tools/mockLanguageModelToolsService.js'; -import { ChatTipTier, TIP_CATALOG } from '../../browser/chatTipCatalog.js'; +import { ChatTipTier, TIP_CATALOG, extractCommandIds } from '../../browser/chatTipCatalog.js'; import { ChatEntitlement, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { TestChatEntitlementService } from '../../../../test/common/workbenchTestServices.js'; import { IChatService } from '../../common/chatService/chatService.js'; @@ -73,6 +74,31 @@ suite('ChatTipService', () => { let mockInstructionFiles: IAgentInstructionFile[]; let mockPromptInstructionFiles: IPromptPath[]; let chatEntitlementService: TestChatEntitlementService; + let catalogCommandRegistrations: Map<string, IDisposable>; + + /** + * Registers every `command:` link referenced by the real {@link TIP_CATALOG} so that tips are + * considered eligible, simulating a running workbench where these commands exist. Returns a map + * keyed by command id so individual registrations can be disposed to simulate a missing command. + */ + function registerCatalogCommands(): Map<string, IDisposable> { + const registrations = new Map<string, IDisposable>(); + for (const tip of TIP_CATALOG) { + const message = tip.buildMessage({ + keybindingService: { lookupKeybinding: () => undefined } as Partial<IKeybindingService> as IKeybindingService, + experimentalTipMessages: new Map(), + }).value; + for (const commandId of extractCommandIds(message)) { + if (registrations.has(commandId) || CommandsRegistry.getCommand(commandId)) { + continue; + } + const registration = CommandsRegistry.registerCommand(commandId, () => { }); + registrations.set(commandId, registration); + testDisposables.add(registration); + } + } + return registrations; + } function createProductService(hasCopilot: boolean): IProductService { return { @@ -132,6 +158,7 @@ suite('ChatTipService', () => { lookupKeybinding: () => undefined, } as Partial<IKeybindingService> as IKeybindingService); instantiationService.stub(IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()); + catalogCommandRegistrations = registerCatalogCommands(); }); test('returns a welcome tip', () => { @@ -605,6 +632,20 @@ suite('ChatTipService', () => { assert.strictEqual(previousTip.id, 'tip.planMode', 'Expected previous tip to reverse the preferred ordering'); }); + test('excludes a tip whose command is not registered', () => { + // Simulate a shipped build where the tip references a command that was never registered + // (see https://github.com/microsoft/vscode/issues/328231). + catalogCommandRegistrations.get('workbench.action.chat.openPlan')!.dispose(); + + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatModeKind.key, ChatModeKind.Agent); + contextKeyService.createKey(ChatContextKeys.chatModeName.key, 'Agent'); + contextKeyService.createKey(ChatContextKeys.chatSessionType.key, localChatSessionType); + contextKeyService.createKey(ChatContextKeys.chatModelId.key, 'auto'); + + assertTipNeverShown(service, 'tip.planMode'); + }); + test('getNextEligibleTip returns next tip even when only one remains', async () => { const service = createService(); From 7be272dbeef40788d165b5057bf74a9137666b9c Mon Sep 17 00:00:00 2001 From: Henning Dieterichs <hdieterichs@microsoft.com> Date: Fri, 31 Jul 2026 18:13:40 +0200 Subject: [PATCH 80/86] Enable breadcrumbs in the Agents window --- src/vs/workbench/browser/parts/editor/breadcrumbs.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts index 7ab9a9f58b2..869126b5085 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts @@ -127,7 +127,7 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat description: localize('enabled', "Enable/disable navigation breadcrumbs."), type: 'boolean', default: true, - agentsWindow: { default: false }, + agentsWindow: { default: true }, }, 'breadcrumbs.filePath': { description: localize('filepath', "Controls whether and how file paths are shown in the breadcrumbs view."), @@ -172,6 +172,7 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat markdownDescription: localize('showEditorType', "Controls whether the breadcrumbs bar shows a dropdown to switch between the editors that can open the current file (for example the text editor and a custom editor). The dropdown only appears when a more specialized editor is available."), type: 'boolean', default: false, + agentsWindow: { default: true }, tags: ['experimental'] }, 'breadcrumbs.symbolPathSeparator': { From 0b3d1eff6223616724c4d8a6212151adefce06bc Mon Sep 17 00:00:00 2001 From: Henning Dieterichs <hdieterichs@microsoft.com> Date: Fri, 31 Jul 2026 18:13:42 +0200 Subject: [PATCH 81/86] Enable Markdown editor defaults in Agents window --- .../browser/parts/editor/editorConfiguration.ts | 5 ++++- .../services/editor/common/editorResolverService.ts | 7 +++++++ .../test/browser/editorResolverService.test.ts | 12 +++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorConfiguration.ts b/src/vs/workbench/browser/parts/editor/editorConfiguration.ts index b2739bc4821..3a95c7ae65e 100644 --- a/src/vs/workbench/browser/parts/editor/editorConfiguration.ts +++ b/src/vs/workbench/browser/parts/editor/editorConfiguration.ts @@ -9,7 +9,7 @@ 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, markdownDefaultEditorAgentsWindowSettingId, RegisteredEditorInfo, RegisteredEditorPriority, toRegisteredEditorPriorityInfo } from '../../../services/editor/common/editorResolverService.js'; +import { diffEditorsAssociationsAgentsWindowDefault, 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'; @@ -192,6 +192,9 @@ export class DynamicEditorConfigurations extends Disposable implements IWorkbenc type: 'string', enum: binaryEditorCandidates, } + }, + agentsWindow: { + default: diffEditorsAssociationsAgentsWindowDefault({ markdownDefaultEditor: markdownDefaultEditorEnabled }) } } } diff --git a/src/vs/workbench/services/editor/common/editorResolverService.ts b/src/vs/workbench/services/editor/common/editorResolverService.ts index b6bc4ac8eca..ec0a6eb950c 100644 --- a/src/vs/workbench/services/editor/common/editorResolverService.ts +++ b/src/vs/workbench/services/editor/common/editorResolverService.ts @@ -59,6 +59,10 @@ export function editorsAssociationsAgentsWindowDefault(options?: { markdownDefau }; } +export function diffEditorsAssociationsAgentsWindowDefault(options?: { markdownDefaultEditor?: boolean }): Record<string, string> { + return editorsAssociationsAgentsWindowDefault(options); +} + const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration); const editorAssociationsConfigurationNode: IConfigurationNode = { @@ -86,6 +90,9 @@ const editorAssociationsConfigurationNode: IConfigurationNode = { markdownDescription: localize('editor.diffEditorAssociations', "Configure [glob patterns](https://aka.ms/vscode-glob-patterns) to editors for diff views (for example `\"*.md\": \"vscode.markdown.preview.editor\"`). These override `workbench.editorAssociations` for diffs."), additionalProperties: { type: 'string' + }, + agentsWindow: { + default: diffEditorsAssociationsAgentsWindowDefault() } } } diff --git a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts index 0883de52895..4a350275cf7 100644 --- a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts @@ -12,11 +12,21 @@ import { EditorPart } from '../../../../browser/parts/editor/editorPart.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { EditorResolverService } from '../../browser/editorResolverService.js'; import { IEditorGroupsService } from '../../common/editorGroupsService.js'; -import { IEditorResolverService, ResolvedStatus, RegisteredEditorPriority, diffEditorsAssociationsSettingId, editorsAssociationsSettingId } from '../../common/editorResolverService.js'; +import { diffEditorsAssociationsAgentsWindowDefault, IEditorResolverService, ResolvedStatus, RegisteredEditorPriority, diffEditorsAssociationsSettingId, editorsAssociationsSettingId } from '../../common/editorResolverService.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { createEditorPart, ITestInstantiationService, TestFileEditorInput, TestServiceAccessor, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; suite('EditorResolverService', () => { + test('Agents window diff editor default follows the Markdown editor setting', () => { + assert.deepStrictEqual({ + enabled: diffEditorsAssociationsAgentsWindowDefault({ markdownDefaultEditor: true }), + disabled: diffEditorsAssociationsAgentsWindowDefault({ markdownDefaultEditor: false }), + }, { + enabled: { '*.md': 'vscode.markdown.editor' }, + disabled: { '*.md': 'vscode.markdown.preview.editor' }, + }); + }); + const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorResolverService'; const disposables = new DisposableStore(); From c5f0840e786da973d9a960096e46f49ba6f14746 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs <hdieterichs@microsoft.com> Date: Fri, 31 Jul 2026 17:36:41 +0200 Subject: [PATCH 82/86] Render Markdown diffs in the Markdown editor --- .../src/preview/lineDiff.ts | 9 ++- .../src/preview/markdownEditorProvider.ts | 67 ++++++++++++++++++- .../src/test/markdownEditorProvider.test.ts | 26 +++++++ 3 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 extensions/markdown-language-features/src/test/markdownEditorProvider.test.ts diff --git a/extensions/markdown-language-features/src/preview/lineDiff.ts b/extensions/markdown-language-features/src/preview/lineDiff.ts index bd5c1d98781..855e93f2e78 100644 --- a/extensions/markdown-language-features/src/preview/lineDiff.ts +++ b/extensions/markdown-language-features/src/preview/lineDiff.ts @@ -9,6 +9,7 @@ import type { MarkdownPreviewChangeIndicator, MarkdownPreviewInnerChange, Markdo interface LineChanges { readonly added: readonly number[]; readonly deleted: readonly number[]; + readonly changedLineRanges: readonly ChangedLineRange[]; readonly originalToModified: readonly number[]; readonly modifiedToOriginal: readonly number[]; readonly originalInnerChanges: readonly MarkdownPreviewInnerChange[]; @@ -21,7 +22,7 @@ interface LineMappings { readonly modifiedToOriginal: number[]; } -type ChangedLineRange = Pick<vscode.TextDiffChange, 'originalRange' | 'modifiedRange'>; +export type ChangedLineRange = Pick<vscode.TextDiffChange, 'originalRange' | 'modifiedRange'>; export class MarkdownPreviewLineDiffProvider { @@ -55,6 +56,10 @@ export class MarkdownPreviewLineDiffProvider { return added.length || innerChanges.length || changeIndicators.length ? { added, innerChanges, changeIndicators } : undefined; } + public async getChangedLineRanges(): Promise<readonly ChangedLineRange[]> { + return (await this.#getLineChanges()).changedLineRanges; + } + public async translateOriginalLineToModified(line: number): Promise<number> { return translateLine(line, (await this.#getLineChanges()).originalToModified, this.#modifiedDocument.lineCount); } @@ -143,7 +148,7 @@ async function computeLineChanges(originalDocument: vscode.TextDocument, modifie const splitChangedLineRanges = splitChangedLineRangesByMarkdownBlocks(changedLineRanges, originalDocument, modifiedDocument); const changeIndicators = createChangeIndicators(splitChangedLineRanges, originalDocument, modifiedDocument, originalInnerChanges, modifiedInnerChanges); - return { added, deleted, originalInnerChanges, modifiedInnerChanges, changeIndicators, ...mappings }; + return { added, deleted, changedLineRanges, originalInnerChanges, modifiedInnerChanges, changeIndicators, ...mappings }; } function createChangeIndicators(ranges: readonly ChangedLineRange[], originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument, originalInnerChanges: readonly MarkdownPreviewInnerChange[], modifiedInnerChanges: readonly MarkdownPreviewInnerChange[]): MarkdownPreviewChangeIndicator[] { diff --git a/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts b/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts index cdab2924fba..cbcdad0492a 100644 --- a/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts +++ b/extensions/markdown-language-features/src/preview/markdownEditorProvider.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import { Disposable } from '../util/dispose'; import { MdLinkOpener } from '../util/openDocumentLink'; import { getMarkdownLocalResourceRoots } from '../util/resources'; +import { ChangedLineRange, MarkdownPreviewLineDiffProvider } from './lineDiff'; /** * Experimental hybrid (WYSIWYG) Markdown editor backed by the @@ -43,6 +44,18 @@ export class MarkdownEditorProvider extends Disposable implements vscode.CustomT webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken, ): Promise<void> { + await this.#resolveEditor(document, webviewPanel, token); + } + + public async resolveCustomTextEditorInlineDiff( + documents: vscode.CustomEditorDiffDocuments<vscode.TextDocument>, + webviewPanel: vscode.WebviewPanel, + token: vscode.CancellationToken, + ): Promise<void> { + await this.#resolveEditor(documents.modified, webviewPanel, token, documents.original); + } + + async #resolveEditor(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken, originalDocument?: vscode.TextDocument): Promise<void> { if (!vscode.workspace.isTrusted) { const cancel = { title: vscode.l10n.t("Cancel"), isCloseAffordance: true }; const openAnyway = { title: vscode.l10n.t("Open Anyway") }; @@ -61,9 +74,12 @@ export class MarkdownEditorProvider extends Disposable implements vscode.CustomT } } + if (token.isCancellationRequested) { + return; + } const webview = webviewPanel.webview; this.#configureWebview(document.uri, webview); - this.#wireSingle(document, webviewPanel); + this.#wireSingle(document, webviewPanel, originalDocument); } #configureWebview(documentUri: vscode.Uri, webview: vscode.Webview): void { @@ -76,7 +92,7 @@ export class MarkdownEditorProvider extends Disposable implements vscode.CustomT webview.html = this.#getHtml(documentUri, webview); } - #wireSingle(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel): void { + #wireSingle(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, originalDocument?: vscode.TextDocument): void { const webview = webviewPanel.webview; let isUpdatingFromWebview = false; let editQueue = Promise.resolve(); @@ -143,7 +159,9 @@ export class MarkdownEditorProvider extends Disposable implements vscode.CustomT }); const highlight = this.#wireHighlight(webview); - const quickDiff = this.#wireQuickDiff(document, webview); + const quickDiff = originalDocument + ? this.#wireDocumentDiff(originalDocument, document, webview) + : this.#wireQuickDiff(document, webview); const comments = this.#wireComments(document, webview); const onDidGrantWorkspaceTrust = vscode.workspace.onDidGrantWorkspaceTrust(() => { this.#configureWebview(document.uri, webview); @@ -198,6 +216,32 @@ export class MarkdownEditorProvider extends Disposable implements vscode.CustomT return vscode.Disposable.from(diffProvider, onChange, onMessage, onDocumentChange); } + #wireDocumentDiff(originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument, webview: vscode.Webview): vscode.Disposable { + const lineDiffProvider = new MarkdownPreviewLineDiffProvider(originalDocument, modifiedDocument); + const postMarkers = async () => { + const originalVersion = originalDocument.version; + const modifiedVersion = modifiedDocument.version; + const changes = await lineDiffProvider.getChangedLineRanges(); + if (originalVersion !== originalDocument.version || modifiedVersion !== modifiedDocument.version) { + return; + } + webview.postMessage({ type: 'gutterMarkers', markers: lineRangesToGutterMarkers(modifiedDocument, changes) }); + }; + + const onMessage = webview.onDidReceiveMessage(message => { + if (message.type === 'ready') { + void postMarkers(); + } + }); + const onDocumentChange = vscode.workspace.onDidChangeTextDocument(event => { + if (event.document.uri.toString() === originalDocument.uri.toString() || event.document.uri.toString() === modifiedDocument.uri.toString()) { + void postMarkers(); + } + }); + + return vscode.Disposable.from(onMessage, onDocumentChange); + } + /** * Bridges the workbench's agent/session comments (the same store the code * editor renders its comments from) to the webview: existing comments are @@ -351,3 +395,20 @@ function toGutterMarkers(document: vscode.TextDocument, changes: readonly vscode } return markers; } + +export function lineRangesToGutterMarkers(document: vscode.TextDocument, changes: readonly ChangedLineRange[]): GutterMarkerMessage[] { + return changes.map(change => { + if (change.modifiedRange.isEmpty) { + const offset = document.offsetAt(change.modifiedRange.start); + return { start: offset, endExclusive: offset, type: 'deleted' }; + } + + const start = document.offsetAt(change.modifiedRange.start); + const endExclusive = document.offsetAt(document.lineAt(change.modifiedRange.end.line - 1).range.end); + return { + start, + endExclusive, + type: change.originalRange.isEmpty ? 'added' : 'modified', + }; + }); +} diff --git a/extensions/markdown-language-features/src/test/markdownEditorProvider.test.ts b/extensions/markdown-language-features/src/test/markdownEditorProvider.test.ts new file mode 100644 index 00000000000..6faec966a64 --- /dev/null +++ b/extensions/markdown-language-features/src/test/markdownEditorProvider.test.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import 'mocha'; +import * as vscode from 'vscode'; +import { lineRangesToGutterMarkers } from '../preview/markdownEditorProvider'; + +suite('Markdown editor diff', () => { + test('maps modified-side line changes to quick diff gutter markers', async () => { + const document = await vscode.workspace.openTextDocument({ language: 'markdown', content: 'one\ntwo changed\nthree added\nfour\n' }); + const changes = [ + { originalRange: new vscode.Range(1, 0, 2, 0), modifiedRange: new vscode.Range(1, 0, 2, 0) }, + { originalRange: new vscode.Range(2, 0, 2, 0), modifiedRange: new vscode.Range(2, 0, 3, 0) }, + { originalRange: new vscode.Range(3, 0, 4, 0), modifiedRange: new vscode.Range(3, 0, 3, 0) }, + ]; + + assert.deepStrictEqual(lineRangesToGutterMarkers(document, changes), [ + { start: 4, endExclusive: 15, type: 'modified' }, + { start: 16, endExclusive: 27, type: 'added' }, + { start: 28, endExclusive: 28, type: 'deleted' }, + ]); + }); +}); From 94de33c54eec333997bea423b31dad8f6f9f3c22 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs <hdieterichs@microsoft.com> Date: Fri, 31 Jul 2026 17:39:27 +0200 Subject: [PATCH 83/86] Classify inline custom editors as diff editors --- .../browser/parts/editor/editorTypePicker.ts | 16 +++--- src/vs/workbench/common/editor.ts | 13 +++++ .../browser/customEditorDiffInput.ts | 11 ++++- .../parts/editor/editorTypePicker.test.ts | 49 +++++++++++++++++-- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorTypePicker.ts b/src/vs/workbench/browser/parts/editor/editorTypePicker.ts index 787a84fe80d..5c7be67457e 100644 --- a/src/vs/workbench/browser/parts/editor/editorTypePicker.ts +++ b/src/vs/workbench/browser/parts/editor/editorTypePicker.ts @@ -8,7 +8,7 @@ import { extUri } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { DEFAULT_EDITOR_ASSOCIATION, EditorResourceAccessor, SideBySideEditor, isDiffEditorInput } from '../../../common/editor.js'; +import { DEFAULT_EDITOR_ASSOCIATION, EditorResourceAccessor, SideBySideEditor, isDiffEditorInput, isEditorInputWithDiffResources } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { IEditorResolverService, RegisteredEditorInfo, RegisteredEditorPriority, priorityToRank } from '../../../services/editor/common/editorResolverService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -33,7 +33,12 @@ export interface IAvailableEditorTypes { * exclusive editor (e.g. the hex editor, for which `getEditors` returns an empty list). */ export function getAvailableEditorTypes(activeEditor: EditorInput | null | undefined, editorResolverService: IEditorResolverService): IAvailableEditorTypes | undefined { - const resource = EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); + const standardDiffResources = isDiffEditorInput(activeEditor) ? { + original: activeEditor.original.resource, + modified: activeEditor.modified.resource, + } : undefined; + const diffResources = standardDiffResources ?? (isEditorInputWithDiffResources(activeEditor) ? activeEditor.diffResources : undefined); + const resource = diffResources?.modified ?? EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); if (!resource) { return undefined; } @@ -41,12 +46,11 @@ export function getAvailableEditorTypes(activeEditor: EditorInput | null | undef if (editors.length <= 1) { return undefined; } - const isDiffEditor = isDiffEditorInput(activeEditor); return { resource, - isDiffEditor, - originalResource: isDiffEditor ? activeEditor.original.resource : undefined, - modifiedResource: isDiffEditor ? activeEditor.modified.resource : undefined, + isDiffEditor: !!diffResources, + originalResource: diffResources?.original, + modifiedResource: diffResources?.modified, currentId: activeEditor?.editorId ?? DEFAULT_EDITOR_ASSOCIATION.id, editors }; diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 88501d42223..93a4c38804a 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -966,6 +966,19 @@ export function isDiffEditorInput(editor: unknown): editor is IDiffEditorInput { return isEditorInput(candidate?.modified) && isEditorInput(candidate?.original); } +export interface IEditorInputWithDiffResources extends EditorInput { + readonly diffResources: { + readonly original: URI; + readonly modified: URI; + }; +} + +export function isEditorInputWithDiffResources(editor: unknown): editor is IEditorInputWithDiffResources { + const candidate = editor as IEditorInputWithDiffResources | undefined; + + return URI.isUri(candidate?.diffResources?.original) && URI.isUri(candidate.diffResources.modified); +} + export interface IUntypedFileEditorInput extends ITextResourceEditorInput { /** diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorDiffInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorDiffInput.ts index 7b69f362424..16951f9d59b 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorDiffInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorDiffInput.ts @@ -12,7 +12,7 @@ import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs. import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; -import { EditorInputCapabilities, GroupIdentifier, IResourceDiffEditorInput, IRevertOptions, ISaveOptions, IUntypedEditorInput, isEditorInput, isResourceEditorInput, isResourceDiffEditorInput, Verbosity } from '../../../common/editor.js'; +import { EditorInputCapabilities, GroupIdentifier, IEditorInputWithDiffResources, IResourceDiffEditorInput, IRevertOptions, ISaveOptions, IUntypedEditorInput, isEditorInput, isResourceEditorInput, isResourceDiffEditorInput, Verbosity } from '../../../common/editor.js'; import { EditorInput, IUntypedEditorOptions } from '../../../common/editor/editorInput.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; @@ -41,7 +41,7 @@ function getCustomEditorSideBySideDiffInputResource(init: CustomEditorSideBySide return init.side === 'original' ? init.originalResource : init.modifiedResource; } -export class CustomEditorDiffInput extends LazilyResolvedWebviewEditorInput { +export class CustomEditorDiffInput extends LazilyResolvedWebviewEditorInput implements IEditorInputWithDiffResources { private readonly _modelRef = this._register(new MutableDisposable<IReference<ICustomEditorModel>>()); @@ -113,6 +113,13 @@ export class CustomEditorDiffInput extends LazilyResolvedWebviewEditorInput { return this.init.modifiedResource; } + get diffResources(): IEditorInputWithDiffResources['diffResources'] { + return { + original: this.originalResource, + modified: this.modifiedResource, + }; + } + override getName(): string { return this.init.label ?? localize('customEditorDiffLabel', "{0} - {1}", basename(this.originalResource), basename(this.modifiedResource)); } diff --git a/src/vs/workbench/test/browser/parts/editor/editorTypePicker.test.ts b/src/vs/workbench/test/browser/parts/editor/editorTypePicker.test.ts index fe90c8dd8f7..0a89b1fc19e 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorTypePicker.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorTypePicker.test.ts @@ -4,15 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { mock } from '../../../../../base/test/common/mock.js'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { DEFAULT_EDITOR_ASSOCIATION } from '../../../../common/editor.js'; -import { IAvailableEditorTypes, hasDefaultEditorAssociation } from '../../../../browser/parts/editor/editorTypePicker.js'; -import { RegisteredEditorInfo, RegisteredEditorPriority } from '../../../../services/editor/common/editorResolverService.js'; +import { DEFAULT_EDITOR_ASSOCIATION, IEditorInputWithDiffResources } from '../../../../common/editor.js'; +import { EditorInput } from '../../../../common/editor/editorInput.js'; +import { getAvailableEditorTypes, IAvailableEditorTypes, hasDefaultEditorAssociation } from '../../../../browser/parts/editor/editorTypePicker.js'; +import { IEditorResolverService, RegisteredEditorInfo, RegisteredEditorPriority } from '../../../../services/editor/common/editorResolverService.js'; suite('Editor Type Picker', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); function editor(id: string, editorPriority: RegisteredEditorPriority, diffPriority = editorPriority): RegisteredEditorInfo { return { @@ -62,4 +64,43 @@ suite('Editor Type Picker', () => { diffDefaultEditor: true, }); }); + + test('inline custom diff editor is classified as a diff editor', () => { + const original = URI.file('/original/test.md'); + const modified = URI.file('/modified/test.md'); + const registeredEditors = [ + editor(DEFAULT_EDITOR_ASSOCIATION.id, RegisteredEditorPriority.builtin), + editor('test.markdownEditor', RegisteredEditorPriority.option, RegisteredEditorPriority.never), + ]; + const input = disposables.add(new class extends EditorInput implements IEditorInputWithDiffResources { + override get typeId(): string { return 'test.inlineCustomDiffEditor'; } + override get editorId(): string { return 'test.markdownEditor'; } + override get resource(): URI { return modified; } + get diffResources(): IEditorInputWithDiffResources['diffResources'] { return { original, modified }; } + override getName(): string { return 'test'; } + }()); + const requestedResources: URI[] = []; + const editorResolverService = new class extends mock<IEditorResolverService>() { + override getEditors(resource?: URI): RegisteredEditorInfo[] { + if (resource) { + requestedResources.push(resource); + } + return registeredEditors; + } + }; + + const result = getAvailableEditorTypes(input, editorResolverService); + + assert.deepStrictEqual({ requestedResources, result }, { + requestedResources: [modified], + result: { + resource: modified, + isDiffEditor: true, + originalResource: original, + modifiedResource: modified, + currentId: 'test.markdownEditor', + editors: registeredEditors, + } + }); + }); }); \ No newline at end of file From e483bc57959ab1b894641c5d20449d3a3f2a7e8c Mon Sep 17 00:00:00 2001 From: Mir <mirimadahmed@outlook.com> Date: Sat, 1 Aug 2026 01:05:23 +0800 Subject: [PATCH 84/86] Voice: warm capture before hands-free playback (#328225) * Voice: warm capture before hands-free playback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 15eb9602-6daa-4c99-8455-0a20804e569a * Voice: handle microphone initialization failures Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix voice controller test dependency ordering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f93d2f2-660e-46e9-a869-98ce147905f9 --------- Co-authored-by: Mir Imad Ahmed <mirimadahmed@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Megan Rogge <merogge@microsoft.com> Copilot-Session: 15eb9602-6daa-4c99-8455-0a20804e569a Copilot-Session: 1f93d2f2-660e-46e9-a869-98ce147905f9 --- .../browser/voiceClient/micCaptureService.ts | 165 +++++-- .../browser/voiceClient/voiceClientService.ts | 13 +- .../voiceClient/voiceSessionController.ts | 112 +++-- .../common/voiceClient/voiceClientService.ts | 6 +- .../voiceClient/micCaptureService.test.ts | 70 +++ .../voiceClient/voiceClientService.test.ts | 30 +- .../voiceSessionController.test.ts | 452 +++++++++++++++++- 7 files changed, 752 insertions(+), 96 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/voiceClient/micCaptureService.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/micCaptureService.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/micCaptureService.ts index e0902844da4..3104dd3d635 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/micCaptureService.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/micCaptureService.ts @@ -17,12 +17,12 @@ import { createPcmCaptureNode } from '../pcmCaptureWorklet.js'; export const IMicCaptureService = createDecorator<IMicCaptureService>('micCaptureService'); -/** - * Number of samples buffered in the capture worklet before a chunk is posted to - * the main thread. Matches the buffer size previously used with - * `ScriptProcessorNode` so the per-chunk drain/diagnostic bookkeeping is unchanged. - */ -const MIC_CAPTURE_CHUNK_SIZE = 2048; +/** Number of samples buffered per 32 ms voice capture chunk at 16 kHz, matching one Silero VAD frame. */ +export const MIC_CAPTURE_CHUNK_SIZE = 512; + +export function isMicrophonePermissionDeniedError(error: unknown): boolean { + return (error instanceof DOMException || error instanceof Error) && error.name === 'NotAllowedError'; +} /** * Per-PTT-press diagnostic emitted after `pttUp` once the diagnostic @@ -170,6 +170,9 @@ export class MicCaptureService extends Disposable implements IMicCaptureService private _workletNode: AudioWorkletNode | undefined; private _analyserNode: AnalyserNode | undefined; private _isCapturing = false; + private _captureGeneration = 0; + private _capturePromise: Promise<void> | undefined; + private _pttGeneration = 0; private _pttHeld = false; private _pttStreaming = false; private _isMuted = false; @@ -244,6 +247,7 @@ export class MicCaptureService extends Disposable implements IMicCaptureService async pttDown(turnId: string, passive: boolean = false): Promise<void> { if (this._pttHeld) { return; } + const pttGeneration = ++this._pttGeneration; // If a previous press is still in its drain window, finish it // now: cancel the fallback timer, mark streaming closed, fire // `_onPttEnd`. Otherwise the backend would keep the prior turn @@ -278,13 +282,22 @@ export class MicCaptureService extends Disposable implements IMicCaptureService try { await this.startCapture(this._window); } catch (err) { + if (pttGeneration !== this._pttGeneration) { + return; + } this._pttHeld = false; this._pttStreaming = false; - this._pttAcquiring = false; this._pttReleasedDuringAcquire = false; throw err; + } finally { + if (pttGeneration === this._pttGeneration) { + this._pttAcquiring = false; + } + } + if (pttGeneration !== this._pttGeneration || !this._isCapturing || !this._pttHeld) { + this._pttReleasedDuringAcquire = false; + return; } - this._pttAcquiring = false; this._onPttStart.fire(passive); if (this._pttReleasedDuringAcquire) { @@ -348,6 +361,8 @@ export class MicCaptureService extends Disposable implements IMicCaptureService } this._pttDrainTargetSamples = 0; this._pttDrainSamplesSent = 0; + this._pttGeneration++; + this._pttAcquiring = false; this._pttHeld = false; this._pttStreaming = false; this._pttReleasedDuringAcquire = false; @@ -359,6 +374,22 @@ export class MicCaptureService extends Disposable implements IMicCaptureService async startCapture(window: Window & typeof globalThis): Promise<void> { this._window = window; if (this._isCapturing) { return; } + if (this._capturePromise) { + return this._capturePromise; + } + const capturePromise = this._startCapture(window); + this._capturePromise = capturePromise; + try { + await capturePromise; + } finally { + if (this._capturePromise === capturePromise) { + this._capturePromise = undefined; + } + } + } + + private async _startCapture(window: Window & typeof globalThis): Promise<void> { + const captureGeneration = this._captureGeneration; const deviceId = this.storageService.get(AgentsVoiceStorageKeys.MicrophoneDevice, StorageScope.APPLICATION); const audioConstraints: MediaTrackConstraints = { channelCount: 1, @@ -396,34 +427,53 @@ export class MicCaptureService extends Disposable implements IMicCaptureService throw err; } } + if (captureGeneration !== this._captureGeneration) { + micStream.getTracks().forEach(track => track.stop()); + return; + } this._micStream = micStream; - // Detect a hardware-muted microphone (e.g. a physical kill switch). - // `getUserMedia` succeeds in this case but the track produces silence, - // so without this check PTT would appear to work while capturing nothing. - this._micTrackListeners.clear(); - this._micMutedNotified = false; - const audioTrack = micStream.getAudioTracks()[0]; - if (audioTrack) { - if (audioTrack.muted) { - this._notifyMicrophoneMuted(); + const cleanupFailedCapture = () => { + if (this._micStream === micStream) { + this._stopCaptureResources(); + } else { + micStream.getTracks().forEach(track => track.stop()); } - this._micTrackListeners.add(addDisposableListener(audioTrack, 'mute', () => this._notifyMicrophoneMuted())); - this._micTrackListeners.add(addDisposableListener(audioTrack, 'unmute', () => { this._micMutedNotified = false; })); + }; + + let ctx: AudioContext; + let source: MediaStreamAudioSourceNode; + try { + // Detect a hardware-muted microphone (e.g. a physical kill switch). + // `getUserMedia` succeeds in this case but the track produces silence, + // so without this check PTT would appear to work while capturing nothing. + this._micTrackListeners.clear(); + this._micMutedNotified = false; + const audioTrack = micStream.getAudioTracks()[0]; + if (audioTrack) { + if (audioTrack.muted) { + this._notifyMicrophoneMuted(); + } + this._micTrackListeners.add(addDisposableListener(audioTrack, 'mute', () => this._notifyMicrophoneMuted())); + this._micTrackListeners.add(addDisposableListener(audioTrack, 'unmute', () => { this._micMutedNotified = false; })); + } + + if (!this._micCtx) { + this._micCtx = new window.AudioContext({ sampleRate: 16000 }); + } + ctx = this._micCtx; + source = ctx.createMediaStreamSource(micStream); + + const analyser = ctx.createAnalyser(); + analyser.fftSize = 256; + source.connect(analyser); + this._analyserNode = analyser; + } catch (err) { + cleanupFailedCapture(); + throw err; } - if (!this._micCtx) { - this._micCtx = new window.AudioContext({ sampleRate: 16000 }); - } - const ctx = this._micCtx; - const source = ctx.createMediaStreamSource(micStream); - - const analyser = ctx.createAnalyser(); - analyser.fftSize = 256; - source.connect(analyser); - this._analyserNode = analyser; - - const { node } = await createPcmCaptureNode(window, ctx, MIC_CAPTURE_CHUNK_SIZE, samples => { + const captureNodePromise = createPcmCaptureNode(window, ctx, MIC_CAPTURE_CHUNK_SIZE, samples => { const nowTs = Date.now(); const ptUpTs = this._diagPttUpTs; // A callback is a "drain" callback while we're still in the @@ -480,20 +530,33 @@ export class MicCaptureService extends Disposable implements IMicCaptureService } }); + let node: AudioWorkletNode; + try { + node = (await captureNodePromise).node; + } catch (err) { + cleanupFailedCapture(); + throw err; + } + // stopCapture() may have run while the worklet module was loading. if (this._micCtx !== ctx) { try { node.disconnect(); } catch { /* ignore */ } return; } - this._workletNode = node; - source.connect(node); - node.connect(ctx.destination); - this._isCapturing = true; + try { + this._workletNode = node; + source.connect(node); + node.connect(ctx.destination); + this._isCapturing = true; + } catch (err) { + cleanupFailedCapture(); + throw err; + } } private _notifyMicPermissionDenied(err: unknown): void { - if (err instanceof DOMException && err.name === 'NotAllowedError') { + if (isMicrophonePermissionDeniedError(err)) { this.notificationService.notify({ severity: Severity.Error, message: localize('mic.permissionDenied', "Microphone access was denied. Grant microphone permission in your system settings to use Voice Mode."), @@ -513,17 +576,9 @@ export class MicCaptureService extends Disposable implements IMicCaptureService }); } - stopCapture(): void { - // Cancel any in-flight drain; do NOT fire `_onPttEnd` here - // because callers (reconnect / disconnect / dispose) have - // already torn down or are about to tear down the backend - // connection. - if (this._pttDrainFallbackTimer) { - clearTimeout(this._pttDrainFallbackTimer); - this._pttDrainFallbackTimer = undefined; - } - this._pttDrainTargetSamples = 0; - this._pttDrainSamplesSent = 0; + private _stopCaptureResources(): void { + this._captureGeneration++; + this._capturePromise = undefined; if (this._workletNode) { this._workletNode.port.onmessage = null; try { this._workletNode.disconnect(); } catch { /* ignore */ } @@ -539,6 +594,22 @@ export class MicCaptureService extends Disposable implements IMicCaptureService this._micTrackListeners.clear(); this._micMutedNotified = false; this._isCapturing = false; + } + + stopCapture(): void { + this._stopCaptureResources(); + this._pttGeneration++; + this._pttAcquiring = false; + // Cancel any in-flight drain; do NOT fire `_onPttEnd` here + // because callers (reconnect / disconnect / dispose) have + // already torn down or are about to tear down the backend + // connection. + if (this._pttDrainFallbackTimer) { + clearTimeout(this._pttDrainFallbackTimer); + this._pttDrainFallbackTimer = undefined; + } + this._pttDrainTargetSamples = 0; + this._pttDrainSamplesSent = 0; this._pttHeld = false; this._pttStreaming = false; this._pttReleasedDuringAcquire = false; diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts index 424ffc59f43..f27c290d007 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts @@ -145,6 +145,10 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic return this._isResuming; } + get willReconnect(): boolean { + return this._reconnectTimer !== undefined; + } + get currentSessionId(): string | undefined { return this._lastSessionId; } @@ -387,7 +391,7 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic this._onSessionInit.fire({ sessionId: msg.session_id ?? '' }); break; case 'speech_started': - this._onSpeechStarted.fire({}); + this._onSpeechStarted.fire({ turnId: asOptionalString(msg.turn_id) }); break; case 'barge_in': this._onBargeIn.fire({ @@ -527,7 +531,6 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic } this._reconnectAttempts++; - this._setConnected(false); this._stopPing(); this._ws = undefined; @@ -535,7 +538,11 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic ? FAST_RETRY_DELAY_MS : SLOW_RETRY_DELAY_MS; this._logService.warn(`[voice] ws closed abnormally (code=${evt.code} reason=${evt.reason || 'none'} wasClean=${evt.wasClean}); reconnecting in ${delay}ms (attempt ${this._reconnectAttempts})`); - this._reconnectTimer = setTimeout(() => this._connectWebSocket(), delay); + this._reconnectTimer = setTimeout(() => { + this._reconnectTimer = undefined; + this._connectWebSocket(); + }, delay); + this._setConnected(false); } }; } diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts index 13453ced663..daca646139b 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceSessionController.ts @@ -24,7 +24,7 @@ import { IAuthenticationService } from '../../../../services/authentication/comm import { IVoiceTranscriptEntryMetadata, IVoiceTranscriptStore, IVoiceTranscriptTurn, VoiceTranscriptKind } from '../../../agentsVoice/common/voiceTranscriptStore.js'; import { IVoiceAudioResponse, IVoiceBargeIn, IVoiceCheckpointNarrationMetadata, IVoiceClientService, IVoicePriorTimelineEntry, IVoiceSessionContext, IVoiceFeedbackPayload, IVoiceFeedbackTranscriptTurn, IVoiceTranscription, IVoiceTurnAutoEnded, IVoiceNarrationAck, IVoiceNarrationSignal, isVoiceCheckpointId, VoiceCheckpointId, VoiceConfirmationType, VoiceNarrationKind, IVoiceSessionPending, IVoicePendingQuestion, derivePendingId, VOICE_AGENT_PROGRESS_SETTING } from '../../common/voiceClient/voiceClientService.js'; import { getVoiceConfirmationType, isPendingVoiceQuestionnaireInvocation, isVoiceQuestionnaireInvocation } from '../../common/voiceClient/voiceConfirmation.js'; -import { IMicCaptureService, IPttDiagnostic } from './micCaptureService.js'; +import { IMicCaptureService, IPttDiagnostic, isMicrophonePermissionDeniedError } from './micCaptureService.js'; import { ITtsPlaybackService } from './ttsPlaybackService.js'; import { IVoiceToolDispatchService, VoiceToolDispatchService } from './voiceToolDispatchService.js'; import { IVoicePlaybackService } from '../../common/voicePlaybackService.js'; @@ -391,6 +391,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC private readonly _connectWatchdog = this._register(new MutableDisposable()); private static readonly _CONNECT_TIMEOUT_MS = 10000; private _connectAttemptGeneration = 0; + private _sessionInitializationGeneration = 0; private readonly _autoApprovedSessions = new Set<string>(); private _transcriptFadeTimer: ReturnType<typeof setTimeout> | undefined; private _pttMaxDurationTimer: ReturnType<typeof setTimeout> | undefined; @@ -1169,6 +1170,10 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // Connection state → start mic + send start session this._voiceEventDisposables.add(this.voiceClientService.onDidChangeConnectionState(async connected => { if (connected) { + const sessionInitializationGeneration = ++this._sessionInitializationGeneration; + // Every socket open, including reconnects, gets a full timeout window + // covering voice instructions, mic warm-up, and the session command. + this._armConnectWatchdog(); const pbCtx = this.ttsPlaybackService.ensureContext(window); pbCtx.resume(); @@ -1196,11 +1201,43 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC const voiceInstructions = await this.promptsService.getVoiceInstructions(CancellationToken.None); if ( connectAttemptGeneration !== this._connectAttemptGeneration || + sessionInitializationGeneration !== this._sessionInitializationGeneration || !this.voiceClientService.isConnected || (!this._isConnecting.get() && !this._isReconnecting.get()) ) { return; } + if (isResuming) { + this.micCaptureService.stopCapture(); + } + this.micCaptureService.prepare(window); + if (this._isHandsFreeEnabled()) { + try { + await this.micCaptureService.startCapture(window); + } catch (err) { + if ( + connectAttemptGeneration !== this._connectAttemptGeneration || + sessionInitializationGeneration !== this._sessionInitializationGeneration || + !this.voiceClientService.isConnected || + (!this._isConnecting.get() && !this._isReconnecting.get()) + ) { + return; + } + this.logService.warn('[voice] failed to warm microphone capture for hands-free mode; resetting voice mode', err); + const permissionDenied = isMicrophonePermissionDeniedError(err); + this._resetFailedConnection(!permissionDenied); + return; + } + if ( + connectAttemptGeneration !== this._connectAttemptGeneration || + sessionInitializationGeneration !== this._sessionInitializationGeneration || + !this.voiceClientService.isConnected || + (!this._isConnecting.get() && !this._isReconnecting.get()) + ) { + return; + } + } + if (isResuming) { this.voiceClientService.sendResumeSession(this._buildSessionContext(), this._getMachineId(), voiceInstructions); } else { @@ -1209,17 +1246,6 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this.voiceClientService.sendStartSession(this._buildSessionContext(), this._getMachineId(), priorTimeline, undefined, voiceInstructions); } - // On a reconnect cycle, refresh the mic stream: the old MediaStream - // may have gone stale while the WS was down, so we stop+start to - // guarantee a clean capture before the user PTTs again. - if (isResuming) { - this.micCaptureService.stopCapture(); - } - this.micCaptureService.prepare(window); - // Mic is acquired lazily on the first pttDown, not eagerly on - // connect. This avoids switching bluetooth headsets into speech - // mode and prevents the backend from hearing ambient audio. - transaction(tx => { this._isConnecting.set(false, tx); this._isReconnecting.set(false, tx); @@ -1565,7 +1591,7 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC this._statusText.set('Hold to speak...', undefined); this._voiceState.set('idle', undefined); - // Wait for the backend session ack before opening the hands-free mic. + // Wait for the backend session ack before opening the hands-free PTT turn. this._enterListenOnSessionInit = this._shouldEnterListenOnSessionInit(isResuming); this.logService.trace(`[voice] connected: isResuming=${isResuming} handsFree=${this._isHandsFreeEnabled()} armListen=${this._enterListenOnSessionInit}`); if (this._enterListenOnSessionInit) { @@ -1577,24 +1603,27 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC } }, 750)); } - } else if (this._fatalDisconnect) { - // Terminal close already handled by _handleFatalDisconnect: stay in - // the clean, restartable state and do NOT enter the reconnect path - // (which would strand the UI on "Reconnecting..." with no reconnect). - } else if (this._isConnected.get()) { - this._onConnectionLost(); - } else if (this._isReconnecting.get()) { - this._isReconnecting.set(false, undefined); - this._voiceState.set('idle', undefined); - this._statusText.set('Tap to start', undefined); - } else if (this._isConnecting.get()) { - // Connection failed during initial handshake (e.g. fatal WS close). - // Clear isConnecting so callers awaiting the state settle properly. - this._isConnecting.set(false, undefined); - this._voiceState.set('idle', undefined); - this._statusText.set('Tap to start', undefined); } else { - this._voiceState.set('idle', undefined); + this._sessionInitializationGeneration++; + if (this._fatalDisconnect) { + // Terminal close already handled by _handleFatalDisconnect: stay in + // the clean, restartable state and do NOT enter the reconnect path + // (which would strand the UI on "Reconnecting..." with no reconnect). + } else if (!this.voiceClientService.willReconnect) { + this.disconnect(); + } else if (this._isConnected.get()) { + this._onConnectionLost(); + } else { + // A transient socket drop invalidates the in-flight warm-up. Keep + // the controller armed for the service's already-scheduled retry. + this.micCaptureService.stopCapture(); + transaction(tx => { + this._isConnecting.set(false, tx); + this._isReconnecting.set(true, tx); + }); + this._voiceState.set('idle', undefined); + this._statusText.set('Reconnecting...', undefined); + } } })); @@ -1649,9 +1678,13 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC // Speech started → stop TTS, suppress late chunks from the previous turn // (same flow as pttDown, but for server-VAD path). - this._voiceEventDisposables.add(this.voiceClientService.onSpeechStarted(() => { + this._voiceEventDisposables.add(this.voiceClientService.onSpeechStarted(event => { this._clearAutoListenTimer(); this._interruptAssistantPlayback(); + const turnId = event.turnId || this._pttCurrentTurnId; + if (turnId && this._transcriptionTurnState?.turnId !== turnId) { + this._beginTranscriptionTurn(turnId); + } this._startUserTurn(); })); @@ -1962,16 +1995,22 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC */ private _armConnectWatchdog(): void { this._connectWatchdog.value = disposableTimeout(() => { - if (!this._isConnecting.get() || this._isConnected.get()) { + if ((!this._isConnecting.get() && !this._isReconnecting.get()) || this._isConnected.get()) { return; } this.logService.warn('[voice] connect handshake timed out; resetting voice mode'); - this.disconnect(); + this._resetFailedConnection(); + }, VoiceSessionController._CONNECT_TIMEOUT_MS); + } + + private _resetFailedConnection(notifyUser = true): void { + this.disconnect(); + if (notifyUser) { this.notificationService.notify({ severity: Severity.Warning, message: localize('voice.connectFailed', "Voice mode could not connect. Please try again."), }); - }, VoiceSessionController._CONNECT_TIMEOUT_MS); + } } /** @@ -2863,9 +2902,8 @@ export class VoiceSessionController extends Disposable implements IVoiceSessionC * stays open and becomes the next listening turn once playback ends * (`onPlaybackStopped` sees `_pttHeld` and stays in 'listening'). * - * Reuses the warm mic left by the previous turn's `abortPtt`, so no - * `getUserMedia` re-acquisition occurs. Idempotent: a no-op while a turn is - * already held. + * Hands-free session initialization keeps capture warm before the backend can + * send playback. Idempotent: a no-op while a turn is already held. */ private _startBargeInListen(): void { if (!this._isHandsFreeEnabled() || !this._isConnected.get() || this._pttHeld || this._autoListenHeld || this._autoListenSuppressed || !this._window) { diff --git a/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts b/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts index a3bac20bf13..1a83c62aed2 100644 --- a/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts +++ b/src/vs/workbench/contrib/chat/common/voiceClient/voiceClientService.ts @@ -216,7 +216,9 @@ export interface IVoiceToolCall { readonly args: Record<string, unknown>; } -export interface IVoiceSpeechStarted { } +export interface IVoiceSpeechStarted { + readonly turnId?: string; +} export interface IVoiceSessionInit { readonly sessionId: string; @@ -431,6 +433,8 @@ export interface IVoiceClientService { // --- State --- readonly isConnected: boolean; readonly isResuming: boolean; + /** Whether the current socket close has an automatic retry scheduled. */ + readonly willReconnect: boolean; /** Backend session id assigned by the realtime server, or ``undefined`` when not yet established. */ readonly currentSessionId: string | undefined; } diff --git a/src/vs/workbench/contrib/chat/test/browser/voiceClient/micCaptureService.test.ts b/src/vs/workbench/contrib/chat/test/browser/voiceClient/micCaptureService.test.ts new file mode 100644 index 00000000000..c594089ade1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/voiceClient/micCaptureService.test.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { mainWindow } from '../../../../../../base/browser/window.js'; +import { mock } from '../../../../../../base/test/common/mock.js'; +import { NullLogService } from '../../../../../../platform/log/common/log.js'; +import { TestNotificationService } from '../../../../../../platform/notification/test/common/testNotificationService.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; +import { MIC_CAPTURE_CHUNK_SIZE, MicCaptureService } from '../../../browser/voiceClient/micCaptureService.js'; + +suite('MicCaptureService', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('buffers 32 ms voice chunks at 16 kHz', () => { + assert.deepStrictEqual({ + samples: MIC_CAPTURE_CHUNK_SIZE, + durationMs: MIC_CAPTURE_CHUNK_SIZE / 16, + }, { + samples: 512, + durationMs: 32, + }); + }); + + test('propagates capture setup failures after cleaning up acquired resources', async () => { + const setupError = new Error('audio source setup failed'); + let trackStopCalls = 0; + const track = new class extends mock<MediaStreamTrack>() { + override stop(): void { trackStopCalls++; } + }(); + const stream = new class extends mock<MediaStream>() { + override getTracks(): MediaStreamTrack[] { return [track]; } + override getAudioTracks(): MediaStreamTrack[] { return []; } + }(); + const targetWindow = Object.create(mainWindow) as Window & typeof globalThis; + Object.defineProperties(targetWindow, { + navigator: { + value: { + mediaDevices: { + getUserMedia: async () => stream, + }, + }, + }, + AudioContext: { + value: class { + close(): Promise<void> { return Promise.resolve(); } + createMediaStreamSource(): never { throw setupError; } + }, + }, + }); + const service = store.add(new MicCaptureService( + store.add(new TestStorageService()), + new TestNotificationService(), + new NullLogService(), + )); + service.prepare(targetWindow); + + await assert.rejects(() => service.pttDown('turn-1'), error => error === setupError); + assert.deepStrictEqual({ + isCapturing: service.isCapturing, + trackStopCalls, + }, { + isCapturing: false, + trackStopCalls: 1, + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts index af298ee2365..f3765510417 100644 --- a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts @@ -12,7 +12,7 @@ import { NullLogService } from '../../../../../../platform/log/common/log.js'; import product from '../../../../../../platform/product/common/product.js'; import { IProductService } from '../../../../../../platform/product/common/productService.js'; import { VoiceClientService } from '../../../browser/voiceClient/voiceClientService.js'; -import { IVoiceAudioResponse, IVoiceBargeIn, IVoiceNarrationAck, IVoiceNarrationSignal, IVoiceTranscription } from '../../../common/voiceClient/voiceClientService.js'; +import { IVoiceAudioResponse, IVoiceBargeIn, IVoiceNarrationAck, IVoiceNarrationSignal, IVoiceSpeechStarted, IVoiceTranscription } from '../../../common/voiceClient/voiceClientService.js'; class TestWebSocket { static instance: TestWebSocket | undefined; @@ -125,6 +125,22 @@ suite('VoiceClientService', () => { }]); }); + test('preserves the turn ID on speech-started events', async () => { + const { service } = createService(); + const events: IVoiceSpeechStarted[] = []; + store.add(service.onSpeechStarted(event => events.push(event))); + + await service.connect(createTestWindow()); + socket().onmessage?.(new mainWindow.MessageEvent('message', { + data: JSON.stringify({ + type: 'speech_started', + turn_id: 'passive-turn', + }), + })); + + assert.deepStrictEqual(events, [{ turnId: 'passive-turn' }]); + }); + test('preserves checkpoint interruption metadata from the backend', async () => { const { service } = createService(); const events: IVoiceNarrationSignal[] = []; @@ -745,4 +761,16 @@ suite('VoiceClientService', () => { assert.strictEqual(service.isResuming, false); assert.strictEqual(service.currentSessionId, undefined); }); + + test('reports when an abnormal close has scheduled a reconnect', async () => { + const { service } = createService(); + await service.connect(createTestWindow()); + socket().onopen?.(); + + socket().onclose?.(new mainWindow.CloseEvent('close', { code: 4000 })); + + assert.strictEqual(service.willReconnect, true); + service.disconnect(); + assert.strictEqual(service.willReconnect, false); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts index ab7ead30d4e..48345dd5c65 100644 --- a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceSessionController.test.ts @@ -20,7 +20,8 @@ import { ICommandService } from '../../../../../../platform/commands/common/comm import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { NullLogService } from '../../../../../../platform/log/common/log.js'; -import { INotificationService } from '../../../../../../platform/notification/common/notification.js'; +import { INotification, INotificationHandle, INotificationService, NoOpNotification } from '../../../../../../platform/notification/common/notification.js'; +import { TestNotificationService } from '../../../../../../platform/notification/test/common/testNotificationService.js'; import { NullTelemetryService, NullTelemetryServiceShape } from '../../../../../../platform/telemetry/common/telemetryUtils.js'; import { IAuthenticationService } from '../../../../../services/authentication/common/authentication.js'; import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; @@ -47,6 +48,8 @@ import { MockChatService } from '../../common/chatService/mockChatService.js'; class TestVoiceClientService extends mock<IVoiceClientService>() { private narrationCounter = 0; readonly requests: { sessionId: string; kind: VoiceNarrationKind; text: string; narrationId: string; pendingId?: string; checkpoint?: IVoiceCheckpointNarrationMetadata; confirmationType?: VoiceConfirmationType }[] = []; + readonly sessionCommands: ('start' | 'resume')[] = []; + readonly sessionCommandSent = new DeferredPromise<void>(); private readonly audioResponseEmitter = new Emitter<IVoiceAudioResponse>(); override readonly onAudioResponse = this.audioResponseEmitter.event; private readonly bargeInEmitter = new Emitter<IVoiceBargeIn>(); @@ -71,8 +74,12 @@ class TestVoiceClientService extends mock<IVoiceClientService>() { override readonly onFatalDisconnect = Event.None; override readonly onTurnAutoEnded = Event.None; private connected = false; + private resuming = false; + private reconnecting = false; override get isConnected(): boolean { return this.connected; } + override get isResuming(): boolean { return this.resuming; } + override get willReconnect(): boolean { return this.reconnecting; } override disconnect(): void { this.connected = false; } override async connect(): Promise<void> { } readonly wireEvents: ({ type: 'session_context'; context: IVoiceSessionContext } | { type: 'request_narration'; kind: VoiceNarrationKind; text: string; confirmationType?: VoiceConfirmationType })[] = []; @@ -87,6 +94,14 @@ class TestVoiceClientService extends mock<IVoiceClientService>() { } } override invalidateSessionCache(): void { } + override sendStartSession(): void { + this.sessionCommands.push('start'); + this.sessionCommandSent.complete(); + } + override sendResumeSession(): void { + this.sessionCommands.push('resume'); + this.sessionCommandSent.complete(); + } readonly playbackCompletions: { sessionId: string; narrationId: string; playbackId: string }[] = []; override sendNarrationPlaybackComplete(codingSessionId: string, narrationId: string, playbackId: string): void { this.playbackCompletions.push({ sessionId: codingSessionId, narrationId, playbackId }); @@ -122,8 +137,8 @@ class TestVoiceClientService extends mock<IVoiceClientService>() { this.toolCallEmitter.fire(event); } - fireSpeechStarted(): void { - this.speechStartedEmitter.fire({}); + fireSpeechStarted(turnId?: string): void { + this.speechStartedEmitter.fire({ turnId }); } fireNarrationInterrupted(event: IVoiceNarrationSignal): void { @@ -138,11 +153,16 @@ class TestVoiceClientService extends mock<IVoiceClientService>() { this.narrationUnblockedEmitter.fire(event); } - fireConnectionState(connected: boolean): void { + fireConnectionState(connected: boolean, willReconnect = false): void { this.connected = connected; + this.reconnecting = !connected && willReconnect; this.connectionStateEmitter.fire(connected); } + setResuming(resuming: boolean): void { + this.resuming = resuming; + } + fireSessionInit(): void { this.sessionInitEmitter.fire({ sessionId: 'voice-session' }); } @@ -165,6 +185,12 @@ class RecordingMicCaptureService extends mock<IMicCaptureService>() { readonly pttDownCalls: { turnId: string; passive: boolean | undefined }[] = []; abortCalls = 0; prepareCalls = 0; + startCaptureCalls = 0; + stopCaptureCalls = 0; + readonly captureStarted = new DeferredPromise<void>(); + constructor(private readonly captureBarrier?: Promise<void>) { + super(); + } override readonly onPttStart = Event.None; override readonly onPttAudioChunk = Event.None; override readonly onPttEnd = Event.None; @@ -172,8 +198,14 @@ class RecordingMicCaptureService extends mock<IMicCaptureService>() { override readonly analyserNode = undefined; override isMuted = false; override prepare(): void { this.prepareCalls++; } - override async startCapture(): Promise<void> { } - override stopCapture(): void { } + override async startCapture(): Promise<void> { + this.startCaptureCalls++; + if (this.startCaptureCalls === 1) { + this.captureStarted.complete(); + } + await this.captureBarrier; + } + override stopCapture(): void { this.stopCaptureCalls++; } override abortPtt(): void { this.abortCalls++; } override pttUp(): void { } override suppressUntil(): void { } @@ -182,6 +214,15 @@ class RecordingMicCaptureService extends mock<IMicCaptureService>() { } } +class VoiceTestNotificationService extends TestNotificationService { + readonly notifications: INotification[] = []; + + override notify(notification: INotification): INotificationHandle { + this.notifications.push(notification); + return new NoOpNotification(); + } +} + class TestTtsPlaybackService extends mock<ITtsPlaybackService>() { readonly playedAudio: string[] = []; stopCount = 0; @@ -434,6 +475,7 @@ suite('VoiceSessionController', () => { override async getVoiceInstructions(): Promise<undefined> { return undefined; } }(), agentSessionsService: IAgentSessionsService = new TestAgentSessionsService(), + notificationService: INotificationService = new VoiceTestNotificationService(), ): IVoiceSessionController { store.add({ dispose: () => voiceClientService.dispose() }); store.add(ttsPlaybackService); @@ -467,7 +509,7 @@ suite('VoiceSessionController', () => { }(), new TestAccessibilityService(), new TestChatWidgetService(), - new class extends mock<INotificationService>() { }(), + notificationService, promptsService, )); } @@ -549,6 +591,351 @@ suite('VoiceSessionController', () => { }); }); + test('warms hands-free capture before starting or resuming the backend session', async () => { + const results: { + command: 'start' | 'resume'; + beforeWarmup: { + prepareCalls: number; + startCaptureCalls: number; + stopCaptureCalls: number; + sessionCommands: readonly ('start' | 'resume')[]; + socketConnected: boolean; + }; + afterWarmup: readonly ('start' | 'resume')[]; + }[] = []; + for (const command of ['start', 'resume'] as const) { + const voiceClientService = new TestVoiceClientService(); + voiceClientService.setResuming(command === 'resume'); + const captureBarrier = new DeferredPromise<void>(); + const micCaptureService = new RecordingMicCaptureService(captureBarrier.p); + const controller = createController( + voiceClientService, + undefined, + undefined, + undefined, + micCaptureService, + new TestConfigurationService({ 'agents.voice.handsFree': true }), + ); + await controller.connect(mainWindow); + + voiceClientService.fireConnectionState(true); + await micCaptureService.captureStarted.p; + const beforeWarmup = { + prepareCalls: micCaptureService.prepareCalls, + startCaptureCalls: micCaptureService.startCaptureCalls, + stopCaptureCalls: micCaptureService.stopCaptureCalls, + sessionCommands: [...voiceClientService.sessionCommands], + socketConnected: voiceClientService.isConnected, + }; + + captureBarrier.complete(); + await voiceClientService.sessionCommandSent.p; + results.push({ command, beforeWarmup, afterWarmup: voiceClientService.sessionCommands }); + } + + assert.deepStrictEqual(results, [{ + command: 'start', + beforeWarmup: { + prepareCalls: 1, + startCaptureCalls: 1, + stopCaptureCalls: 0, + sessionCommands: [], + socketConnected: true, + }, + afterWarmup: ['start'], + }, { + command: 'resume', + beforeWarmup: { + prepareCalls: 1, + startCaptureCalls: 1, + stopCaptureCalls: 1, + sessionCommands: [], + socketConnected: true, + }, + afterWarmup: ['resume'], + }]); + }); + + test('keeps microphone acquisition lazy when hands-free mode is disabled', async () => { + const voiceClientService = new TestVoiceClientService(); + const micCaptureService = new RecordingMicCaptureService(); + const controller = createController( + voiceClientService, + undefined, + undefined, + undefined, + micCaptureService, + new TestConfigurationService({ 'agents.voice.handsFree': false }), + ); + await controller.connect(mainWindow); + + voiceClientService.fireConnectionState(true); + await voiceClientService.sessionCommandSent.p; + + assert.deepStrictEqual({ + prepareCalls: micCaptureService.prepareCalls, + startCaptureCalls: micCaptureService.startCaptureCalls, + sessionCommands: voiceClientService.sessionCommands, + }, { + prepareCalls: 1, + startCaptureCalls: 0, + sessionCommands: ['start'], + }); + }); + + test('hands-free warm-up failure returns to idle and allows retry', async () => { + const voiceClientService = new TestVoiceClientService(); + const resetObserved = new DeferredPromise<void>(); + const micCaptureService = new class extends RecordingMicCaptureService { + override async startCapture(): Promise<void> { + this.startCaptureCalls++; + if (this.startCaptureCalls === 1) { + throw new Error('microphone unavailable'); + } + } + }(); + const controller = createController( + voiceClientService, + undefined, + undefined, + undefined, + micCaptureService, + new TestConfigurationService({ 'agents.voice.handsFree': true }), + undefined, + undefined, + undefined, + new class extends VoiceTestNotificationService { + override notify(notification: INotification): INotificationHandle { + resetObserved.complete(); + return super.notify(notification); + } + }(), + ); + await controller.connect(mainWindow); + + voiceClientService.fireConnectionState(true); + await resetObserved.p; + await Promise.resolve(); + const afterFailure = { + startCaptureCalls: micCaptureService.startCaptureCalls, + stopCaptureCalls: micCaptureService.stopCaptureCalls, + sessionCommands: [...voiceClientService.sessionCommands], + connecting: controller.isConnecting.get(), + connected: controller.isConnected.get(), + status: controller.statusText.get(), + }; + + await controller.connect(mainWindow); + voiceClientService.fireConnectionState(true); + await Promise.resolve(); + await Promise.resolve(); + assert.deepStrictEqual({ + afterFailure, + startCaptureCalls: micCaptureService.startCaptureCalls, + sessionCommands: voiceClientService.sessionCommands, + }, { + afterFailure: { + startCaptureCalls: 1, + stopCaptureCalls: 1, + sessionCommands: [], + connecting: false, + connected: false, + status: 'Tap to start', + }, + startCaptureCalls: 2, + sessionCommands: ['start'], + }); + }); + + test('hands-free permission denial does not add a generic connection notification', async () => { + const voiceClientService = new TestVoiceClientService(); + const notificationService = new VoiceTestNotificationService(); + const permissionError = new Error('Permission denied'); + permissionError.name = 'NotAllowedError'; + const micCaptureService = new class extends RecordingMicCaptureService { + override async startCapture(): Promise<void> { + this.startCaptureCalls++; + throw permissionError; + } + }(); + const controller = createController( + voiceClientService, + undefined, + undefined, + undefined, + micCaptureService, + new TestConfigurationService({ 'agents.voice.handsFree': true }), + undefined, + undefined, + undefined, + notificationService, + ); + await controller.connect(mainWindow); + voiceClientService.fireConnectionState(true); + await clock.tickAsync(0); + + assert.deepStrictEqual({ + startCaptureCalls: micCaptureService.startCaptureCalls, + notifications: notificationService.notifications.map(notification => notification.message), + sessionCommands: voiceClientService.sessionCommands, + connecting: controller.isConnecting.get(), + connected: controller.isConnected.get(), + status: controller.statusText.get(), + }, { + startCaptureCalls: 1, + notifications: [], + sessionCommands: [], + connecting: false, + connected: false, + status: 'Tap to start', + }); + }); + + test('connect watchdog covers a stalled hands-free warm-up', async () => { + const voiceClientService = new TestVoiceClientService(); + const captureBarrier = new DeferredPromise<void>(); + const micCaptureService = new RecordingMicCaptureService(captureBarrier.p); + const controller = createController( + voiceClientService, + undefined, + undefined, + undefined, + micCaptureService, + new TestConfigurationService({ 'agents.voice.handsFree': true }), + ); + await controller.connect(mainWindow); + + voiceClientService.fireConnectionState(true); + await micCaptureService.captureStarted.p; + clock.tick(10_000); + captureBarrier.complete(); + await Promise.resolve(); + + assert.deepStrictEqual({ + stopCaptureCalls: micCaptureService.stopCaptureCalls, + sessionCommands: voiceClientService.sessionCommands, + connecting: controller.isConnecting.get(), + connected: controller.isConnected.get(), + status: controller.statusText.get(), + }, { + stopCaptureCalls: 1, + sessionCommands: [], + connecting: false, + connected: false, + status: 'Tap to start', + }); + }); + + test('clean socket close during acquisition aborts initialization and allows explicit retry', async () => { + const voiceClientService = new TestVoiceClientService(); + const firstCaptureBarrier = new DeferredPromise<void>(); + const micCaptureService = new RecordingMicCaptureService(firstCaptureBarrier.p); + const controller = createController( + voiceClientService, + undefined, + undefined, + undefined, + micCaptureService, + new TestConfigurationService({ 'agents.voice.handsFree': true }), + ); + await controller.connect(mainWindow); + + voiceClientService.fireConnectionState(true); + await micCaptureService.captureStarted.p; + voiceClientService.fireConnectionState(false); + firstCaptureBarrier.complete(); + await Promise.resolve(); + + const afterDrop = { + startCaptureCalls: micCaptureService.startCaptureCalls, + stopCaptureCalls: micCaptureService.stopCaptureCalls, + sessionCommands: [...voiceClientService.sessionCommands], + connected: controller.isConnected.get(), + status: controller.statusText.get(), + }; + + await controller.connect(mainWindow); + voiceClientService.fireConnectionState(true); + await voiceClientService.sessionCommandSent.p; + + assert.deepStrictEqual({ + afterDrop, + afterRetry: { + startCaptureCalls: micCaptureService.startCaptureCalls, + stopCaptureCalls: micCaptureService.stopCaptureCalls, + sessionCommands: voiceClientService.sessionCommands, + connected: controller.isConnected.get(), + }, + }, { + afterDrop: { + startCaptureCalls: 1, + stopCaptureCalls: 1, + sessionCommands: [], + connected: false, + status: 'Tap to start', + }, + afterRetry: { + startCaptureCalls: 2, + stopCaptureCalls: 1, + sessionCommands: ['start'], + connected: true, + }, + }); + }); + + test('transient socket drop during acquisition retries warm-up before starting the session', async () => { + const voiceClientService = new TestVoiceClientService(); + const firstCaptureBarrier = new DeferredPromise<void>(); + const micCaptureService = new RecordingMicCaptureService(firstCaptureBarrier.p); + const controller = createController( + voiceClientService, + undefined, + undefined, + undefined, + micCaptureService, + new TestConfigurationService({ 'agents.voice.handsFree': true }), + ); + await controller.connect(mainWindow); + + voiceClientService.fireConnectionState(true); + await micCaptureService.captureStarted.p; + voiceClientService.fireConnectionState(false, true); + firstCaptureBarrier.complete(); + await Promise.resolve(); + const afterDrop = { + connecting: controller.isConnecting.get(), + reconnecting: controller.isReconnecting.get(), + stopCaptureCalls: micCaptureService.stopCaptureCalls, + sessionCommands: [...voiceClientService.sessionCommands], + status: controller.statusText.get(), + }; + + voiceClientService.fireConnectionState(true); + await voiceClientService.sessionCommandSent.p; + + assert.deepStrictEqual({ + afterDrop, + afterRetry: { + startCaptureCalls: micCaptureService.startCaptureCalls, + sessionCommands: voiceClientService.sessionCommands, + connected: controller.isConnected.get(), + }, + }, { + afterDrop: { + connecting: false, + reconnecting: true, + stopCaptureCalls: 1, + sessionCommands: [], + status: 'Reconnecting...', + }, + afterRetry: { + startCaptureCalls: 2, + sessionCommands: ['start'], + connected: true, + }, + }); + }); + test('narrates visible questionnaire prompts and choices immediately without internal ids', () => { const voiceClientService = new TestVoiceClientService(); const controller = createController(voiceClientService); @@ -3347,6 +3734,57 @@ suite('VoiceSessionController', () => { }); }); + test('speech-started alone interrupts playback and accepts the scoped passive turn', async () => { + const voiceClientService = new TestVoiceClientService(); + const ttsPlaybackService = new TestTtsPlaybackService(); + const controller = createController(voiceClientService, ttsPlaybackService); + await controller.connect(mainWindow); + + voiceClientService.fireAudioResponse({ + audio: 'story-start', + isFirstChunk: true, + isFinal: false, + turnId: 'story-turn', + responseId: 'story-response', + }); + voiceClientService.fireSpeechStarted('follow-up-turn'); + voiceClientService.fireTranscription({ + text: 'check the repository instead', + status: 'final', + turnId: 'follow-up-turn', + revision: 1, + }); + voiceClientService.fireAudioResponse({ + audio: 'stale-story', + isFirstChunk: false, + isFinal: true, + turnId: 'story-turn', + responseId: 'story-response', + }); + voiceClientService.fireAudioResponse({ + audio: 'follow-up', + isFirstChunk: true, + isFinal: false, + turnId: 'follow-up-turn', + responseId: 'follow-up-response', + }); + + assert.deepStrictEqual({ + playedAudio: ttsPlaybackService.playedAudio, + stopCount: ttsPlaybackService.stopCount, + transcript: controller.transcriptTurns.get().at(-1), + }, { + playedAudio: ['story-start', 'follow-up'], + stopCount: 1, + transcript: { + speaker: 'user', + text: 'check the repository instead', + committed: '', + isPartial: false, + }, + }); + }); + test('stale interrupted audio does not consume follow-up latency telemetry', async () => { const voiceClientService = new TestVoiceClientService(); const telemetryService = new TestTelemetryService(); From 4eacb4740c0c3b34f522a77305402608bb506fe9 Mon Sep 17 00:00:00 2001 From: Simon Siefke <simon.siefke@gmail.com> Date: Fri, 31 Jul 2026 10:07:00 -0700 Subject: [PATCH 85/86] fix: memory leak in settings-tree (#327909) fix: dispose settings tree toolbars Co-authored-by: Dmitriy Vasyura <dmitriv@microsoft.com> --- .../preferences/browser/settingsTree.ts | 2 +- .../test/browser/settingsTree.test.ts | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/vs/workbench/contrib/preferences/test/browser/settingsTree.test.ts diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 1760fe70176..99458e8b36d 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -950,7 +950,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); const toolbarContainer = DOM.append(container, $('.setting-toolbar-container')); - const toolbar = this.renderSettingToolbar(toolbarContainer); + const toolbar = toDispose.add(this.renderSettingToolbar(toolbarContainer)); const template: ISettingItemTemplate = { toDispose, diff --git a/src/vs/workbench/contrib/preferences/test/browser/settingsTree.test.ts b/src/vs/workbench/contrib/preferences/test/browser/settingsTree.test.ts new file mode 100644 index 00000000000..5f686ae95ad --- /dev/null +++ b/src/vs/workbench/contrib/preferences/test/browser/settingsTree.test.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ITreeNode } from '../../../../../base/browser/ui/tree/tree.js'; +import { ToolBar } from '../../../../../base/browser/ui/toolbar/toolbar.js'; +import { IAction } from '../../../../../base/common/actions.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ISetting } from '../../../../services/preferences/common/preferences.js'; +import { SettingsTarget } from '../../browser/preferencesWidgets.js'; +import { AbstractSettingRenderer } from '../../browser/settingsTree.js'; +import { SettingsTreeSettingElement } from '../../browser/settingsTreeModels.js'; + +class TestSettingRenderer extends AbstractSettingRenderer { + readonly templateId = 'test'; + toolbarDisposed = false; + + constructor() { + super( + [], + (_setting: ISetting, _settingTarget: SettingsTarget): IAction[] => [], + undefined!, + undefined!, + undefined!, + { createInstance: () => ({ dispose() { } }) } as never, + undefined!, + undefined!, + undefined!, + new TestConfigurationService(), + undefined!, + undefined!, + undefined!, + undefined!, + { setupDelayedHover: () => Disposable.None } as never, + undefined!, + ); + } + + renderTemplate(container: HTMLElement) { + return this.renderCommonTemplate(undefined, container, 'test'); + } + + renderElement(_element: ITreeNode<SettingsTreeSettingElement, never>, _index: number, _templateData: unknown): void { + } + + protected override renderSettingToolbar(_container: HTMLElement): ToolBar { + return { + dispose: () => this.toolbarDisposed = true + } as unknown as ToolBar; + } + + protected renderValue(): void { + } +} + +suite('SettingsTree renderer', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('disposes the setting toolbar with its template', () => { + const renderer = new TestSettingRenderer(); + const template = renderer.renderTemplate(document.createElement('div')); + + assert.strictEqual(renderer.toolbarDisposed, false); + renderer.disposeTemplate(template); + assert.strictEqual(renderer.toolbarDisposed, true); + + renderer.dispose(); + }); +}); From add330dbd5704e5e27a4aa1bba7f41549ca7e671 Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:07:31 +0000 Subject: [PATCH 86/86] chore: bump @github/copilot-sdk to 1.0.9-preview.1 and @github/copilot to 1.0.77 (#328392) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- package-lock.json | 80 ++++++++++++++++++++-------------------- package.json | 4 +- remote/package-lock.json | 80 ++++++++++++++++++++-------------------- remote/package.json | 4 +- 4 files changed, 84 insertions(+), 84 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20eff3d3fa0..7c4b3c2f112 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.82.0", - "@github/copilot": "^1.0.76", - "@github/copilot-sdk": "^1.0.9-preview.0", + "@github/copilot": "^1.0.77", + "@github/copilot-sdk": "^1.0.9-preview.1", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/dev-tunnels-connections": "^1.3.41", @@ -1113,9 +1113,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.76.tgz", - "integrity": "sha512-5aP3y9lTTGEx0JeaCnNLlHU0Y+pgq/FS74R6Q6VniOBya8jqv2hulraWNN1sOhGD7CyLcOeOPSIrNTANcQiM5A==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.77.tgz", + "integrity": "sha512-nkTtDPKvsClAByPPqnD/57vK7YIBK1dgiv7aVc9uO3rxKCyqiqYaBqwi8pMzesvGP3yl+//+iMzaBXNWEcZVWQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -1124,20 +1124,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.76", - "@github/copilot-darwin-x64": "1.0.76", - "@github/copilot-linux-arm64": "1.0.76", - "@github/copilot-linux-x64": "1.0.76", - "@github/copilot-linuxmusl-arm64": "1.0.76", - "@github/copilot-linuxmusl-x64": "1.0.76", - "@github/copilot-win32-arm64": "1.0.76", - "@github/copilot-win32-x64": "1.0.76" + "@github/copilot-darwin-arm64": "1.0.77", + "@github/copilot-darwin-x64": "1.0.77", + "@github/copilot-linux-arm64": "1.0.77", + "@github/copilot-linux-x64": "1.0.77", + "@github/copilot-linuxmusl-arm64": "1.0.77", + "@github/copilot-linuxmusl-x64": "1.0.77", + "@github/copilot-win32-arm64": "1.0.77", + "@github/copilot-win32-x64": "1.0.77" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.76.tgz", - "integrity": "sha512-A0Izj4xZRm4syCaHXcAdHXF1IDuwLGCQiDdriGhennvGbGck5Ku+cDbLEgoBGb6Eqk2VcToV0Aik5YQrAlfRlw==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.77.tgz", + "integrity": "sha512-sCWSH5+Flm/OxFe7dzsBfyj7ADBkzkR54Sz5NGw7dtcVVEOnVUkZLjEtNmZ1t5QRD4Sf1+g/DiwgJEbsR9xR1w==", "cpu": [ "arm64" ], @@ -1151,9 +1151,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.76.tgz", - "integrity": "sha512-F/I+F6oLBvKoSjxgRLytjxyRk/e+Zi01dsE9KT95qg29ntdAM2MGplrpmmk08eQly7IyfJT2eZcfceOHZFPvUQ==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.77.tgz", + "integrity": "sha512-ReNlB+g+OBiqHwmY5leJBIyvHZQcjyWL/OY8aVimHyESn2ToPKP3eUNTzSUJvvbPM6+0LXwEpijLedkRd2Cn1g==", "cpu": [ "x64" ], @@ -1167,9 +1167,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.76.tgz", - "integrity": "sha512-gI0ZdgIcL5bMj3yM25GIlfw1pIIZAYwMux/gSb406OlAOcBlkssLgcioltZV1ifHe/344FsO+BhZ5zAS6tEn7g==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.77.tgz", + "integrity": "sha512-A8j/WBPFvV5WfLbgnIIQLUVuFRAR7kLyc5WgId6XLCu1ARbkRM7353zz9mEXXwjc6LqotHVg80ooANJjNtmSPg==", "cpu": [ "arm64" ], @@ -1186,9 +1186,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.76.tgz", - "integrity": "sha512-mZXoaiOW6SZD++YEonprGLsesxRFiUQme1K17Q7x7jycD4FMgexGFPOzF6KwfxnPBIPwQu/RXvz1VUPYY3n3DQ==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.77.tgz", + "integrity": "sha512-2eefKkdUnQ1Y8oxyRyexHBXVpuSmrfEM8XJauquVjPc0JqF5nab9axwpFPzrRSF1GB+25F9tUK2sDQRyp08wag==", "cpu": [ "x64" ], @@ -1205,9 +1205,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.76.tgz", - "integrity": "sha512-6r9IsqQZfWvGOl5viz0nXLjHw4WMpITkkOv0odaIhENTWk28yVI/nXGLdI/FXKA0MLhcW2odNVc2yL3Z3igDWA==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.77.tgz", + "integrity": "sha512-YtltOZQp8plytSKSGTWWKbOx3QD8iZH04sLtKTrYs6nu5UalIgFPoMkwamy1gh7h5EBkeVxD2s3epVrlvP4X4w==", "cpu": [ "arm64" ], @@ -1224,9 +1224,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.76.tgz", - "integrity": "sha512-YHpphnuSRu/T0fYoFVIu6AeutmMWPiOqDo9Fk4WKtPOT4Sn69SZbI1LLlbnk0JzU4QhjpH0CWQyuHCc6yRDgAg==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.77.tgz", + "integrity": "sha512-owINwPgHU/ZZBwFhVPgkgGjLkF6e4QbdofADvKMdKJWV2+7oWjXUIlPA4/PwraD2Gkuu583l7m0XLL27TN8oUA==", "cpu": [ "x64" ], @@ -1243,9 +1243,9 @@ } }, "node_modules/@github/copilot-sdk": { - "version": "1.0.9-preview.0", - "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.9-preview.0.tgz", - "integrity": "sha512-0k8GHW0ix1e5MtoHp797f75Xxea4WLp1LE3cB1J6vcEXf5fl57qgGICZEw6w5boYahbthYTW48+P85ILd7upTQ==", + "version": "1.0.9-preview.1", + "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.9-preview.1.tgz", + "integrity": "sha512-/hUYdxpa4HL57uKmRCLmcg31wWjZPKGBbBfIhVqJdWXkp4/E6lbtHoiIbT5SLCPpf4c8Ka5zw356z+k4Nn/diA==", "license": "MIT", "dependencies": { "@github/copilot": "^1.0.76-5", @@ -1267,9 +1267,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.76.tgz", - "integrity": "sha512-c4FJP/7TV3qiGeSXFVC3dtNIC2D2awq6XSC1FTYkyBWVZSG8ZByWfweltUlB//iyzvHmVoHeUfu6r8E6utp1sQ==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.77.tgz", + "integrity": "sha512-l5oQaMLCRup0nmmpbqOAYEAJ5YWgNlaoO0psNaKDzvTbdzEJRZqib2t7+p3bgoDpK7SB/m8m1uxFC4XT3hlprg==", "cpu": [ "arm64" ], @@ -1283,9 +1283,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.76.tgz", - "integrity": "sha512-twVo1UnnIYx77NF9E7qYKWRuo6IX0UOEIT+8ZIF4FO/9uEoPRDUX+C5MLkHFufDROr/bW/dhVRbZocJ1Rwy7Ew==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.77.tgz", + "integrity": "sha512-8Mo9y3/8CVU2w35WqwSiRMTGH1kKHR3URPSJYF4J4OG8L7NOEy2fafXR9Tuq3H21Srg3OzFkl/A+Taunqz9KcA==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 4616d7a71da..b2b925627ad 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,8 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.82.0", - "@github/copilot": "^1.0.76", - "@github/copilot-sdk": "^1.0.9-preview.0", + "@github/copilot": "^1.0.77", + "@github/copilot-sdk": "^1.0.9-preview.1", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/dev-tunnels-connections": "^1.3.41", diff --git a/remote/package-lock.json b/remote/package-lock.json index a1997fa4473..0367a56d604 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -8,8 +8,8 @@ "name": "vscode-reh", "version": "0.0.0", "dependencies": { - "@github/copilot": "^1.0.76", - "@github/copilot-sdk": "^1.0.9-preview.0", + "@github/copilot": "^1.0.77", + "@github/copilot-sdk": "^1.0.9-preview.1", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/mxc-sdk": "0.6.1", @@ -61,9 +61,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.76.tgz", - "integrity": "sha512-5aP3y9lTTGEx0JeaCnNLlHU0Y+pgq/FS74R6Q6VniOBya8jqv2hulraWNN1sOhGD7CyLcOeOPSIrNTANcQiM5A==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.77.tgz", + "integrity": "sha512-nkTtDPKvsClAByPPqnD/57vK7YIBK1dgiv7aVc9uO3rxKCyqiqYaBqwi8pMzesvGP3yl+//+iMzaBXNWEcZVWQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -72,20 +72,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.76", - "@github/copilot-darwin-x64": "1.0.76", - "@github/copilot-linux-arm64": "1.0.76", - "@github/copilot-linux-x64": "1.0.76", - "@github/copilot-linuxmusl-arm64": "1.0.76", - "@github/copilot-linuxmusl-x64": "1.0.76", - "@github/copilot-win32-arm64": "1.0.76", - "@github/copilot-win32-x64": "1.0.76" + "@github/copilot-darwin-arm64": "1.0.77", + "@github/copilot-darwin-x64": "1.0.77", + "@github/copilot-linux-arm64": "1.0.77", + "@github/copilot-linux-x64": "1.0.77", + "@github/copilot-linuxmusl-arm64": "1.0.77", + "@github/copilot-linuxmusl-x64": "1.0.77", + "@github/copilot-win32-arm64": "1.0.77", + "@github/copilot-win32-x64": "1.0.77" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.76.tgz", - "integrity": "sha512-A0Izj4xZRm4syCaHXcAdHXF1IDuwLGCQiDdriGhennvGbGck5Ku+cDbLEgoBGb6Eqk2VcToV0Aik5YQrAlfRlw==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.77.tgz", + "integrity": "sha512-sCWSH5+Flm/OxFe7dzsBfyj7ADBkzkR54Sz5NGw7dtcVVEOnVUkZLjEtNmZ1t5QRD4Sf1+g/DiwgJEbsR9xR1w==", "cpu": [ "arm64" ], @@ -99,9 +99,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.76.tgz", - "integrity": "sha512-F/I+F6oLBvKoSjxgRLytjxyRk/e+Zi01dsE9KT95qg29ntdAM2MGplrpmmk08eQly7IyfJT2eZcfceOHZFPvUQ==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.77.tgz", + "integrity": "sha512-ReNlB+g+OBiqHwmY5leJBIyvHZQcjyWL/OY8aVimHyESn2ToPKP3eUNTzSUJvvbPM6+0LXwEpijLedkRd2Cn1g==", "cpu": [ "x64" ], @@ -115,9 +115,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.76.tgz", - "integrity": "sha512-gI0ZdgIcL5bMj3yM25GIlfw1pIIZAYwMux/gSb406OlAOcBlkssLgcioltZV1ifHe/344FsO+BhZ5zAS6tEn7g==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.77.tgz", + "integrity": "sha512-A8j/WBPFvV5WfLbgnIIQLUVuFRAR7kLyc5WgId6XLCu1ARbkRM7353zz9mEXXwjc6LqotHVg80ooANJjNtmSPg==", "cpu": [ "arm64" ], @@ -134,9 +134,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.76.tgz", - "integrity": "sha512-mZXoaiOW6SZD++YEonprGLsesxRFiUQme1K17Q7x7jycD4FMgexGFPOzF6KwfxnPBIPwQu/RXvz1VUPYY3n3DQ==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.77.tgz", + "integrity": "sha512-2eefKkdUnQ1Y8oxyRyexHBXVpuSmrfEM8XJauquVjPc0JqF5nab9axwpFPzrRSF1GB+25F9tUK2sDQRyp08wag==", "cpu": [ "x64" ], @@ -153,9 +153,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.76.tgz", - "integrity": "sha512-6r9IsqQZfWvGOl5viz0nXLjHw4WMpITkkOv0odaIhENTWk28yVI/nXGLdI/FXKA0MLhcW2odNVc2yL3Z3igDWA==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.77.tgz", + "integrity": "sha512-YtltOZQp8plytSKSGTWWKbOx3QD8iZH04sLtKTrYs6nu5UalIgFPoMkwamy1gh7h5EBkeVxD2s3epVrlvP4X4w==", "cpu": [ "arm64" ], @@ -172,9 +172,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.76.tgz", - "integrity": "sha512-YHpphnuSRu/T0fYoFVIu6AeutmMWPiOqDo9Fk4WKtPOT4Sn69SZbI1LLlbnk0JzU4QhjpH0CWQyuHCc6yRDgAg==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.77.tgz", + "integrity": "sha512-owINwPgHU/ZZBwFhVPgkgGjLkF6e4QbdofADvKMdKJWV2+7oWjXUIlPA4/PwraD2Gkuu583l7m0XLL27TN8oUA==", "cpu": [ "x64" ], @@ -191,9 +191,9 @@ } }, "node_modules/@github/copilot-sdk": { - "version": "1.0.9-preview.0", - "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.9-preview.0.tgz", - "integrity": "sha512-0k8GHW0ix1e5MtoHp797f75Xxea4WLp1LE3cB1J6vcEXf5fl57qgGICZEw6w5boYahbthYTW48+P85ILd7upTQ==", + "version": "1.0.9-preview.1", + "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.9-preview.1.tgz", + "integrity": "sha512-/hUYdxpa4HL57uKmRCLmcg31wWjZPKGBbBfIhVqJdWXkp4/E6lbtHoiIbT5SLCPpf4c8Ka5zw356z+k4Nn/diA==", "license": "MIT", "dependencies": { "@github/copilot": "^1.0.76-5", @@ -215,9 +215,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.76.tgz", - "integrity": "sha512-c4FJP/7TV3qiGeSXFVC3dtNIC2D2awq6XSC1FTYkyBWVZSG8ZByWfweltUlB//iyzvHmVoHeUfu6r8E6utp1sQ==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.77.tgz", + "integrity": "sha512-l5oQaMLCRup0nmmpbqOAYEAJ5YWgNlaoO0psNaKDzvTbdzEJRZqib2t7+p3bgoDpK7SB/m8m1uxFC4XT3hlprg==", "cpu": [ "arm64" ], @@ -231,9 +231,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.76", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.76.tgz", - "integrity": "sha512-twVo1UnnIYx77NF9E7qYKWRuo6IX0UOEIT+8ZIF4FO/9uEoPRDUX+C5MLkHFufDROr/bW/dhVRbZocJ1Rwy7Ew==", + "version": "1.0.77", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.77.tgz", + "integrity": "sha512-8Mo9y3/8CVU2w35WqwSiRMTGH1kKHR3URPSJYF4J4OG8L7NOEy2fafXR9Tuq3H21Srg3OzFkl/A+Taunqz9KcA==", "cpu": [ "x64" ], diff --git a/remote/package.json b/remote/package.json index 8d0bbc6280c..3f1ea10856e 100644 --- a/remote/package.json +++ b/remote/package.json @@ -3,8 +3,8 @@ "version": "0.0.0", "private": true, "dependencies": { - "@github/copilot": "^1.0.76", - "@github/copilot-sdk": "^1.0.9-preview.0", + "@github/copilot": "^1.0.77", + "@github/copilot-sdk": "^1.0.9-preview.1", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/mxc-sdk": "0.6.1",