From ed26f06c82bf366656d6a2e9b9fa6b0f8ae8bf55 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:55:48 -0800 Subject: [PATCH 01/21] Mark chat math as finalized Forgot to remove the preview tag when defaulting this to on --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9f80ffb1cb0..fb1ced9c056 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -562,7 +562,6 @@ configurationRegistry.registerConfiguration({ type: 'boolean', description: nls.localize('chat.mathEnabled.description', "Enable math rendering in chat responses using KaTeX."), default: true, - tags: ['preview'], }, [ChatConfiguration.ShowCodeBlockProgressAnimation]: { type: 'boolean', From 3e7b2cb5e66fdc5ba0343c3a406ec618c520e650 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:47:31 -0800 Subject: [PATCH 02/21] chore: bump native-is-elevated (#287912) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6bbc06b5856..3932e294a5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", - "native-is-elevated": "0.8.0", + "native-is-elevated": "0.9.0", "native-keymap": "^3.3.5", "node-pty": "^1.2.0-beta.7", "open": "^10.1.2", @@ -12828,9 +12828,9 @@ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, "node_modules/native-is-elevated": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/native-is-elevated/-/native-is-elevated-0.8.0.tgz", - "integrity": "sha512-utx846s63JTqN2DcsLSAd0YpwOMcBezBzN55gSyVJX2kZAsvqOt6+ypdyogNqjSnzd7NvOCEvzMRq+AB2ekVxQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/native-is-elevated/-/native-is-elevated-0.9.0.tgz", + "integrity": "sha512-DTPZixwPTtWHU9g46hXMyK6eW77e8L69uZNkB/cjY00BF8NCXQ5cyJEDim91WmxHruJoIQzLdZBb4TEjrI+PQw==", "hasInstallScript": true, "license": "MIT" }, diff --git a/package.json b/package.json index 4b401a1b37a..5ab7a100a18 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", - "native-is-elevated": "0.8.0", + "native-is-elevated": "0.9.0", "native-keymap": "^3.3.5", "node-pty": "^1.2.0-beta.7", "open": "^10.1.2", From 868d1ca73d3fbeab29a7b4dc5074456a16290173 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 21 Jan 2026 15:53:52 -0500 Subject: [PATCH 03/21] Incorporate thinking in chat response accessible view, remove thinking specific view (#289460) fixes #289223 --- .../chatResponseAccessibleView.ts | 157 +++++++++++------- .../chatThinkingAccessibleView.ts | 72 -------- .../actions/chatAccessibilityActions.ts | 42 +---- .../browser/actions/chatAccessibilityHelp.ts | 4 +- .../contrib/chat/browser/chat.contribution.ts | 2 - 5 files changed, 95 insertions(+), 182 deletions(-) delete mode 100644 src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index 3de7e243cb1..c4866fed119 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -5,7 +5,7 @@ import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { IMarkdownString, isMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js'; import { stripIcons } from '../../../../../base/common/iconLabels.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -214,79 +214,108 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi } private _getContent(item: ChatTreeItem): string { - let responseContent = isResponseVM(item) ? item.response.toString() : ''; - if (!responseContent && 'errorDetails' in item && item.errorDetails) { - responseContent = item.errorDetails.message; + const contentParts: string[] = []; + + if (!isResponseVM(item)) { + return ''; } - if (isResponseVM(item)) { - item.response.value.filter(item => item.kind === 'elicitation2' || item.kind === 'elicitationSerialized').forEach(elicitation => { - const title = elicitation.title; - if (typeof title === 'string') { - responseContent += `${title}\n`; - } else if (isMarkdownString(title)) { - responseContent += renderAsPlaintext(title, { includeCodeBlocksFences: true }) + '\n'; - } - const message = elicitation.message; - if (isMarkdownString(message)) { - responseContent += renderAsPlaintext(message, { includeCodeBlocksFences: true }); - } else { - responseContent += message; - } - }); - const toolInvocations = item.response.value.filter(item => item.kind === 'toolInvocation'); - for (const toolInvocation of toolInvocations) { - const state = toolInvocation.state.get(); - if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.title) { - const title = this._renderMessageAsPlaintext(state.confirmationMessages.title); - const message = state.confirmationMessages.message ? this._renderMessageAsPlaintext(state.confirmationMessages.message) : ''; - const toolDataDesc = getToolSpecificDataDescription(toolInvocation.toolSpecificData); - responseContent += `${title}`; - if (toolDataDesc) { - responseContent += `: ${toolDataDesc}`; + + if ('errorDetails' in item && item.errorDetails) { + contentParts.push(item.errorDetails.message); + } + + // Process all parts in order to maintain the natural flow + for (const part of item.response.value) { + switch (part.kind) { + case 'thinking': { + const thinkingValue = Array.isArray(part.value) ? part.value.join('') : (part.value || ''); + const trimmed = thinkingValue.trim(); + if (trimmed) { + contentParts.push(localize('thinkingContent', "Thinking: {0}", trimmed)); } - if (message) { - responseContent += `\n${message}`; + break; + } + case 'markdownContent': { + const text = renderAsPlaintext(part.content, { includeCodeBlocksFences: true, useLinkFormatter: true }); + if (text.trim()) { + contentParts.push(text); } - responseContent += '\n'; - } else if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { - const postApprovalDetails = isToolResultInputOutputDetails(state.resultDetails) - ? state.resultDetails.input - : isToolResultOutputDetails(state.resultDetails) - ? undefined - : toolContentToA11yString(state.contentForModel); - responseContent += localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", toolInvocation.toolId) + (postApprovalDetails ?? '') + '\n'; - } else { - const resultDetails = IChatToolInvocation.resultDetails(toolInvocation); - const isComplete = IChatToolInvocation.isComplete(toolInvocation); + break; + } + case 'elicitation2': + case 'elicitationSerialized': { + const title = part.title; + let elicitationContent = ''; + if (typeof title === 'string') { + elicitationContent += `${title}\n`; + } else if (isMarkdownString(title)) { + elicitationContent += renderAsPlaintext(title, { includeCodeBlocksFences: true }) + '\n'; + } + const message = part.message; + if (isMarkdownString(message)) { + elicitationContent += renderAsPlaintext(message, { includeCodeBlocksFences: true }); + } else { + elicitationContent += message; + } + if (elicitationContent.trim()) { + contentParts.push(elicitationContent); + } + break; + } + case 'toolInvocation': { + const state = part.state.get(); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.title) { + const title = this._renderMessageAsPlaintext(state.confirmationMessages.title); + const message = state.confirmationMessages.message ? this._renderMessageAsPlaintext(state.confirmationMessages.message) : ''; + const toolDataDesc = getToolSpecificDataDescription(part.toolSpecificData); + let toolContent = title; + if (toolDataDesc) { + toolContent += `: ${toolDataDesc}`; + } + if (message) { + toolContent += `\n${message}`; + } + contentParts.push(toolContent); + } else if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + const postApprovalDetails = isToolResultInputOutputDetails(state.resultDetails) + ? state.resultDetails.input + : isToolResultOutputDetails(state.resultDetails) + ? undefined + : toolContentToA11yString(state.contentForModel); + contentParts.push(localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", part.toolId) + (postApprovalDetails ?? '')); + } else { + const resultDetails = IChatToolInvocation.resultDetails(part); + const isComplete = IChatToolInvocation.isComplete(part); + const description = getToolInvocationA11yDescription( + this._renderMessageAsPlaintext(part.invocationMessage), + part.pastTenseMessage ? this._renderMessageAsPlaintext(part.pastTenseMessage) : undefined, + part.toolSpecificData, + resultDetails, + isComplete + ); + if (description) { + contentParts.push(description); + } + } + break; + } + case 'toolInvocationSerialized': { const description = getToolInvocationA11yDescription( - this._renderMessageAsPlaintext(toolInvocation.invocationMessage), - toolInvocation.pastTenseMessage ? this._renderMessageAsPlaintext(toolInvocation.pastTenseMessage) : undefined, - toolInvocation.toolSpecificData, - resultDetails, - isComplete + this._renderMessageAsPlaintext(part.invocationMessage), + part.pastTenseMessage ? this._renderMessageAsPlaintext(part.pastTenseMessage) : undefined, + part.toolSpecificData, + part.resultDetails, + part.isComplete ); if (description) { - responseContent += '\n' + description + '\n'; + contentParts.push(description); } - } - } - - const pastConfirmations = item.response.value.filter(item => item.kind === 'toolInvocationSerialized'); - for (const pastConfirmation of pastConfirmations) { - const description = getToolInvocationA11yDescription( - this._renderMessageAsPlaintext(pastConfirmation.invocationMessage), - pastConfirmation.pastTenseMessage ? this._renderMessageAsPlaintext(pastConfirmation.pastTenseMessage) : undefined, - pastConfirmation.toolSpecificData, - pastConfirmation.resultDetails, - pastConfirmation.isComplete - ); - if (description) { - responseContent += '\n' + description + '\n'; + break; } } } - const plainText = renderAsPlaintext(new MarkdownString(responseContent), { includeCodeBlocksFences: true, useLinkFormatter: true }); - return this._normalizeWhitespace(plainText); + + return this._normalizeWhitespace(contentParts.join('\n')); } private _normalizeWhitespace(content: string): string { diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts deleted file mode 100644 index 0c8e067e875..00000000000 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts +++ /dev/null @@ -1,72 +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 { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { IChatWidgetService } from '../chat.js'; -import { IChatResponseViewModel, isResponseVM } from '../../common/model/chatViewModel.js'; - -export class ChatThinkingAccessibleView implements IAccessibleViewImplementation { - readonly priority = 105; - readonly name = 'chatThinking'; - readonly type = AccessibleViewType.View; - // Never match via the registry - this view is only opened via the explicit command (Alt+Shift+F2) - readonly when = ContextKeyExpr.false(); - - getProvider(accessor: ServicesAccessor) { - const widgetService = accessor.get(IChatWidgetService); - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return; - } - - const viewModel = widget.viewModel; - if (!viewModel) { - return; - } - - // Get the latest response from the chat - const items = viewModel.getItems(); - const latestResponse = [...items].reverse().find(item => isResponseVM(item)); - if (!latestResponse || !isResponseVM(latestResponse)) { - return; - } - - // Extract thinking content from the response - const thinkingContent = this._extractThinkingContent(latestResponse); - if (!thinkingContent) { - return; - } - - return new AccessibleContentProvider( - AccessibleViewProviderId.ChatThinking, - { type: AccessibleViewType.View, id: AccessibleViewProviderId.ChatThinking, language: 'markdown' }, - () => thinkingContent, - () => widget.focusInput(), - AccessibilityVerbositySettingId.Chat - ); - } - - private _extractThinkingContent(response: IChatResponseViewModel): string | undefined { - const thinkingParts: string[] = []; - for (const part of response.response.value) { - if (part.kind === 'thinking') { - const value = Array.isArray(part.value) ? part.value.join('') : (part.value || ''); - const trimmed = value.trim(); - if (trimmed) { - thinkingParts.push(trimmed); - } - } - } - - if (thinkingParts.length === 0) { - return undefined; - } - return thinkingParts.join('\n\n'); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts index 118e7128b4e..f1ec099751c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts @@ -6,19 +6,15 @@ import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { IChatWidgetService } from '../chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js'; -import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { ChatThinkingAccessibleView } from '../accessibility/chatThinkingAccessibleView.js'; -import { CHAT_CATEGORY } from './chatActions.js'; export const ACTION_ID_FOCUS_CHAT_CONFIRMATION = 'workbench.action.chat.focusConfirmation'; -export const ACTION_ID_OPEN_THINKING_ACCESSIBLE_VIEW = 'workbench.action.chat.openThinkingAccessibleView'; class AnnounceChatConfirmationAction extends Action2 { constructor() { @@ -71,42 +67,6 @@ class AnnounceChatConfirmationAction extends Action2 { } } -class OpenThinkingAccessibleViewAction extends Action2 { - constructor() { - super({ - id: ACTION_ID_OPEN_THINKING_ACCESSIBLE_VIEW, - title: { value: localize('openThinkingAccessibleView', 'Open Thinking Accessible View'), original: 'Open Thinking Accessible View' }, - category: CHAT_CATEGORY, - precondition: ChatContextKeys.enabled, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F2, - linux: { - primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F3, - }, - when: ChatContextKeys.inChatSession - } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const accessibleViewService = accessor.get(IAccessibleViewService); - const instantiationService = accessor.get(IInstantiationService); - - const thinkingView = new ChatThinkingAccessibleView(); - const provider = instantiationService.invokeFunction(thinkingView.getProvider.bind(thinkingView)); - - if (!provider) { - alert(localize('noThinking', 'No thinking')); - return; - } - - accessibleViewService.show(provider); - } -} - export function registerChatAccessibilityActions(): void { registerAction2(AnnounceChatConfirmationAction); - registerAction2(OpenThinkingAccessibleViewAction); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 9c42a79f6ef..b82569c057a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -17,7 +17,6 @@ import { TerminalContribCommandId } from '../../../terminal/terminalContribExpor import { ChatContextKeyExprs, ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { FocusAgentSessionsAction } from '../agentSessions/agentSessionsActions.js'; -import { ACTION_ID_OPEN_THINKING_ACCESSIBLE_VIEW } from './chatAccessibilityActions.js'; import { IChatWidgetService } from '../chat.js'; import { ChatEditingShowChangesAction, ViewPreviousEditsAction } from '../chatEditing/chatEditingActions.js'; @@ -75,8 +74,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'age } 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.attachments.removal', 'To remove attached contexts, focus an attachment and press Delete or Backspace.')); - content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}.', '')); - content.push(localize('chat.openThinkingAccessibleView', 'To inspect thinking content from the latest response, invoke the Open Thinking Accessible View command{0}.', ``)); + content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}. Thinking content is included in order.', '')); content.push(localize('workbench.action.chat.focus', 'To focus the chat request and response list, invoke the Focus Chat command{0}. This will move focus to the most recent response, which you can then navigate using the up and down arrow keys.', getChatFocusKeybindingLabel(keybindingService, type, 'last'))); content.push(localize('workbench.action.chat.focusLastFocusedItem', 'To return to the last chat response you focused, invoke the Focus Last Focused Chat Response command{0}.', getChatFocusKeybindingLabel(keybindingService, type, 'lastFocused'))); content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command{0}.', getChatFocusKeybindingLabel(keybindingService, type, 'input'))); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 0b04471cdf3..4eb76e92ddd 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -116,7 +116,6 @@ import { ChatPasteProvidersFeature } from './widget/input/editor/chatPasteProvid import { QuickChatService } from './widgetHosts/chatQuick.js'; import { ChatResponseAccessibleView } from './accessibility/chatResponseAccessibleView.js'; import { ChatTerminalOutputAccessibleView } from './accessibility/chatTerminalOutputAccessibleView.js'; -import { ChatThinkingAccessibleView } from './accessibility/chatThinkingAccessibleView.js'; import { ChatSetupContribution, ChatTeardownContribution } from './chatSetup/chatSetupContributions.js'; import { ChatStatusBarEntry } from './chatStatus/chatStatusEntry.js'; import { ChatVariablesService } from './attachments/chatVariables.js'; @@ -1180,7 +1179,6 @@ class ToolReferenceNamesContribution extends Disposable implements IWorkbenchCon } AccessibleViewRegistry.register(new ChatTerminalOutputAccessibleView()); -AccessibleViewRegistry.register(new ChatThinkingAccessibleView()); AccessibleViewRegistry.register(new ChatResponseAccessibleView()); AccessibleViewRegistry.register(new PanelChatAccessibilityHelp()); AccessibleViewRegistry.register(new QuickChatAccessibilityHelp()); From 8ae978b2234a6ee3650ce51dd5bc40c76aea18d4 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 21 Jan 2026 13:03:16 -0800 Subject: [PATCH 04/21] mcp: implement McpUiUpdateModelContextRequest handler (#289474) Implements the ui/update-model-context MCP Apps protocol method to allow MCP UIs to update the chat widget's model context with structured data and content attachments. Each update replaces only the context added by that specific MCP app instance, preserving user-added attachments. - Maps MCP content blocks to chat attachments: - Images become image attachments - Resource links become file attachments using McpResourceURI - Text blocks become generic attachments with preview and tooltip - Structured content becomes a single JSON attachment formatted nicely - Uses hashed server ID prefix to track and replace only app-specific context - Announces updateModelContext capability in host capabilities Fixes https://github.com/microsoft/vscode/issues/289473 (Commit message generated by Copilot) --- .../attachments/chatAttachmentWidgets.ts | 2 +- .../toolInvocationParts/chatMcpAppModel.ts | 79 ++++++++++++++++++- .../common/attachments/chatVariableEntries.ts | 1 + .../mcp/common/modelContextProtocolApps.ts | 44 +++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 6e388ab0286..7a212fe3b53 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -600,7 +600,7 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { } // Setup tooltip hover for string context attachments - if (isStringVariableEntry(attachment) && attachment.tooltip) { + if ((isStringVariableEntry(attachment) || attachment.kind === 'generic') && attachment.tooltip) { this._setupTooltipHover(attachment.tooltip); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts index 9360828c6ed..e2b59181043 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts @@ -9,6 +9,8 @@ import { disposableTimeout } from '../../../../../../../base/common/async.js'; import { decodeBase64 } from '../../../../../../../base/common/buffer.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../../../base/common/event.js'; +import { hash } from '../../../../../../../base/common/hash.js'; +import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { autorun, autorunSelfDisposable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js'; import { basename } from '../../../../../../../base/common/resources.js'; @@ -396,8 +398,12 @@ export class ChatMcpAppModel extends Disposable { result = await this._handleUiMessage(request.params); break; + case 'ui/update-model-context': + result = await this._handleUpdateModelContext(request.params); + break; + case 'notifications/message': - await this._mcpToolCallUI.log(request.params); + await this._mcpToolCallUI.log(request.params as MCP.LoggingMessageNotification['params']); break; default: { @@ -474,8 +480,15 @@ export class ChatMcpAppModel extends Disposable { logging: {}, sandbox: { csp: this._latestCsp, - permissions: { clipboardWrite: true }, + permissions: { clipboardWrite: {} }, }, + updateModelContext: { + audio: {}, + image: {}, + resourceLink: {}, + resource: {}, + structuredContent: {}, + } }, hostContext: this.hostContext.get(), } satisfies Required; @@ -520,6 +533,68 @@ export class ChatMcpAppModel extends Disposable { return { isError: false }; } + private async _handleUpdateModelContext(params: McpApps.McpUiUpdateModelContextRequest['params']): Promise { + const widget = this._chatWidgetService.getWidgetBySessionResource(this.renderData.sessionResource); + if (!widget) { + return {}; + } + + const idPrefix = `mcpui-context-${hash(this.renderData.serverDefinitionId)}-`; + const toDelete = widget.attachmentModel.getAttachmentIDs(); + const idsToDelete = Array.from(toDelete).filter(id => id.startsWith(idPrefix)); + const entries: IChatRequestVariableEntry[] = []; + let entryIndex = 0; + + if (params.content) { + for (const block of params.content) { + const id = `${idPrefix}${entryIndex++}`; + if (block.type === 'image') { + entries.push({ + kind: 'image', + value: decodeBase64(block.data).buffer, + id, + name: 'Image', + mimeType: block.mimeType, + }); + } else if (block.type === 'resource_link') { + const uri = McpResourceURI.fromServer({ id: this.renderData.serverDefinitionId, label: '' }, block.uri); + entries.push({ + kind: 'file', + value: uri, + id, + name: basename(uri), + }); + } else if (block.type === 'text') { + const preview = block.text.replaceAll(/\s+/g, ' ').trim(); + const truncateTo = 20; + entries.push({ + kind: 'generic', + value: block.text, + id, + tooltip: new MarkdownString().appendCodeblock('plaintext', block.text), + name: preview.length > truncateTo ? preview.slice(0, truncateTo) + '…' : preview, + }); + } + } + } + + if (params.structuredContent && Object.keys(params.structuredContent).length > 0) { + const id = `${idPrefix}structured`; + const value = JSON.stringify(params.structuredContent, null, 2); + entries.push({ + kind: 'generic', + value, + tooltip: new MarkdownString().appendCodeblock('json', value), + id, + name: 'UI Data', + }); + } + + widget.attachmentModel.updateContext(idsToDelete, entries); + + return {}; + } + private _handleSizeChanged(params: McpApps.McpUiSizeChangedNotification['params']): void { if (params.height !== undefined) { this._height = params.height; diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index dec115cb277..33910c91c59 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -41,6 +41,7 @@ interface IBaseChatRequestVariableEntry { export interface IGenericChatRequestVariableEntry extends IBaseChatRequestVariableEntry { kind: 'generic'; + tooltip?: IMarkdownString; } export interface IChatRequestDirectoryEntry extends IBaseChatRequestVariableEntry { diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts index dd2f5f70035..d69b1f8b653 100644 --- a/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts @@ -19,6 +19,7 @@ export namespace McpApps { | MCP.ReadResourceRequest | MCP.PingRequest | (McpUiOpenLinkRequest & MCP.JSONRPCRequest) + | (McpUiUpdateModelContextRequest & MCP.JSONRPCRequest) | (McpUiMessageRequest & MCP.JSONRPCRequest) | (McpUiRequestDisplayModeRequest & MCP.JSONRPCRequest) | (McpApps.McpUiInitializeRequest & MCP.JSONRPCRequest); @@ -427,6 +428,28 @@ export namespace McpApps { params: McpUiHostContext; } + /** + * @description Request to update the agent's context without requiring a follow-up action (Guest UI -> Host). + * + * Unlike `notifications/message` which is for debugging/logging, this request is intended + * to update the Host's model context. Each request overwrites the previous context sent by the Guest UI. + * Unlike messages, context updates do not trigger follow-ups. + * + * The host will typically defer sending the context to the model until the next user message + * (including `ui/message`), and will only send the last update received. + * + * @see {@link app.App.updateModelContext} for the method that sends this request + */ + export interface McpUiUpdateModelContextRequest { + method: "ui/update-model-context"; + params: { + /** @description Context content blocks (text, image, etc.). */ + content?: ContentBlock[]; + /** @description Structured content for machine-readable context data. */ + structuredContent?: Record; + }; + } + /** * @description Request for graceful shutdown of the Guest UI (Host -> Guest UI). * @see {@link app-bridge.AppBridge.teardownResource} for the host method that sends this @@ -447,6 +470,21 @@ export namespace McpApps { [key: string]: unknown; } + export interface McpUiSupportedContentBlockModalities { + /** @description Host supports text content blocks. */ + text?: {}; + /** @description Host supports image content blocks. */ + image?: {}; + /** @description Host supports audio content blocks. */ + audio?: {}; + /** @description Host supports resource content blocks. */ + resource?: {}; + /** @description Host supports resource link content blocks. */ + resourceLink?: {}; + /** @description Host supports structured content. */ + structuredContent?: {}; + } + /** * @description Capabilities supported by the host application. * @see {@link McpUiInitializeResult} for the initialization result that includes these capabilities @@ -475,6 +513,10 @@ export namespace McpApps { /** @description CSP domains approved by the host. */ csp?: McpUiResourceCsp; }; + /** @description Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns. */ + updateModelContext?: McpUiSupportedContentBlockModalities; + /** @description Host supports receiving content messages (ui/message) from the Guest UI. */ + message?: McpUiSupportedContentBlockModalities; } /** @@ -674,4 +716,6 @@ export namespace McpApps { "ui/notifications/initialized"; export const REQUEST_DISPLAY_MODE_METHOD: McpUiRequestDisplayModeRequest["method"] = "ui/request-display-mode"; + export const UPDATE_MODEL_CONTEXT_METHOD: McpUiUpdateModelContextRequest["method"] = + "ui/update-model-context"; } From 461887743ad05a491baa01ab05f29341cfa2d831 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 21 Jan 2026 22:03:51 +0100 Subject: [PATCH 05/21] Fixes bug that long distance widget does not have correct width (#289477) --- src/vs/editor/browser/editorBrowser.ts | 6 +++ src/vs/editor/browser/observableCodeEditor.ts | 47 +++++++++++++++++++ src/vs/editor/browser/view.ts | 4 ++ .../browser/viewParts/viewLines/viewLine.ts | 18 +++++++ .../browser/viewParts/viewLines/viewLines.ts | 8 ++++ .../widget/codeEditor/codeEditorWidget.ts | 7 +++ .../browser/view/inlineEdits/utils/utils.ts | 11 +---- 7 files changed, 92 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 34d897d6ea4..0c018c2a75b 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -1226,6 +1226,12 @@ export interface ICodeEditor extends editorCommon.IEditor { getWidthOfLine(lineNumber: number): number; + /** + * Reset cached line widths. Call this when the editor becomes visible after being hidden. + * @internal + */ + resetLineWidthCaches(): void; + /** * Force an editor render now. */ diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 6c7f5cddaa0..4c415ec53dd 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -497,6 +497,53 @@ export class ObservableCodeEditor extends Disposable { private readonly _onDidHiddenAreasChanged; private readonly _onDidLineHeightChanged; + /** + * Tracks whether getWidthOfLine returned 0, indicating the editor may be hidden. + * When resize happens and this flag is set, we reset cached line widths. + */ + private _sawZeroLineWidth = false; + + /** + * Fires when the editor container resizes. + * This is lazily created only when someone subscribes to it. + * Useful for detecting when a parent element's display changes from 'none' to 'block'. + */ + private readonly _onDidContainerResize = observableFromEventOpts( + { owner: this, getTransaction: () => this._currentTransaction }, + e => { + const container = this.editor.getContainerDomNode(); + const resizeObserver = new ResizeObserver(() => { + // If we previously saw a 0 width, the editor was likely hidden. + // Now that it resized (became visible), flush the cached widths. + if (this._sawZeroLineWidth) { + this._sawZeroLineWidth = false; + this.editor.resetLineWidthCaches(); + } + e(undefined); + }); + resizeObserver.observe(container); + return { dispose: () => resizeObserver.disconnect() }; + }, + () => ({}) // Return new object each time to ensure change detection + ); + + /** + * Get the width of a line in pixels. + * Reading the returned value depends on layoutInfo, value, scrollTop, and container resize events. + * The container resize dependency ensures correct values when the editor becomes visible after being hidden. + */ + getWidthOfLine(lineNumber: number, reader: IReader | undefined): number { + this.layoutInfo.read(reader); + this.value.read(reader); + this.scrollTop.read(reader); + const width = this.editor.getWidthOfLine(lineNumber); + this._onDidContainerResize.read(reader); + if (width === 0) { + this._sawZeroLineWidth = true; + } + return width; + } + /** * Get the vertical position (top offset) for the line's bottom w.r.t. to the first line. */ diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 2521a18eec7..ad08f4e10f0 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -688,6 +688,10 @@ export class View extends ViewEventHandler { return width; } + public resetLineWidthCaches(): void { + this._viewLines.resetLineWidthCaches(); + } + public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null { const mouseTarget = this._pointerHandler.getTargetAtClientPoint(clientX, clientY); if (!mouseTarget) { diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index a3763b98430..4aaa9200561 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -323,6 +323,10 @@ export class ViewLine implements IVisibleLine { } return this._renderedViewLine.getColumnOfNodeOffset(spanNode, offset); } + + public resetCachedWidth(): void { + this._renderedViewLine?.resetCachedWidth(); + } } interface IRenderedViewLine { @@ -330,6 +334,7 @@ interface IRenderedViewLine { readonly input: RenderLineInput; getWidth(context: DomReadingContext | null): number; getWidthIsFast(): boolean; + resetCachedWidth(): void; getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null; getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number; } @@ -391,6 +396,10 @@ class FastRenderedViewLine implements IRenderedViewLine { return (this.input.lineContent.length < Constants.MaxMonospaceDistance) || this._cachedWidth !== -1; } + public resetCachedWidth(): void { + this._cachedWidth = -1; + } + public monospaceAssumptionsAreValid(): boolean { if (!this.domNode) { return monospaceAssumptionsAreValid; @@ -528,6 +537,15 @@ class RenderedViewLine implements IRenderedViewLine { return true; } + public resetCachedWidth(): void { + this._cachedWidth = -1; + if (this._pixelOffsetCache !== null) { + for (let column = 0, len = this._pixelOffsetCache.length; column < len; column++) { + this._pixelOffsetCache[column] = -1; + } + } + } + /** * Visible ranges for a model range */ diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index 2b54e9a46a9..cb819821291 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -412,6 +412,14 @@ export class ViewLines extends ViewPart implements IViewLines { return result; } + public resetLineWidthCaches(): void { + const rendStartLineNumber = this._visibleLines.getStartLineNumber(); + const rendEndLineNumber = this._visibleLines.getEndLineNumber(); + for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) { + this._visibleLines.getVisibleLine(lineNumber).resetCachedWidth(); + } + } + public linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null { const originalEndLineNumber = _range.endLineNumber; const range = Range.intersectRanges(_range, this._lastRenderedData.getCurrentVisibleRange()); diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 9a4ae242fce..10dd3e6db9f 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -1694,6 +1694,13 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._modelData.view.getLineWidth(lineNumber); } + public resetLineWidthCaches(): void { + if (!this._modelData || !this._modelData.hasRealView) { + return; + } + this._modelData.view.resetLineWidthCaches(); + } + public render(forceRedraw: boolean = false): void { if (!this._modelData || !this._modelData.hasRealView) { return; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts index 401a8f2dc25..580d401fa7a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts @@ -34,16 +34,12 @@ import { Size2D } from '../../../../../../common/core/2d/size.js'; * Warning: might return 0. */ export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number { - editor.layoutInfo.read(reader); - editor.value.read(reader); - const model = editor.model.read(reader); if (!model) { return 0; } let maxContentWidth = 0; - editor.scrollTop.read(reader); for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { - const lineContentWidth = editor.editor.getWidthOfLine(i); + const lineContentWidth = editor.getWidthOfLine(i, reader); maxContentWidth = Math.max(maxContentWidth, lineContentWidth); } const lines = range.mapToLineArray(l => model.getLineContent(l)); @@ -55,8 +51,6 @@ export function maxContentWidthInRange(editor: ObservableCodeEditor, range: Line } export function getContentSizeOfLines(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): Size2D[] { - editor.layoutInfo.read(reader); - editor.value.read(reader); observableSignalFromEvent(editor, editor.editor.onDidChangeLineHeight).read(reader); const model = editor.model.read(reader); @@ -64,9 +58,8 @@ export function getContentSizeOfLines(editor: ObservableCodeEditor, range: LineR const sizes: Size2D[] = []; - editor.scrollTop.read(reader); for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { - let lineContentWidth = editor.editor.getWidthOfLine(i); + let lineContentWidth = editor.getWidthOfLine(i, reader); if (lineContentWidth === -1) { // approximation const column = model.getLineMaxColumn(i); From 3d7f0710fed7a93c4caf89076e33840706d876bb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:04:51 +0000 Subject: [PATCH 06/21] Merge pull request #288741 from microsoft/copilot/fix-agent-projection-mode Fix agent projection mode behavior during chat sessions --- .../agentSessionProjectionService.ts | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts index 2755033c631..071059491be 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts @@ -14,7 +14,7 @@ import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IEditorGroupsService, IEditorWorkingSet } from '../../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { IAgentSession } from '../agentSessionsModel.js'; +import { IAgentSession, isSessionInProgressStatus } from '../agentSessionsModel.js'; import { ChatViewPaneTarget, IChatWidgetService } from '../../chat.js'; import { AgentSessionProviders } from '../agentSessions.js'; import { IChatSessionsService } from '../../../common/chatSessionsService.js'; @@ -27,6 +27,7 @@ import { inAgentSessionProjection } from './agentSessionProjection.js'; import { ChatConfiguration } from '../../../common/constants.js'; import { ISessionOpenOptions, sessionOpenerRegistry } from '../agentSessionsOpener.js'; import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { IAgentSessionsService } from '../agentSessionsService.js'; //#region Configuration @@ -70,8 +71,9 @@ export interface IAgentSessionProjectionService { /** * Exit projection mode. + * @param options.startNewChat If true (default), starts a new chat after exiting. Set to false to keep the current chat open. */ - exitProjection(): Promise; + exitProjection(options?: { startNewChat?: boolean }): Promise; } export const IAgentSessionProjectionService = createDecorator('agentSessionProjectionService'); @@ -116,6 +118,7 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS @ICommandService private readonly commandService: ICommandService, @IChatEditingService private readonly chatEditingService: IChatEditingService, @IAgentTitleBarStatusService private readonly agentTitleBarStatusService: IAgentTitleBarStatusService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, ) { super(); @@ -123,6 +126,11 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS // Listen for editor close events to exit projection mode when all editors are closed this._register(this.editorService.onDidCloseEditor(() => this._checkForEmptyEditors())); + + // Listen for session changes to exit projection mode if active session becomes in progress + // Note: onDidChangeSessions fires for any session change, but _checkForInProgressSession() + // has early exit guards and only checks when projection mode is active, making this efficient + this._register(this.agentSessionsService.model.onDidChangeSessions(() => this._checkForInProgressSession())); } private _isEnabled(): boolean { @@ -144,6 +152,37 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS } } + private _checkForInProgressSession(): void { + // Only check if we're in projection mode + if (!this._isActive || !this._activeSession) { + return; + } + + // Get the updated session from the model + const updatedSession = this.agentSessionsService.getSession(this._activeSession.resource); + if (!updatedSession) { + return; + } + + // If the session is now in progress, exit projection mode + if (isSessionInProgressStatus(updatedSession.status)) { + this.logService.trace('[AgentSessionProjection] Active session transitioned to in-progress, exiting projection mode'); + this.exitProjection({ startNewChat: false }); + } + } + + /** + * Opens a session in the chat panel without entering projection mode. + */ + private async _openSessionInChatPanel(session: IAgentSession): Promise { + session.setRead(true); + await this.chatSessionsService.activateChatSessionItemProvider(session.providerType); + await this.chatWidgetService.openSession(session.resource, ChatViewPaneTarget, { + title: { preferred: session.label }, + revealIfOpened: true + }); + } + /** * Open the session's files in a multi-diff editor. * @returns true if any files were opened, false if nothing to display @@ -208,6 +247,14 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS return; } + // Never enter projection mode for sessions that are in progress + // The user should only be in projection mode when reviewing completed code + if (isSessionInProgressStatus(session.status)) { + this.logService.trace('[AgentSessionProjection] Session is in progress, opening chat without projection mode'); + await this._openSessionInChatPanel(session); + return; + } + // For local sessions, check if there are pending edits to show // If there's nothing to focus, just open the chat without entering projection mode let hasUndecidedChanges = true; @@ -273,12 +320,7 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS } // Open the session in the chat panel (always, even without changes) - session.setRead(true); - await this.chatSessionsService.activateChatSessionItemProvider(session.providerType); - await this.chatWidgetService.openSession(session.resource, ChatViewPaneTarget, { - title: { preferred: session.label }, - revealIfOpened: true - }); + await this._openSessionInChatPanel(session); // For local sessions with changes, also pop open the edit session's changes view // Must be after openSession so the editing session context is available @@ -287,11 +329,13 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS } } - async exitProjection(): Promise { + async exitProjection(options?: { startNewChat?: boolean }): Promise { if (!this._isActive) { return; } + const startNewChat = options?.startNewChat ?? true; + // Save the current session's working set before exiting if (this._activeSession) { const sessionKey = this._activeSession.resource.toString(); @@ -323,8 +367,10 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS this._onDidChangeProjectionMode.fire(false); this._onDidChangeActiveSession.fire(undefined); - // Start a new chat to clear the sidebar - await this.commandService.executeCommand(ACTION_ID_NEW_CHAT); + // Start a new chat to clear the sidebar (unless caller wants to keep current chat) + if (startNewChat) { + await this.commandService.executeCommand(ACTION_ID_NEW_CHAT); + } } } From e0d13f07807876e3f829feb0d5cea48e68741574 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:06:46 -0500 Subject: [PATCH 07/21] Add progress sound on retry request for accessibility (#289462) --- .../contrib/chat/browser/actions/chatTitleActions.ts | 4 +++- .../workbench/contrib/chat/browser/widget/chatListWidget.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index e610c8500a8..bcbb0b5ba83 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -24,7 +24,7 @@ import { applyingChatEditsFailedContextKey, isChatEditingActionContext } from '. import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '../../common/chatService/chatService.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; import { ChatModeKind } from '../../common/constants.js'; -import { IChatWidgetService } from '../chat.js'; +import { IChatAccessibilityService, IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY } from './chatActions.js'; export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful'; @@ -260,6 +260,8 @@ export function registerChatTitleActions() { const request = chatModel?.getRequests().find(candidate => candidate.id === item.requestId); const languageModelId = widget?.input.currentLanguageModel; + const chatAccessibilityService = accessor.get(IChatAccessibilityService); + chatAccessibilityService.acceptRequest(item.sessionResource); chatService.resendRequest(request!, { userSelectedModelId: languageModelId, attempt: (request?.attempt ?? -1) + 1, diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index c26ee8b2f81..11d2b3e74c7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -30,7 +30,7 @@ import { IChatRequestModeInfo } from '../../common/model/chatModel.js'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; import { ChatAccessibilityProvider } from '../accessibility/chatAccessibilityProvider.js'; -import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from '../chat.js'; +import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from '../chat.js'; import { CodeBlockPart } from './chatContentParts/codeBlockPart.js'; import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js'; import { ChatEditorOptions } from './chatOptions.js'; @@ -257,6 +257,7 @@ export class ChatListWidget extends Disposable { @IContextMenuService private readonly contextMenuService: IContextMenuService, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService, ) { super(); @@ -343,6 +344,7 @@ export class ChatListWidget extends Disposable { userSelectedModelId: this._getCurrentLanguageModelId?.(), modeInfo: this._getCurrentModeInfo?.(), }; + this.chatAccessibilityService.acceptRequest(e.sessionResource); this.chatService.resendRequest(request, sendOptions).catch(e => this.logService.error('FAILED to rerun request', e)); } })); From 2970892bf385394d755f47ad481a6dfba6229239 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:23:37 -0800 Subject: [PATCH 08/21] Hook up archive state change notifications for extensions For #280422 --- .../api/browser/mainThreadChatSessions.ts | 16 ++++++++-- .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostChatSessions.ts | 30 +++++++++++++++---- .../browser/mainThreadChatSessions.test.ts | 2 ++ .../agentSessions/agentSessionsModel.ts | 9 ++++++ .../agentSessionsDataSource.test.ts | 1 + .../vscode.proposed.chatSessionsProvider.d.ts | 6 ++-- 7 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 38de78caf4a..27250fdab03 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -17,16 +17,17 @@ import { localize } from '../../../nls.js'; import { IDialogService } from '../../../platform/dialogs/common/dialogs.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { hasValidDiff, IAgentSession } from '../../contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { IAgentSessionsService } from '../../contrib/chat/browser/agentSessions/agentSessionsService.js'; import { ChatViewPaneTarget, IChatWidgetService, isIChatViewViewContext } from '../../contrib/chat/browser/chat.js'; import { IChatEditorOptions } from '../../contrib/chat/browser/widgetHosts/editor/chatEditor.js'; import { ChatEditorInput } from '../../contrib/chat/browser/widgetHosts/editor/chatEditorInput.js'; +import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { awaitStatsForSession } from '../../contrib/chat/common/chat.js'; -import { IChatAgentRequest } from '../../contrib/chat/common/participants/chatAgents.js'; -import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; import { IChatContentInlineReference, IChatProgress, IChatService, ResponseModelState } from '../../contrib/chat/common/chatService/chatService.js'; import { ChatSessionStatus, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionItem, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; -import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; +import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; +import { IChatAgentRequest } from '../../contrib/chat/common/participants/chatAgents.js'; import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../services/editor/common/editorService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; @@ -334,6 +335,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat constructor( private readonly _extHostContext: IExtHostContext, + @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, @IChatSessionsService private readonly _chatSessionsService: IChatSessionsService, @IChatService private readonly _chatService: IChatService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @@ -352,6 +354,14 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat await this.notifyOptionsChange(handle, sessionResource, updates); } }); + + this._register(this._agentSessionsService.model.onDidChangeSessionArchivedState(session => { + for (const [handle, { provider }] of this._itemProvidersRegistrations) { + if (provider.chatSessionType === session.providerType) { + this._proxy.$onDidChangeChatSessionItemState(handle, session.resource, session.isArchived()); + } + } + })); } private _getHandleForSessionType(chatSessionType: string): number | undefined { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6945985fc2c..78afb689625 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3363,6 +3363,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { export interface ExtHostChatSessionsShape { $provideChatSessionItems(providerHandle: number, token: CancellationToken): Promise[]>; + $onDidChangeChatSessionItemState(providerHandle: number, sessionResource: UriComponents, archived: boolean): void; $provideChatSessionContent(providerHandle: number, sessionResource: UriComponents, token: CancellationToken): Promise; $interruptChatSessionActiveResponse(providerHandle: number, sessionResource: UriComponents, requestId: string): Promise; diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index c4d34921e45..24064e799da 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -240,6 +240,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio readonly controller: vscode.ChatSessionItemController; readonly extension: IExtensionDescription; readonly disposable: DisposableStore; + readonly onDidChangeChatSessionItemStateEmitter: Emitter; }>(); private _nextChatSessionItemProviderHandle = 0; private readonly _chatSessionContentProviders = new Map()); + const onDidChangeChatSessionItemStateEmitter = disposables.add(new Emitter()); const collection = new ChatSessionItemCollectionImpl(() => { this._proxy.$onDidChangeChatSessionItems(controllerHandle); @@ -334,11 +334,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio let isDisposed = false; - const controller: vscode.ChatSessionItemController = { + const controller = Object.freeze({ id, refreshHandler, items: collection, - onDidArchiveChatSessionItem: onDidArchiveChatSessionItem.event, + onDidChangeChatSessionItemState: onDidChangeChatSessionItemStateEmitter.event, createChatSessionItem: (resource: vscode.Uri, label: string) => { if (isDisposed) { throw new Error('ChatSessionItemController has been disposed'); @@ -353,9 +353,9 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio isDisposed = true; disposables.dispose(); }, - }; + }); - this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, sessionType: id }); + this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, sessionType: id, onDidChangeChatSessionItemStateEmitter }); this._proxy.$registerChatSessionItemProvider(controllerHandle, id); disposables.add(toDisposable(() => { @@ -734,4 +734,22 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio return []; } } + + $onDidChangeChatSessionItemState(providerHandle: number, sessionResourceComponents: UriComponents, archived: boolean): void { + const controllerData = this._chatSessionItemControllers.get(providerHandle); + if (!controllerData) { + this._logService.warn(`No controller found for provider handle ${providerHandle}`); + return; + } + + const sessionResource = URI.revive(sessionResourceComponents); + const item = controllerData.controller.items.get(sessionResource); + if (!item) { + this._logService.warn(`No item found for session resource ${sessionResource.toString()}`); + return; + } + + item.archived = archived; + controllerData.onDidChangeChatSessionItemStateEmitter.fire(item); + } } diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 9e818d0a34a..2d0dd6906f2 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -58,6 +58,7 @@ suite('ObservableChatSession', function () { $invokeChatSessionRequestHandler: sinon.stub(), $disposeChatSessionContent: sinon.stub(), $provideChatSessionItems: sinon.stub(), + $onDidChangeChatSessionItemState: sinon.stub(), }; }); @@ -354,6 +355,7 @@ suite('MainThreadChatSessions', function () { $invokeChatSessionRequestHandler: sinon.stub(), $disposeChatSessionContent: sinon.stub(), $provideChatSessionItems: sinon.stub(), + $onDidChangeChatSessionItemState: sinon.stub(), }; const extHostContext = new class implements IExtHostContext { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 7023219910e..3af738df8e7 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -34,6 +34,7 @@ export interface IAgentSessionsModel { readonly onDidResolve: Event; readonly onDidChangeSessions: Event; + readonly onDidChangeSessionArchivedState: Event; readonly sessions: IAgentSession[]; getSession(resource: URI): IAgentSession | undefined; @@ -386,6 +387,9 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode private readonly _onDidChangeSessions = this._register(new Emitter()); readonly onDidChangeSessions = this._onDidChangeSessions.event; + private readonly _onDidChangeSessionArchivedState = this._register(new Emitter()); + readonly onDidChangeSessionArchivedState = this._onDidChangeSessionArchivedState.event; + private _sessions: ResourceMap; get sessions(): IAgentSession[] { return Array.from(this._sessions.values()); } @@ -661,6 +665,11 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 }; this.sessionStates.set(session.resource, { ...state, archived }); + const agentSession = this._sessions.get(session.resource); + if (agentSession) { + this._onDidChangeSessionArchivedState.fire(agentSession); + } + this._onDidChangeSessions.fire(); } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts index d551277757b..98267eb4bfa 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts @@ -54,6 +54,7 @@ suite('AgentSessionsDataSource', () => { onWillResolve: Event.None, onDidResolve: Event.None, onDidChangeSessions: Event.None, + onDidChangeSessionArchivedState: Event.None, resolve: async () => { }, }; } diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 016b45c2916..3f8a6a0b89f 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -100,11 +100,9 @@ declare module 'vscode' { refreshHandler: () => Thenable; /** - * Fired when an item is archived by the editor - * - * TODO: expose archive state on the item too? + * Fired when an item's archived state changes. */ - readonly onDidArchiveChatSessionItem: Event; + readonly onDidChangeChatSessionItemState: Event; } /** From 2b514ef2024945bee2e4e5163d30cb2cb2d71b63 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:31:36 +0000 Subject: [PATCH 09/21] Fix disposed store error in DetachedTerminalSnapshotMirror (#289154) --- .gitignore | 1 + .../browser/chatTerminalCommandMirror.ts | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 92971a7a573..1dbe527cb8a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ product.overrides.json *.tsbuildinfo .vscode-test vscode-telemetry-docs/ +test-output.json diff --git a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts index 3ea882a7f34..95908faac2f 100644 --- a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts +++ b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts @@ -576,7 +576,14 @@ export class DetachedTerminalSnapshotMirror extends Disposable { return getChatTerminalBackgroundColor(theme, this._contextKeyService, storedBackground); } } - }).then(terminal => this._register(terminal)); + }).then(terminal => { + // If the store is already disposed, dispose the terminal immediately + if (this._store.isDisposed) { + terminal.dispose(); + return terminal; + } + return this._register(terminal); + }); } private async _getTerminal(): Promise { @@ -593,6 +600,9 @@ export class DetachedTerminalSnapshotMirror extends Disposable { public async attach(container: HTMLElement): Promise { const terminal = await this._getTerminal(); + if (this._store.isDisposed) { + return; + } container.classList.add('chat-terminal-output-terminal'); const needsAttach = this._attachedContainer !== container || container.firstChild === null; if (needsAttach) { @@ -613,6 +623,9 @@ export class DetachedTerminalSnapshotMirror extends Disposable { return { lineCount: this._lastRenderedLineCount ?? output.lineCount, maxColumnWidth: this._lastRenderedMaxColumnWidth }; } const terminal = await this._getTerminal(); + if (this._store.isDisposed) { + return undefined; + } if (this._container) { this._applyTheme(this._container); } @@ -625,6 +638,9 @@ export class DetachedTerminalSnapshotMirror extends Disposable { return { lineCount: 0, maxColumnWidth: 0 }; } await new Promise(resolve => terminal.xterm.write(text, resolve)); + if (this._store.isDisposed) { + return undefined; + } this._dirty = false; this._lastRenderedLineCount = lineCount; // Only compute max column width for small outputs to avoid performance issues From a4683de307cd110f7d8b3710c9d1e2c2208cfdbb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:34:54 +0000 Subject: [PATCH 10/21] Fix task output tool cancellation token handling (#289446) --- .../terminalContrib/chatAgentTools/browser/taskHelpers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts index 7f8f1e8d39e..2c592de63d9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts @@ -257,7 +257,10 @@ export async function collectTerminalResults( } const outputMonitor = disposableStore.add(instantiationService.createInstance(OutputMonitor, execution, taskProblemPollFn, invocationContext, token, task._label)); - await Event.toPromise(outputMonitor.onDidFinishCommand); + await Promise.race([ + Event.toPromise(outputMonitor.onDidFinishCommand), + Event.toPromise(token.onCancellationRequested as Event) + ]); const pollingResult = outputMonitor.pollingResult; return { name: instance.shellLaunchConfig.name ?? instance.title ?? 'unknown', From f889dfd7305c592fb72f58377456a3c78065b996 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 22 Jan 2026 05:46:53 +0800 Subject: [PATCH 11/21] some fixes to dropdowns and better ai headers (#289475) * some fixes to dropdowns and better ai headers * add some synonyms --- .../chatThinkingContentPart.ts | 57 ++++++++++++------- .../media/chatSubagentContent.css | 5 ++ 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 6ed7bee0fb8..d52eb041205 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -549,34 +549,49 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen - Summarize ALL actions, not just tool calls. If there's reasoning or analysis without tool calls, summarize that too - Examples of non-tool actions: "Analyzing code structure", "Planning implementation", "Reviewing dependencies" + TOOL NAME FILTERING: + - NEVER include tool names like "Replace String in File", "Multi Replace String in File", "Create File", "Read File", etc. in the output + - If an action says "Edited X and used Replace String in File", output ONLY the action on X + - Tool names describe HOW something was done, not WHAT was done - always omit them + + VOCABULARY - Use varied synonyms for natural-sounding summaries: + - For edits: "Updated", "Modified", "Changed", "Refactored", "Fixed", "Adjusted" + - For reads: "Reviewed", "Examined", "Checked", "Inspected", "Analyzed", "Explored" + - For creates: "Created", "Added", "Generated" + - For searches: "Searched for", "Looked up", "Investigated" + - For terminal: "Ran command", "Executed" + - Choose the synonym that best fits the context of what was done + RULES FOR TOOL CALLS: - 1. If the SAME file was both edited AND read: Start with "Read and edited " - 2. If exactly ONE file was edited: Start with "Edited " (include actual filename) - 3. If exactly ONE file was read: Start with "Read " (include actual filename) - 4. If MULTIPLE files were edited: Start with "Edited X files" - 5. If MULTIPLE files were read: Start with "Read X files" - 6. If BOTH edits AND reads occurred on DIFFERENT files: Start with "Edited and read " if one each, otherwise "Edited X files and read Y files" - 7. For searches: Say "searched for " with the actual search term, NOT "searched for files" - 8. After the file info, you may add a brief summary of other actions (e.g., ran terminal, searched for X) if space permits + 1. If the SAME file was both edited AND read: Use a combined phrase like "Reviewed and updated " + 2. If exactly ONE file was edited: Start with an edit synonym + "" (include actual filename) + 3. If exactly ONE file was read: Start with a read synonym + "" (include actual filename) + 4. If MULTIPLE files were edited: Start with an edit synonym + "X files" + 5. If MULTIPLE files were read: Start with a read synonym + "X files" + 6. If BOTH edits AND reads occurred on DIFFERENT files: Combine them naturally + 7. For searches: Say "searched for " or "looked up " with the actual search term, NOT "searched for files" + 8. After the file info, you may add a brief summary of other actions if space permits 9. NEVER say "1 file" - always use the actual filename when there's only one file EXAMPLES: - - "Read HomePage.tsx, Edited HomePage.tsx" → "Read and edited HomePage.tsx" - - "Edited HomePage.tsx" → "Edited HomePage.tsx" - - "Read config.json, Read package.json" → "Read 2 files" - - "Edited App.tsx, Read utils.ts" → "Edited App.tsx and read utils.ts" - - "Edited App.tsx, Read utils.ts, Read types.ts" → "Edited App.tsx and read 2 files" - - "Edited index.ts, Edited styles.css, Ran terminal command" → "Edited 2 files and ran command" - - "Read README.md, Searched for AuthService" → "Read README.md and searched for AuthService" + - "Read HomePage.tsx, Edited HomePage.tsx" → "Reviewed and updated HomePage.tsx" + - "Edited HomePage.tsx" → "Updated HomePage.tsx" + - "Edited config.css and used Replace String in File" → "Modified config.css" + - "Edited App.tsx, used Multi Replace String in File" → "Refactored App.tsx" + - "Read config.json, Read package.json" → "Reviewed 2 files" + - "Edited App.tsx, Read utils.ts" → "Updated App.tsx and checked utils.ts" + - "Edited App.tsx, Read utils.ts, Read types.ts" → "Updated App.tsx and reviewed 2 files" + - "Edited index.ts, Edited styles.css, Ran terminal command" → "Modified 2 files and ran command" + - "Read README.md, Searched for AuthService" → "Checked README.md and searched for AuthService" - "Searched for login, Searched for authentication" → "Searched for login and authentication" - - "Edited api.ts, Edited models.ts, Read schema.json" → "Edited 2 files and read schema.json" - - "Edited Button.tsx, Edited Button.css, Edited index.ts" → "Edited 3 files" - - "Searched codebase for error handling" → "Searched for error handling" - - "Grep search for useState, Read App.tsx" → "Read App.tsx and searched for useState" + - "Edited api.ts, Edited models.ts, Read schema.json" → "Updated 2 files and reviewed schema.json" + - "Edited Button.tsx, Edited Button.css, Edited index.ts" → "Modified 3 files" + - "Searched codebase for error handling" → "Looked up error handling" + - "Grep search for useState, Read App.tsx" → "Examined App.tsx and searched for useState" - "Analyzing component architecture" → "Analyzed component architecture" - - "Planning refactor strategy, Read utils.ts" → "Planned refactor and read utils.ts" + - "Planning refactor strategy, Read utils.ts" → "Planned refactor and reviewed utils.ts" - No quotes, no trailing punctuation. Never say "searched for files" - always include the actual search term. + No quotes, no trailing punctuation. Never say "searched for files" - always include the actual search term. Never include tool names. Actions: ${context}`; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css index 3d606a77814..3821ce19322 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css @@ -37,6 +37,11 @@ display: none; } + /* renable icons in confirmation widget */ + .chat-confirmation-widget-container .codicon:not(.codicon-terminal) { + display: inline-flex; + } + .chat-collapsible-markdown-content { .rendered-markdown { font-size: var(--vscode-chat-font-size-body-s); From c146d03144156f7332a24fbd7ab96e8958f42422 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:51:38 -0800 Subject: [PATCH 12/21] Add mock --- .../api/test/browser/mainThreadChatSessions.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 2d0dd6906f2..438a6769d47 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -32,6 +32,9 @@ import { MainThreadChatSessions, ObservableChatSession } from '../../browser/mai import { ExtHostChatSessionsShape, IChatProgressDto, IChatSessionProviderOptions } from '../../common/extHost.protocol.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { MockChatService } from '../../../contrib/chat/test/common/chatService/mockChatService.js'; +import { IAgentSessionsService } from '../../../contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { IAgentSessionsModel } from '../../../contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { Event } from '../../../../base/common/event.js'; suite('ObservableChatSession', function () { let disposables: DisposableStore; @@ -389,6 +392,14 @@ suite('MainThreadChatSessions', function () { } }); instantiationService.stub(IChatService, new MockChatService()); + instantiationService.stub(IAgentSessionsService, new class extends mock() { + override get model(): IAgentSessionsModel { + return new class extends mock() { + override onDidChangeSessionArchivedState = Event.None; + }; + } + + }); chatSessionsService = disposables.add(instantiationService.createInstance(ChatSessionsService)); instantiationService.stub(IChatSessionsService, chatSessionsService); From 02ed32ae91bd84d92355e1e2f50ab1464c8550ce Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:55:07 -0800 Subject: [PATCH 13/21] fix https://github.com/microsoft/vscode-copilot-issues/issues/401 --- .../browser/tools/languageModelToolsService.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 2d29e9c5c9e..dc8e5a809b2 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -204,6 +204,24 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this._logService.trace(`LanguageModelToolsService#isPermitted: ToolSet ${toolOrToolSet.id} (${toolOrToolSet.referenceName}) permitted=${permitted}`); return permitted; } + for (const toolSet of this._toolSets) { + if (toolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolSet.referenceName)) { + for (const memberTool of toolSet.getTools()) { + if (memberTool.id === toolOrToolSet.id) { + this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=true (member of ${toolSet.referenceName})`); + return true; + } + } + } + } + + // Special case for 'vscode_fetchWebPage_internal', which is allowed if we allow 'web' tools + // Fetch is implemented with two tools, this one and 'copilot_fetchWebPage' + if (toolOrToolSet.id === 'vscode_fetchWebPage_internal' && permittedInternalToolSetIds.includes(SpecedToolAliases.web)) { + this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=true (special case)`); + return true; + } + this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=false`); return false; } From 510e4307cab3dcda172e5e3c771162d90b7ae3be Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:18:29 -0800 Subject: [PATCH 14/21] add tests --- .../tools/languageModelToolsService.test.ts | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts index 5232ad7deeb..13aeb97db96 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -2655,4 +2655,342 @@ suite('LanguageModelToolsService', () => { const result = await promise; assert.strictEqual(result.content[0].value, 'commit blocked'); }); + + test('isPermitted allows tools in permitted toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create tool in the 'read' toolset (permitted) + const readTool: IToolData = { + id: 'readToolInSet', + toolReferenceName: 'readToolRef', + modelDescription: 'Read Tool in Set', + displayName: 'Read Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(readTool)); + store.add(service.readToolSet.addTool(readTool)); + + // Create standalone tool not in any permitted toolset + const standaloneTool: IToolData = { + id: 'standaloneTool', + toolReferenceName: 'standaloneRef', + modelDescription: 'Standalone Tool', + displayName: 'Standalone Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(standaloneTool)); + + // Get tools - should include the tool in the read toolset but not the standalone tool + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('readToolInSet'), 'Tool in read toolset should be permitted when agent mode is disabled'); + assert.ok(!toolIds.includes('standaloneTool'), 'Standalone tool not in permitted toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted allows all tools when agent mode is enabled', () => { + // Enable agent mode (default) + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, true); + + // Create tool in the 'read' toolset + const readTool: IToolData = { + id: 'readToolEnabled', + toolReferenceName: 'readToolEnabledRef', + modelDescription: 'Read Tool', + displayName: 'Read Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(readTool)); + store.add(service.readToolSet.addTool(readTool)); + + // Create standalone tool not in any permitted toolset + const standaloneTool: IToolData = { + id: 'standaloneToolEnabled', + toolReferenceName: 'standaloneEnabledRef', + modelDescription: 'Standalone Tool', + displayName: 'Standalone Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(standaloneTool)); + + // Get tools - both should be available when agent mode is enabled + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('readToolEnabled'), 'Tool in read toolset should be permitted when agent mode is enabled'); + assert.ok(toolIds.includes('standaloneToolEnabled'), 'Standalone tool should be permitted when agent mode is enabled'); + }); + + test('isPermitted filters toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a custom internal toolset that is NOT in the permitted list + const customToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'customToolSet', + 'customToolSetRef', + { description: 'Custom Tool Set' } + )); + + const customTool: IToolData = { + id: 'customToolInSet', + toolReferenceName: 'customToolRef', + modelDescription: 'Custom Tool', + displayName: 'Custom Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(customTool)); + store.add(customToolSet.addTool(customTool)); + + // Get toolsets - read/search/web should be available, custom should not + const toolSets = Array.from(service.toolSets.get()); + const toolSetIds = Array.from(toolSets).map(ts => ts.id); + + assert.ok(toolSetIds.includes('read'), 'read toolset should be permitted when agent mode is disabled'); + assert.ok(!toolSetIds.includes('customToolSet'), 'custom toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted allows execute toolset tools when agent mode is enabled', () => { + // Enable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, true); + + // Create tool in the 'execute' toolset (only permitted when agent mode is enabled) + const executeTool: IToolData = { + id: 'executeToolInSet', + toolReferenceName: 'executeToolRef', + modelDescription: 'Execute Tool', + displayName: 'Execute Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(executeTool)); + store.add(service.executeToolSet.addTool(executeTool)); + + // Get tools - execute tool should be available when agent mode is enabled + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('executeToolInSet'), 'Tool in execute toolset should be permitted when agent mode is enabled'); + }); + + test('isPermitted blocks execute toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create tool in the 'execute' toolset (NOT permitted when agent mode is disabled) + const executeTool: IToolData = { + id: 'executeToolBlocked', + toolReferenceName: 'executeToolBlockedRef', + modelDescription: 'Execute Tool', + displayName: 'Execute Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(executeTool)); + store.add(service.executeToolSet.addTool(executeTool)); + + // Get tools - execute tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('executeToolBlocked'), 'Tool in execute toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted allows search toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a 'search' toolset (permitted when agent mode is disabled) + const searchToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'search', + SpecedToolAliases.search, + { description: 'Search Tool Set' } + )); + + const searchTool: IToolData = { + id: 'searchToolInSet', + toolReferenceName: 'searchToolRef', + modelDescription: 'Search Tool', + displayName: 'Search Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(searchTool)); + store.add(searchToolSet.addTool(searchTool)); + + // Get tools - search tool should be available when agent mode is disabled + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('searchToolInSet'), 'Tool in search toolset should be permitted when agent mode is disabled'); + }); + + test('isPermitted allows web toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a 'web' toolset (permitted when agent mode is disabled) + const webToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'web', + SpecedToolAliases.web, + { description: 'Web Tool Set' } + )); + + const webTool: IToolData = { + id: 'webToolInSet', + toolReferenceName: 'webToolRef', + modelDescription: 'Web Tool', + displayName: 'Web Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(webTool)); + store.add(webToolSet.addTool(webTool)); + + // Get tools - web tool should be available when agent mode is disabled + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('webToolInSet'), 'Tool in web toolset should be permitted when agent mode is disabled'); + }); + + test('isPermitted allows vscode_fetchWebPage_internal special case when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Register the special-cased fetch tool (not added to any toolset) + const fetchTool: IToolData = { + id: 'vscode_fetchWebPage_internal', + toolReferenceName: 'fetchWebPage', + modelDescription: 'Fetch Web Page', + displayName: 'Fetch Web Page', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(fetchTool)); + + // Get tools - this special tool should be available even when not in a toolset + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('vscode_fetchWebPage_internal'), 'vscode_fetchWebPage_internal should be permitted as special case when agent mode is disabled'); + }); + + test('isPermitted blocks extension tools not in permitted toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create extension tool not in any permitted toolset + const extensionTool: IToolData = { + id: 'extensionToolBlocked', + toolReferenceName: 'extensionToolRef', + modelDescription: 'Extension Tool', + displayName: 'Extension Tool', + source: { type: 'extension', label: 'Test Extension', extensionId: new ExtensionIdentifier('test.extension') }, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(extensionTool)); + + // Get tools - extension tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('extensionToolBlocked'), 'Extension tool not in permitted toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted blocks MCP tools not in permitted toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create MCP toolset (not in permitted list) + const mcpToolSet = store.add(service.createToolSet( + { type: 'mcp', label: 'Test MCP', serverLabel: 'Test MCP Server', instructions: undefined, collectionId: 'testMcp', definitionId: 'testMcpDef' }, + 'mcpToolSetBlocked', + 'mcpToolSetBlockedRef', + { description: 'MCP Tool Set' } + )); + + const mcpTool: IToolData = { + id: 'mcpToolBlocked', + toolReferenceName: 'mcpToolRef', + modelDescription: 'MCP Tool', + displayName: 'MCP Tool', + source: { type: 'mcp', label: 'Test MCP', serverLabel: 'Test MCP Server', instructions: undefined, collectionId: 'testMcp', definitionId: 'testMcpDef' }, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(mcpTool)); + store.add(mcpToolSet.addTool(mcpTool)); + + // Get tools - MCP tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('mcpToolBlocked'), 'MCP tool should NOT be permitted when agent mode is disabled'); + + // Get toolsets - MCP toolset should NOT be available + const toolSets = Array.from(service.toolSets.get()); + const toolSetIds = Array.from(toolSets).map(ts => ts.id); + + assert.ok(!toolSetIds.includes('mcpToolSetBlocked'), 'MCP toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted blocks agent toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create tool in the 'agent' toolset (NOT permitted when agent mode is disabled) + const agentTool: IToolData = { + id: 'agentToolBlocked', + toolReferenceName: 'agentToolBlockedRef', + modelDescription: 'Agent Tool', + displayName: 'Agent Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(agentTool)); + store.add(service.agentToolSet.addTool(agentTool)); + + // Get tools - agent tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('agentToolBlocked'), 'Tool in agent toolset should NOT be permitted when agent mode is disabled'); + + // Get toolsets - agent toolset should NOT be available + const toolSets = Array.from(service.toolSets.get()); + const toolSetIds = Array.from(toolSets).map(ts => ts.id); + + assert.ok(!toolSetIds.includes('agent'), 'agent toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted includes tool in multiple toolsets if one is permitted', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a tool that is added to both a permitted toolset (read) and a non-permitted toolset + const multiSetTool: IToolData = { + id: 'multiSetTool', + toolReferenceName: 'multiSetToolRef', + modelDescription: 'Multi Set Tool', + displayName: 'Multi Set Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(multiSetTool)); + + // Add to read toolset (permitted) + store.add(service.readToolSet.addTool(multiSetTool)); + + // Also create and add to a non-permitted toolset + const customToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'customMultiSet', + 'customMultiSetRef', + { description: 'Custom Multi Set' } + )); + store.add(customToolSet.addTool(multiSetTool)); + + // Get tools - tool should be available because it's in the 'read' toolset + const tools = Array.from(service.getTools()); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('multiSetTool'), 'Tool should be permitted if it belongs to at least one permitted toolset'); + }); }); From 99f2c90385d092c4ed59b7f56351d2800f151622 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:39:13 -0800 Subject: [PATCH 15/21] preserve agent session user filter when clicking notification filter --- .../experiments/agentTitleBarStatusWidget.ts | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts index 7afb4fbac28..e4876fe58e7 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts @@ -50,6 +50,8 @@ const QUICK_OPEN_ACTION_ID = 'workbench.action.quickOpenWithModes'; // Storage key for filter state const FILTER_STORAGE_KEY = 'agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu'; +// Storage key for saving user's filter state before we override it +const PREVIOUS_FILTER_STORAGE_KEY = 'agentSessions.filterExcludes.previousUserFilter'; const NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); const TITLE_DIRTY = '\u25cf '; @@ -673,14 +675,14 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { /** * Clear the filter if the currently filtered category becomes empty. - * For example, if filtered to "unread" but no unread sessions exist, clear the filter. + * For example, if filtered to "unread" but no unread sessions exist, restore user's previous filter. */ private _clearFilterIfCategoryEmpty(hasUnreadSessions: boolean, hasActiveSessions: boolean): void { const { isFilteredToUnread, isFilteredToInProgress } = this._getCurrentFilterState(); - // Clear filter if filtered category is now empty + // Restore user's filter if filtered category is now empty if ((isFilteredToUnread && !hasUnreadSessions) || (isFilteredToInProgress && !hasActiveSessions)) { - this._clearFilter(); + this._restoreUserFilter(); } } @@ -736,7 +738,38 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { } /** - * Opens the agent sessions view with a specific filter applied, or clears filter if already applied. + * Save the current user filter before we override it with a badge filter. + */ + private _saveUserFilter(): void { + const currentFilter = this._getStoredFilter(); + if (currentFilter) { + this.storageService.store(PREVIOUS_FILTER_STORAGE_KEY, JSON.stringify(currentFilter), StorageScope.PROFILE, StorageTarget.USER); + } + } + + /** + * Restore the user's previous filter (saved before we applied a badge filter). + */ + private _restoreUserFilter(): void { + const previousFilterStr = this.storageService.get(PREVIOUS_FILTER_STORAGE_KEY, StorageScope.PROFILE); + if (previousFilterStr) { + try { + const previousFilter = JSON.parse(previousFilterStr); + this._storeFilter(previousFilter); + } catch { + // Fall back to clearing if parse fails + this._clearFilter(); + } + } else { + // No previous filter saved, clear to default + this._clearFilter(); + } + // Clear the saved filter after restoring + this.storageService.remove(PREVIOUS_FILTER_STORAGE_KEY, StorageScope.PROFILE); + } + + /** + * Opens the agent sessions view with a specific filter applied, or restores previous filter if already applied. * @param filterType 'unread' to show only unread sessions, 'inProgress' to show only in-progress sessions */ private _openSessionsWithFilter(filterType: 'unread' | 'inProgress'): void { @@ -745,8 +778,11 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { // Toggle filter based on current state if (filterType === 'unread') { if (isFilteredToUnread) { - this._clearFilter(); + // Already filtered to unread - restore user's previous filter + this._restoreUserFilter(); } else { + // Save current filter before applying our own + this._saveUserFilter(); // Exclude read sessions to show only unread this._storeFilter({ providers: [], @@ -757,8 +793,11 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { } } else { if (isFilteredToInProgress) { - this._clearFilter(); + // Already filtered to in-progress - restore user's previous filter + this._restoreUserFilter(); } else { + // Save current filter before applying our own + this._saveUserFilter(); // Exclude Completed and Failed to show InProgress and NeedsInput this._storeFilter({ providers: [], From 1ba7fde75a7ca3e495032a3e2e777ad696176ee2 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:50:20 -0800 Subject: [PATCH 16/21] consistent colors for agents status ux --- .../media/agentsessionprojection.css | 18 ++++++------- .../media/agenttitlebarstatuswidget.css | 25 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css index 7f64094c2b4..cf18ea2566a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css @@ -5,12 +5,12 @@ /* Style all tabs with the same background as the agent status */ .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent) !important; + background-color: var(--vscode-quickInput-background) !important; } /* Active tab gets slightly stronger tint */ .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent) !important; + background-color: var(--vscode-chat-requestBubbleBackground) !important; } .hc-black .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab, @@ -27,15 +27,15 @@ @keyframes agent-session-projection-glow-pulse { 0%, 100% { box-shadow: - 0 0 8px 2px color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent), - 0 0 20px 4px color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent), - inset 0 0 15px 2px color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); + 0 0 8px 2px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 50%, transparent), + 0 0 20px 4px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 25%, transparent), + inset 0 0 15px 2px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 15%, transparent); } 50% { box-shadow: - 0 0 15px 4px color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent), - 0 0 35px 8px color-mix(in srgb, var(--vscode-progressBar-background) 35%, transparent), - inset 0 0 25px 4px color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); + 0 0 15px 4px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 70%, transparent), + 0 0 35px 8px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 35%, transparent), + inset 0 0 25px 4px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 25%, transparent); } } @@ -45,7 +45,7 @@ inset: 0; pointer-events: none; z-index: 1000; - border: 2px solid var(--vscode-progressBar-background); + border: 2px solid var(--vscode-chat-requestBubbleBackground); border-radius: 4px; animation: agent-session-projection-glow-pulse 2s ease-in-out infinite; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css index 0dfed92e7b6..7910d82f62f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css @@ -88,13 +88,13 @@ /* Active state - has running sessions */ .agent-status-pill.chat-input-mode.has-active { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); - border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); + background-color: var(--vscode-quickInput-background); + border: 1px solid var(--vscode-commandCenter-border, transparent); } .agent-status-pill.chat-input-mode.has-active:hover { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); - border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent); + background-color: var(--vscode-chat-requestBubbleBackground); + border-color: var(--vscode-commandCenter-border, transparent); } .agent-status-pill.chat-input-mode.has-active .agent-status-label { @@ -105,17 +105,18 @@ /* Unread state - has unread sessions (no background change, just indicator) */ .agent-status-pill.chat-input-mode.has-unread .agent-status-icon { font-size: 8px; + } /* Needs attention state - session requires user approval/confirmation/input */ .agent-status-pill.chat-input-mode.needs-attention { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); - border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); + background-color: var(--vscode-quickInput-background); + border: 1px solid var(--vscode-commandCenter-border, transparent); } .agent-status-pill.chat-input-mode.needs-attention:hover { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); - border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent); + background-color: var(--vscode-chat-requestBubbleBackground); + border-color: var(--vscode-commandCenter-border, transparent); } .agent-status-pill.chat-input-mode.needs-attention .agent-status-label { @@ -125,14 +126,14 @@ /* Session mode (viewing a session) */ .agent-status-pill.session-mode { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); - border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); + background-color: var(--vscode-quickInput-background); + border: 1px solid var(--vscode-commandCenter-border, transparent); padding: 0 12px; } .agent-status-pill.session-mode:hover { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); - border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent); + background-color: var(--vscode-chat-requestBubbleBackground); + border-color: var(--vscode-commandCenter-border, transparent); } /* Label (workspace name, centered) */ From f5aa2ad7205b65ed9b754c642a5b13662e550e2e Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 21 Jan 2026 14:53:01 -0800 Subject: [PATCH 17/21] Update prompt file provider interface for API discussion (#289441) --- src/vs/base/browser/markdownRenderer.ts | 1 - src/vs/base/common/network.ts | 3 - .../api/browser/mainThreadChatAgents2.ts | 13 +- .../workbench/api/common/extHost.api.impl.ts | 12 +- .../api/common/extHostChatAgents2.ts | 79 +----- src/vs/workbench/api/common/extHostTypes.ts | 20 -- .../api/test/browser/extHostTypes.test.ts | 114 -------- .../contrib/chat/browser/chat.contribution.ts | 5 - .../promptSyntax/chatPromptContentProvider.ts | 49 ---- .../chatPromptFileSystemProvider.ts | 119 -------- .../promptSyntax/chatPromptContentStore.ts | 74 ----- .../promptSyntax/service/promptsService.ts | 12 - .../service/promptsServiceImpl.ts | 20 -- .../chatPromptContentProvider.test.ts | 215 -------------- .../chatPromptFileSystemProvider.test.ts | 257 ----------------- .../chatPromptContentStore.test.ts | 193 ------------- .../service/promptsService.test.ts | 266 ------------------ .../vscode.proposed.chatPromptFiles.d.ts | 164 ++--------- 18 files changed, 37 insertions(+), 1579 deletions(-) delete mode 100644 src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts delete mode 100644 src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts delete mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts delete mode 100644 src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptContentProvider.test.ts delete mode 100644 src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts delete mode 100644 src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index b5006737fb6..e3f20d96726 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -600,7 +600,6 @@ function getDomSanitizerConfig(mdStrConfig: MdStrConfig, options: MarkdownSaniti Schemas.vscodeRemote, Schemas.vscodeRemoteResource, Schemas.vscodeNotebookCell, - Schemas.vscodeChatPrompt, // For links that are handled entirely by the action handler Schemas.internal, ]; diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 00859ca7344..74cb106fd3c 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -90,9 +90,6 @@ export namespace Schemas { /** Scheme used for local chat session content */ export const vscodeLocalChatSession = 'vscode-chat-session'; - /** Scheme used for virtual chat prompt files with embedded content */ - export const vscodeChatPrompt = 'vscode-chat-prompt'; - /** * Scheme used internally for webviews that aren't linked to a resource (i.e. not custom editors) */ diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index f0fd0935c21..e0416a2d09b 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -28,7 +28,6 @@ import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../cont import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/participants/chatAgents.js'; import { IPromptFileContext, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { isValidPromptType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; -import { IChatPromptContentStore } from '../../contrib/chat/common/promptSyntax/chatPromptContentStore.js'; import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/editing/chatEditingService.js'; import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; import { ChatRequestAgentPart } from '../../contrib/chat/common/requestParser/chatParserTypes.js'; @@ -123,7 +122,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @IExtensionService private readonly _extensionService: IExtensionService, @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, @IPromptsService private readonly _promptsService: IPromptsService, - @IChatPromptContentStore private readonly _chatPromptContentStore: IChatPromptContentStore, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, ) { super(); @@ -487,17 +485,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA } // Convert UriComponents to URI and register any inline content return contributions.map(c => { - const uri = URI.revive(c.uri); - // If this is a virtual prompt with inline content, register it with the store - if (c.content && uri.scheme === Schemas.vscodeChatPrompt) { - const uriKey = uri.toString(); - // Dispose any previous registration for this URI before registering new content - contentRegistrations.deleteAndDispose(uriKey); - contentRegistrations.set(uriKey, this._chatPromptContentStore.registerContent(uri, c.content)); - } return { - uri, - isEditable: c.isEditable + uri: URI.revive(c.uri), }; }); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 96c3e1380c0..c09e72566e7 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1546,19 +1546,19 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatContextProvider'); return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider); }, - registerCustomAgentProvider(provider: vscode.CustomAgentProvider): vscode.Disposable { + registerCustomAgentProvider(provider: vscode.ChatCustomAgentProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.agent, provider); }, - registerInstructionsProvider(provider: vscode.InstructionsProvider): vscode.Disposable { + registerInstructionsProvider(provider: vscode.ChatInstructionsProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.instructions, provider); }, - registerPromptFileProvider(provider: vscode.PromptFileProvider): vscode.Disposable { + registerPromptFileProvider(provider: vscode.ChatPromptFileProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.prompt, provider); }, - registerSkillProvider(provider: vscode.SkillProvider): vscode.Disposable { + registerSkillProvider(provider: vscode.ChatSkillProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.skill, provider); }, @@ -1973,10 +1973,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I McpStdioServerDefinition2: extHostTypes.McpStdioServerDefinition, McpToolAvailability: extHostTypes.McpToolAvailability, SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind, - CustomAgentChatResource: extHostTypes.CustomAgentChatResource, - InstructionsChatResource: extHostTypes.InstructionsChatResource, - PromptFileChatResource: extHostTypes.PromptFileChatResource, - SkillChatResource: extHostTypes.SkillChatResource, }; }; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index bc91aeec37d..5d246285967 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -38,7 +38,6 @@ import * as extHostTypes from './extHostTypes.js'; import { IPromptFileContext, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; -import { Schemas } from '../../../base/common/network.js'; export class ChatAgentResponseStream { @@ -426,7 +425,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _relatedFilesProviders = new Map(); private static _contributionsProviderIdPool = 0; - private readonly _promptFileProviders = new Map(); + private readonly _promptFileProviders = new Map(); private readonly _sessionDisposables: DisposableResourceMap = this._register(new DisposableResourceMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); @@ -510,7 +509,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS * Internal method that handles all prompt file provider types. * Routes custom agents, instructions, prompt files, and skills to the unified internal implementation. */ - registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider | vscode.SkillProvider): vscode.Disposable { + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: vscode.ChatCustomAgentProvider | vscode.ChatInstructionsProvider | vscode.ChatPromptFileProvider | vscode.ChatSkillProvider): vscode.Disposable { const handle = ExtHostChatAgents2._contributionsProviderIdPool++; this._promptFileProviders.set(handle, { extension, provider }); this._proxy.$registerPromptFileProvider(handle, type, extension.identifier); @@ -522,16 +521,16 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS let changeEvent: vscode.Event | undefined; switch (type) { case PromptsType.agent: - changeEvent = (provider as vscode.CustomAgentProvider).onDidChangeCustomAgents; + changeEvent = (provider as vscode.ChatCustomAgentProvider).onDidChangeCustomAgents; break; case PromptsType.instructions: - changeEvent = (provider as vscode.InstructionsProvider).onDidChangeInstructions; + changeEvent = (provider as vscode.ChatInstructionsProvider).onDidChangeInstructions; break; case PromptsType.prompt: - changeEvent = (provider as vscode.PromptFileProvider).onDidChangePromptFiles; + changeEvent = (provider as vscode.ChatPromptFileProvider).onDidChangePromptFiles; break; case PromptsType.skill: - changeEvent = (provider as vscode.SkillProvider).onDidChangeSkills; + changeEvent = (provider as vscode.ChatSkillProvider).onDidChangeSkills; break; } @@ -566,77 +565,23 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } const provider = providerData.provider; - let resources: vscode.CustomAgentChatResource[] | vscode.InstructionsChatResource[] | vscode.PromptFileChatResource[] | vscode.SkillChatResource[] | undefined; + let resources: vscode.ChatResource[] | undefined; switch (type) { case PromptsType.agent: - resources = await (provider as vscode.CustomAgentProvider).provideCustomAgents(context, token) ?? undefined; + resources = await (provider as vscode.ChatCustomAgentProvider).provideCustomAgents(context, token) ?? undefined; break; case PromptsType.instructions: - resources = await (provider as vscode.InstructionsProvider).provideInstructions(context, token) ?? undefined; + resources = await (provider as vscode.ChatInstructionsProvider).provideInstructions(context, token) ?? undefined; break; case PromptsType.prompt: - resources = await (provider as vscode.PromptFileProvider).providePromptFiles(context, token) ?? undefined; + resources = await (provider as vscode.ChatPromptFileProvider).providePromptFiles(context, token) ?? undefined; break; case PromptsType.skill: - resources = await (provider as vscode.SkillProvider).provideSkills(context, token) ?? undefined; + resources = await (provider as vscode.ChatSkillProvider).provideSkills(context, token) ?? undefined; break; } - // Convert ChatResourceDescriptor to IPromptFileResource format - return resources?.map(r => this.convertChatResourceDescriptorToPromptFileResource(r.resource, providerData.extension.identifier.value, type)); - } - - /** - * Creates a virtual URI for a prompt file. - * Format varies by type: - * - Skills: /${extensionId}/skills/${id}/SKILL.md - * - Agents: /${extensionId}/agents/${id}.agent.md - * - Instructions: /${extensionId}/instructions/${id}.instructions.md - * - Prompts: /${extensionId}/prompts/${id}.prompt.md - */ - createVirtualPromptUri(id: string, extensionId: string, type: PromptsType): URI { - let path: string; - switch (type) { - case PromptsType.skill: - path = `/${extensionId}/skills/${id}/SKILL.md`; - break; - case PromptsType.agent: - path = `/${extensionId}/agents/${id}.agent.md`; - break; - case PromptsType.instructions: - path = `/${extensionId}/instructions/${id}.instructions.md`; - break; - case PromptsType.prompt: - path = `/${extensionId}/prompts/${id}.prompt.md`; - break; - default: - throw new Error(`Unsupported PromptsType: ${type}`); - } - return URI.from({ - scheme: Schemas.vscodeChatPrompt, - path - }); - } - - convertChatResourceDescriptorToPromptFileResource(resource: vscode.ChatResourceDescriptor, extensionId: string, type: PromptsType): IPromptFileResource { - if (URI.isUri(resource)) { - // Plain URI - return { uri: resource }; - } else if ('id' in resource && 'content' in resource) { - // { id, content } - return { - content: resource.content, - uri: this.createVirtualPromptUri(resource.id, extensionId, type), - isEditable: undefined - }; - } else if ('uri' in resource && URI.isUri(resource.uri)) { - // { uri, isEditable? } - return { - uri: URI.revive(resource.uri), - isEditable: resource.isEditable - }; - } - throw new Error(`Invalid ChatResourceDescriptor: ${JSON.stringify(resource)}`); + return resources; } async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b4f2638fc95..c142462a992 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3895,24 +3895,4 @@ export class McpHttpServerDefinition implements vscode.McpHttpServerDefinition { //#endregion //#region Chat Prompt Files - -@es5ClassCompat -export class CustomAgentChatResource implements vscode.CustomAgentChatResource { - constructor(public readonly resource: vscode.ChatResourceDescriptor) { } -} - -@es5ClassCompat -export class InstructionsChatResource implements vscode.InstructionsChatResource { - constructor(public readonly resource: vscode.ChatResourceDescriptor) { } -} - -@es5ClassCompat -export class PromptFileChatResource implements vscode.PromptFileChatResource { - constructor(public readonly resource: vscode.ChatResourceDescriptor) { } -} - -@es5ClassCompat -export class SkillChatResource implements vscode.SkillChatResource { - constructor(public readonly resource: vscode.ChatResourceDescriptor) { } -} //#endregion diff --git a/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/src/vs/workbench/api/test/browser/extHostTypes.test.ts index ef844667ecc..8ee4767df55 100644 --- a/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -788,118 +788,4 @@ suite('ExtHostTypes', function () { m.content = 'Hello'; assert.deepStrictEqual(m.content, [new types.LanguageModelTextPart('Hello')]); }); - - test('CustomAgentChatResource - URI constructor', function () { - const uri = URI.file('/path/to/agent.md'); - const resource = new types.CustomAgentChatResource(uri); - - assert.ok(URI.isUri(resource.resource)); - assert.strictEqual(resource.resource.toString(), uri.toString()); - }); - - test('CustomAgentChatResource - URI constructor with options', function () { - const uri = URI.file('/path/to/agent.md'); - const resource = new types.CustomAgentChatResource({ uri, isEditable: true }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { uri: URI; isEditable?: boolean }; - assert.strictEqual(descriptor.uri.toString(), uri.toString()); - assert.strictEqual(descriptor.isEditable, true); - }); - - test('CustomAgentChatResource - content constructor', function () { - const content = '# My Agent\nThis is agent content'; - const resource = new types.CustomAgentChatResource({ id: 'my-agent-id', content }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { id: string; content: string }; - assert.strictEqual(descriptor.id, 'my-agent-id'); - assert.strictEqual(descriptor.content, content); - }); - - - - test('InstructionsChatResource - URI constructor', function () { - const uri = URI.file('/path/to/instructions.md'); - const resource = new types.InstructionsChatResource(uri); - - assert.ok(URI.isUri(resource.resource)); - assert.strictEqual(resource.resource.toString(), uri.toString()); - }); - - test('InstructionsChatResource - URI constructor with options', function () { - const uri = URI.file('/path/to/instructions.md'); - const resource = new types.InstructionsChatResource({ uri, isEditable: true }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { uri: URI; isEditable?: boolean }; - assert.strictEqual(descriptor.uri.toString(), uri.toString()); - assert.strictEqual(descriptor.isEditable, true); - }); - - test('InstructionsChatResource - content constructor', function () { - const content = '# Instructions\nFollow these steps'; - const resource = new types.InstructionsChatResource({ id: 'my-instructions-id', content }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { id: string; content: string }; - assert.strictEqual(descriptor.id, 'my-instructions-id'); - assert.strictEqual(descriptor.content, content); - }); - - - - test('PromptFileChatResource - URI constructor', function () { - const uri = URI.file('/path/to/prompt.md'); - const resource = new types.PromptFileChatResource(uri); - - assert.ok(URI.isUri(resource.resource)); - assert.strictEqual(resource.resource.toString(), uri.toString()); - }); - - test('PromptFileChatResource - URI constructor with options', function () { - const uri = URI.file('/path/to/prompt.md'); - const resource = new types.PromptFileChatResource({ uri, isEditable: true }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { uri: URI; isEditable?: boolean }; - assert.strictEqual(descriptor.uri.toString(), uri.toString()); - assert.strictEqual(descriptor.isEditable, true); - }); - - test('PromptFileChatResource - content constructor', function () { - const content = '# Prompt\nThis is my prompt content'; - const resource = new types.PromptFileChatResource({ id: 'my-prompt-id', content }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { id: string; content: string }; - assert.strictEqual(descriptor.id, 'my-prompt-id'); - assert.strictEqual(descriptor.content, content); - }); - - - - test('Chat prompt resources store different descriptors for different IDs', function () { - const resource1 = new types.CustomAgentChatResource({ id: 'id-one', content: 'content1' }); - const resource2 = new types.CustomAgentChatResource({ id: 'id-two', content: 'content2' }); - - const desc1 = resource1.resource as { id: string; content: string }; - const desc2 = resource2.resource as { id: string; content: string }; - assert.strictEqual(desc1.id, 'id-one'); - assert.strictEqual(desc2.id, 'id-two'); - assert.notStrictEqual(desc1.id, desc2.id); - }); - - test('Chat prompt resources store resource descriptors correctly', function () { - const agent = new types.CustomAgentChatResource({ id: 'test', content: 'content' }); - const instructions = new types.InstructionsChatResource({ id: 'test', content: 'content' }); - const prompt = new types.PromptFileChatResource({ id: 'test', content: 'content' }); - - assert.ok(!URI.isUri(agent.resource)); - assert.ok(!URI.isUri(instructions.resource)); - assert.ok(!URI.isUri(prompt.resource)); - assert.strictEqual((agent.resource as { id: string; content: string }).id, 'test'); - assert.strictEqual((instructions.resource as { id: string; content: string }).id, 'test'); - assert.strictEqual((prompt.resource as { id: string; content: string }).id, 'test'); - }); }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 4eb76e92ddd..264c0e1290b 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -53,7 +53,6 @@ import { ILanguageModelStatsService, LanguageModelStatsService } from '../common import { ILanguageModelToolsConfirmationService } from '../common/tools/languageModelToolsConfirmationService.js'; import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; import { ChatPromptFilesExtensionPointHandler } from '../common/promptSyntax/chatPromptFilesContribution.js'; -import { ChatPromptContentStore, IChatPromptContentStore } from '../common/promptSyntax/chatPromptContentStore.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME } from '../common/promptSyntax/config/promptFileLocations.js'; import { PromptLanguageFeaturesProvider } from '../common/promptSyntax/promptFileContributions.js'; @@ -95,8 +94,6 @@ import { ChatAttachmentResolveService, IChatAttachmentResolveService } from './a import { ChatMarkdownAnchorService, IChatMarkdownAnchorService } from './widget/chatContentParts/chatMarkdownAnchorService.js'; import { ChatContextPickService, IChatContextPickService } from './attachments/chatContextPickService.js'; import { ChatInputBoxContentProvider } from './widget/input/editor/chatEditorInputContentProvider.js'; -import { ChatPromptContentProvider } from './promptSyntax/chatPromptContentProvider.js'; -import './promptSyntax/chatPromptFileSystemProvider.js'; import { ChatEditingEditorAccessibility } from './chatEditing/chatEditingEditorAccessibility.js'; import { registerChatEditorActions } from './chatEditing/chatEditingEditorActions.js'; import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorContextKeys.js'; @@ -1186,7 +1183,6 @@ AccessibleViewRegistry.register(new EditsChatAccessibilityHelp()); AccessibleViewRegistry.register(new AgentChatAccessibilityHelp()); registerEditorFeature(ChatInputBoxContentProvider); -registerEditorFeature(ChatPromptContentProvider); class ChatSlashStaticSlashCommandsContribution extends Disposable { @@ -1348,7 +1344,6 @@ registerSingleton(IChatEditingService, ChatEditingService, InstantiationType.Del registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, InstantiationType.Delayed); registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed); registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed); -registerSingleton(IChatPromptContentStore, ChatPromptContentStore, InstantiationType.Delayed); registerSingleton(IChatContextPickService, ChatContextPickService, InstantiationType.Delayed); registerSingleton(IChatModeService, ChatModeService, InstantiationType.Delayed); registerSingleton(IChatAttachmentResolveService, ChatAttachmentResolveService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts deleted file mode 100644 index b193910212f..00000000000 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts +++ /dev/null @@ -1,49 +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 { Disposable } from '../../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ILanguageService } from '../../../../../editor/common/languages/language.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { ITextModelContentProvider, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { IChatPromptContentStore } from '../../common/promptSyntax/chatPromptContentStore.js'; -import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js'; - -/** - * Content provider for virtual chat prompt files created with inline content. - * These URIs have the scheme 'vscode-chat-prompt' and retrieve their content - * from the {@link IChatPromptContentStore} which maintains an in-memory map - * of content indexed by URI. This approach avoids putting content in the URI - * query string which is a misuse of URIs. - */ -export class ChatPromptContentProvider extends Disposable implements ITextModelContentProvider { - constructor( - @ITextModelService textModelService: ITextModelService, - @IModelService private readonly modelService: IModelService, - @ILanguageService private readonly languageService: ILanguageService, - @IChatPromptContentStore private readonly chatPromptContentStore: IChatPromptContentStore - ) { - super(); - this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeChatPrompt, this)); - } - - async provideTextContent(resource: URI): Promise { - const existing = this.modelService.getModel(resource); - if (existing) { - return existing; - } - - // Get the content from the content store - const content = this.chatPromptContentStore.getContent(resource) ?? ''; - - return this.modelService.createModel( - content, - this.languageService.createById(PROMPT_LANGUAGE_ID), - resource - ); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts deleted file mode 100644 index daee1c26006..00000000000 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts +++ /dev/null @@ -1,119 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from '../../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileWriteOptions, IFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, FileSystemProviderErrorCode, IFileService } from '../../../../../platform/files/common/files.js'; -import { IChatPromptContentStore } from '../../common/promptSyntax/chatPromptContentStore.js'; -import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../../common/contributions.js'; - -/** - * File system provider for virtual chat prompt files created with inline content. - * These URIs have the scheme 'vscode-chat-prompt' and retrieve their content - * from the {@link IChatPromptContentStore} which maintains an in-memory map - * of content indexed by URI. - * - * This enables external extensions to use VS Code's file system API to read - * these virtual prompt files. - */ -export class ChatPromptFileSystemProvider implements IFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability { - - get capabilities() { - return FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.Readonly; - } - - constructor( - private readonly chatPromptContentStore: IChatPromptContentStore - ) { } - - //#region Supported File Operations - - async stat(resource: URI): Promise { - const content = this.chatPromptContentStore.getContent(resource); - if (content === undefined) { - throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound); - } - - const size = VSBuffer.fromString(content).byteLength; - - return { - type: FileType.File, - ctime: 0, - mtime: 0, - size - }; - } - - async readFile(resource: URI): Promise { - const content = this.chatPromptContentStore.getContent(resource); - if (content === undefined) { - throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound); - } - - return VSBuffer.fromString(content).buffer; - } - - //#endregion - - //#region Unsupported File Operations - - readonly onDidChangeCapabilities = Event.None; - readonly onDidChangeFile = Event.None; - - async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise { - throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); - } - - async mkdir(resource: URI): Promise { - throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); - } - - async readdir(resource: URI): Promise<[string, FileType][]> { - return []; - } - - async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { - throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); - } - - async delete(resource: URI, opts: IFileDeleteOptions): Promise { - throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); - } - - watch(resource: URI, opts: IWatchOptions): IDisposable { - return Disposable.None; - } - - //#endregion -} - -/** - * Workbench contribution that registers the chat prompt file system provider. - */ -export class ChatPromptFileSystemProviderContribution extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.chatPromptFileSystemProvider'; - - constructor( - @IFileService fileService: IFileService, - @IChatPromptContentStore chatPromptContentStore: IChatPromptContentStore - ) { - super(); - - this._register(fileService.registerProvider( - Schemas.vscodeChatPrompt, - new ChatPromptFileSystemProvider(chatPromptContentStore) - )); - } -} - -registerWorkbenchContribution2( - ChatPromptFileSystemProviderContribution.ID, - ChatPromptFileSystemProviderContribution, - WorkbenchPhase.Eventually -); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts deleted file mode 100644 index a28402f8897..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts +++ /dev/null @@ -1,74 +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 { Disposable } from '../../../../../base/common/lifecycle.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; - -export const IChatPromptContentStore = createDecorator('chatPromptContentStore'); - -/** - * Service for managing virtual chat prompt content. - * - * This store maintains an in-memory map of content indexed by URI. - * URIs use the vscode-chat-prompt scheme with just the ID in the path, - * avoiding the need to encode large content in the URI query string. - */ -export interface IChatPromptContentStore { - readonly _serviceBrand: undefined; - - /** - * Registers content for a given URI. - * @param uri The URI to associate with the content. - * @param content The content to store. - * @returns A disposable that removes the content when disposed. - */ - registerContent(uri: URI, content: string): { dispose: () => void }; - - /** - * Retrieves content by URI. - * @param uri The URI to look up. - * @returns The content if found, or undefined. - */ - getContent(uri: URI): string | undefined; -} - -export class ChatPromptContentStore extends Disposable implements IChatPromptContentStore { - readonly _serviceBrand: undefined; - - private readonly _contentMap = new Map(); - - constructor() { - super(); - } - - /** - * Normalizes a URI by stripping query and fragment for consistent lookup. - * Query parameters like vscodeLinkType are metadata for rendering, not content identification. - */ - private normalizeUri(uri: URI): string { - return uri.with({ query: '', fragment: '' }).toString(); - } - - registerContent(uri: URI, content: string): { dispose: () => void } { - const key = this.normalizeUri(uri); - this._contentMap.set(key, content); - - const dispose = () => { - this._contentMap.delete(key); - }; - - return { dispose }; - } - - getContent(uri: URI): string | undefined { - return this._contentMap.get(this.normalizeUri(uri)); - } - - override dispose(): void { - this._contentMap.clear(); - super.dispose(); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 69735174644..604ea54f149 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -36,18 +36,6 @@ export interface IPromptFileResource { * The URI to the agent or prompt resource file. */ readonly uri: URI; - - /** - * Indicates whether the custom agent resource is editable. Defaults to false. - */ - readonly isEditable?: boolean; - - /** - * The inline content for virtual prompt files. This property is only used - * during IPC transfer from extension host to main thread - the content is - * immediately registered with the ChatPromptContentStore and not passed further. - */ - readonly content?: string; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index d25cc1fd8b5..20f827a4d70 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -34,7 +34,6 @@ import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../p import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; -import { IChatPromptContentStore } from '../chatPromptContentStore.js'; /** * Error thrown when a skill file is missing the required name attribute. @@ -129,7 +128,6 @@ export class PromptsService extends Disposable implements IPromptsService { @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IChatPromptContentStore private readonly chatPromptContentStore: IChatPromptContentStore ) { super(); @@ -298,15 +296,6 @@ export class PromptsService extends Disposable implements IPromptsService { } for (const file of files) { - if (!file.isEditable) { - try { - await this.filesConfigService.updateReadonly(file.uri, true); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - this.logger.error(`[listFromProviders] Failed to make file readonly: ${file.uri}`, msg); - } - } - result.push({ uri: file.uri, storage: PromptsStorage.extension, @@ -543,15 +532,6 @@ export class PromptsService extends Disposable implements IPromptsService { return this.getParsedPromptFile(model); } - // Handle virtual prompt URIs - get content from the content store - if (uri.scheme === Schemas.vscodeChatPrompt) { - const content = this.chatPromptContentStore.getContent(uri); - if (content !== undefined) { - return new PromptFileParser().parse(uri, content); - } - throw new Error(`Content not found in store for virtual prompt URI: ${uri.toString()}`); - } - const fileContent = await this.fileService.readFile(uri); if (token.isCancellationRequested) { throw new CancellationError(); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptContentProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptContentProvider.test.ts deleted file mode 100644 index 6323d234eba..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptContentProvider.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert from 'assert'; -import { URI } from '../../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { ILanguageService, ILanguageSelection } from '../../../../../../editor/common/languages/language.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { IModelService } from '../../../../../../editor/common/services/model.js'; -import { ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; -import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { ChatPromptContentProvider } from '../../../browser/promptSyntax/chatPromptContentProvider.js'; -import { ChatPromptContentStore, IChatPromptContentStore } from '../../../common/promptSyntax/chatPromptContentStore.js'; -import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/promptTypes.js'; -import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../../../base/common/network.js'; - -suite('ChatPromptContentProvider', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let instantiationService: TestInstantiationService; - let contentStore: ChatPromptContentStore; - let mockModelService: MockModelService; - let mockLanguageService: MockLanguageService; - let mockTextModelService: MockTextModelService; - let contentProvider: ChatPromptContentProvider; - - class MockLanguageSelection implements ILanguageSelection { - readonly languageId = PROMPT_LANGUAGE_ID; - readonly onDidChange = testDisposables.add(new (class extends Disposable { readonly event = () => ({ dispose: () => { } }); })()).event; - } - - class MockLanguageService { - createById(languageId: string): ILanguageSelection { - return new MockLanguageSelection(); - } - } - - class MockTextModel implements Partial { - constructor( - readonly uri: URI, - readonly content: string, - readonly languageId: string - ) { } - - getValue(): string { - return this.content; - } - - getLanguageId(): string { - return this.languageId; - } - } - - class MockModelService { - private models = new Map(); - - getModel(resource: URI): ITextModel | null { - return this.models.get(resource.toString()) ?? null; - } - - createModel(content: string, languageSelection: ILanguageSelection, resource: URI): ITextModel { - const model = new MockTextModel(resource, content, languageSelection.languageId) as unknown as ITextModel; - this.models.set(resource.toString(), model); - return model; - } - - setExistingModel(uri: URI, model: ITextModel): void { - this.models.set(uri.toString(), model); - } - - clear(): void { - this.models.clear(); - } - } - - class MockTextModelService { - private providers = new Map Promise }>(); - - registerTextModelContentProvider(scheme: string, provider: { provideTextContent: (resource: URI) => Promise }): IDisposable { - this.providers.set(scheme, provider); - return { dispose: () => this.providers.delete(scheme) }; - } - - getProvider(scheme: string) { - return this.providers.get(scheme); - } - } - - setup(() => { - instantiationService = testDisposables.add(new TestInstantiationService()); - - contentStore = testDisposables.add(new ChatPromptContentStore()); - mockModelService = new MockModelService(); - mockLanguageService = new MockLanguageService(); - mockTextModelService = new MockTextModelService(); - - instantiationService.stub(IChatPromptContentStore, contentStore); - instantiationService.stub(IModelService, mockModelService); - instantiationService.stub(ILanguageService, mockLanguageService as unknown as ILanguageService); - instantiationService.stub(ITextModelService, mockTextModelService as unknown as ITextModelService); - - contentProvider = testDisposables.add(instantiationService.createInstance(ChatPromptContentProvider)); - }); - - teardown(() => { - mockModelService.clear(); - }); - - test('registers as content provider for vscode-chat-prompt scheme', () => { - const provider = mockTextModelService.getProvider(Schemas.vscodeChatPrompt); - assert.ok(provider, 'Provider should be registered for vscode-chat-prompt scheme'); - }); - - test('provideTextContent creates model from stored content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-agent'); - const content = '# Test Agent\nThis is the agent content.'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const model = await contentProvider.provideTextContent(uri); - - assert.ok(model, 'Model should be created'); - assert.strictEqual((model as unknown as MockTextModel).getValue(), content); - assert.strictEqual((model as unknown as MockTextModel).getLanguageId(), PROMPT_LANGUAGE_ID); - }); - - test('provideTextContent returns existing model if available', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/existing'); - const existingContent = 'Existing model content'; - - const existingModel = new MockTextModel(uri, existingContent, PROMPT_LANGUAGE_ID) as unknown as ITextModel; - mockModelService.setExistingModel(uri, existingModel); - - const model = await contentProvider.provideTextContent(uri); - - assert.strictEqual(model, existingModel, 'Should return existing model'); - }); - - test('provideTextContent creates model with empty content when URI has no stored content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.instructions.md/missing'); - - const model = await contentProvider.provideTextContent(uri); - - assert.ok(model, 'Model should be created even without stored content'); - assert.strictEqual((model as unknown as MockTextModel).getValue(), ''); - }); - - test('provideTextContent uses prompt language ID', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/language-test'); - const content = 'Test content'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const model = await contentProvider.provideTextContent(uri); - - assert.ok(model); - assert.strictEqual((model as unknown as MockTextModel).getLanguageId(), PROMPT_LANGUAGE_ID); - }); - - test('handles multiple sequential requests for different URIs', async () => { - const uri1 = URI.parse('vscode-chat-prompt:/.agent.md/agent-1'); - const uri2 = URI.parse('vscode-chat-prompt:/.instructions.md/instructions-1'); - const uri3 = URI.parse('vscode-chat-prompt:/.prompt.md/prompt-1'); - - const content1 = 'Agent content'; - const content2 = 'Instructions content'; - const content3 = 'Prompt content'; - - testDisposables.add(contentStore.registerContent(uri1, content1)); - testDisposables.add(contentStore.registerContent(uri2, content2)); - testDisposables.add(contentStore.registerContent(uri3, content3)); - - const model1 = await contentProvider.provideTextContent(uri1); - const model2 = await contentProvider.provideTextContent(uri2); - const model3 = await contentProvider.provideTextContent(uri3); - - assert.strictEqual((model1 as unknown as MockTextModel).getValue(), content1); - assert.strictEqual((model2 as unknown as MockTextModel).getValue(), content2); - assert.strictEqual((model3 as unknown as MockTextModel).getValue(), content3); - }); - - test('content with special characters is handled correctly', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/special'); - const content = '# Unicode Test\n\n日本語テスト 🎉\n\n```typescript\nconst x = "hello";\n```'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const model = await contentProvider.provideTextContent(uri); - - assert.ok(model); - assert.strictEqual((model as unknown as MockTextModel).getValue(), content); - }); - - test('disposed content results in empty model', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/disposed-test'); - const content = 'Content that will be disposed'; - - const registration = contentStore.registerContent(uri, content); - - // Verify content exists - const model1 = await contentProvider.provideTextContent(uri); - assert.strictEqual((model1 as unknown as MockTextModel).getValue(), content); - - // Clear the model cache and dispose the content - mockModelService.clear(); - registration.dispose(); - - // Now requesting should return model with empty content - const model2 = await contentProvider.provideTextContent(uri); - assert.strictEqual((model2 as unknown as MockTextModel).getValue(), ''); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts deleted file mode 100644 index f43e53d2cce..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert from 'assert'; -import { URI } from '../../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { FileSystemProviderErrorCode, toFileSystemProviderErrorCode } from '../../../../../../platform/files/common/files.js'; -import { ChatPromptFileSystemProvider } from '../../../browser/promptSyntax/chatPromptFileSystemProvider.js'; -import { ChatPromptContentStore } from '../../../common/promptSyntax/chatPromptContentStore.js'; - -suite('ChatPromptFileSystemProvider', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let contentStore: ChatPromptContentStore; - let provider: ChatPromptFileSystemProvider; - - setup(() => { - contentStore = testDisposables.add(new ChatPromptContentStore()); - provider = new ChatPromptFileSystemProvider(contentStore); - }); - - suite('stat', () => { - test('returns stat for registered content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-agent'); - const content = '# Test Agent\nThis is test content.'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const stat = await provider.stat(uri); - - assert.strictEqual(stat.type, 1); // FileType.File - assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); - }); - - test('throws FileNotFound for unregistered URI', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/missing'); - - await assert.rejects( - () => provider.stat(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; - }, - 'Should throw FileNotFound error' - ); - }); - - test('returns correct size for empty content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/empty'); - - testDisposables.add(contentStore.registerContent(uri, '')); - - const stat = await provider.stat(uri); - - assert.strictEqual(stat.size, 0); - }); - - test('returns correct size for unicode content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.instructions.md/unicode'); - const content = '日本語テスト 🎉'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const stat = await provider.stat(uri); - - assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); - }); - }); - - suite('readFile', () => { - test('returns content for registered URI', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-agent'); - const content = '# Test Agent\nThis is test content.'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const result = await provider.readFile(uri); - - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - }); - - test('throws FileNotFound for unregistered URI', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/missing'); - - await assert.rejects( - () => provider.readFile(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; - }, - 'Should throw FileNotFound error' - ); - }); - - test('returns empty buffer for empty content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/empty'); - - testDisposables.add(contentStore.registerContent(uri, '')); - - const result = await provider.readFile(uri); - - assert.strictEqual(result.byteLength, 0); - }); - - test('preserves unicode content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.instructions.md/unicode'); - const content = '日本語テスト 🎉\n\n```typescript\nconst greeting = "こんにちは";\n```'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const result = await provider.readFile(uri); - - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - }); - - test('handles content with special markdown characters', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/markdown'); - const content = '# Heading\n\n- List item\n- Another item\n\n> Blockquote\n\n```\ncode block\n```'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const result = await provider.readFile(uri); - - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - }); - }); - - suite('content lifecycle', () => { - test('readFile fails after content is disposed', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/lifecycle-test'); - const content = 'Temporary content'; - - const registration = contentStore.registerContent(uri, content); - - // Verify content is readable - const result = await provider.readFile(uri); - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - - // Dispose the content - registration.dispose(); - - // Now reading should fail - await assert.rejects( - () => provider.readFile(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; - } - ); - }); - - test('stat fails after content is disposed', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/lifecycle-stat'); - const content = 'Content for stat test'; - - const registration = contentStore.registerContent(uri, content); - - // Verify stat works - const stat = await provider.stat(uri); - assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); - - // Dispose the content - registration.dispose(); - - // Now stat should fail - await assert.rejects( - () => provider.stat(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; - } - ); - }); - }); - - suite('URI normalization', () => { - test('readFile succeeds when URI has query parameters', async () => { - const baseUri = URI.parse('vscode-chat-prompt:/.agent.md/query-test'); - const content = 'Content for query test'; - - testDisposables.add(contentStore.registerContent(baseUri, content)); - - // Read with query parameters - const uriWithQuery = baseUri.with({ query: 'vscodeLinkType=prompt' }); - const result = await provider.readFile(uriWithQuery); - - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - }); - - test('stat succeeds when URI has fragment', async () => { - const baseUri = URI.parse('vscode-chat-prompt:/.instructions.md/fragment-test'); - const content = 'Content for fragment test'; - - testDisposables.add(contentStore.registerContent(baseUri, content)); - - // Stat with fragment - const uriWithFragment = baseUri.with({ fragment: 'section1' }); - const stat = await provider.stat(uriWithFragment); - - assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); - }); - }); - - suite('unsupported operations', () => { - test('writeFile throws NoPermissions error', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/write-test'); - - await assert.rejects( - () => provider.writeFile(uri, new Uint8Array(), { create: true, overwrite: true, unlock: false, atomic: false }), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; - } - ); - }); - - test('mkdir throws NoPermissions error', async () => { - const uri = URI.parse('vscode-chat-prompt:/test-dir'); - - await assert.rejects( - () => provider.mkdir(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; - } - ); - }); - - test('delete throws NoPermissions error', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/delete-test'); - - await assert.rejects( - () => provider.delete(uri, { recursive: false, useTrash: false, atomic: false }), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; - } - ); - }); - - test('rename throws NoPermissions error', async () => { - const from = URI.parse('vscode-chat-prompt:/.agent.md/rename-from'); - const to = URI.parse('vscode-chat-prompt:/.agent.md/rename-to'); - - await assert.rejects( - () => provider.rename(from, to, { overwrite: false }), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; - } - ); - }); - - test('readdir returns empty array', async () => { - const uri = URI.parse('vscode-chat-prompt:/'); - - const result = await provider.readdir(uri); - - assert.deepStrictEqual(result, []); - }); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts deleted file mode 100644 index 75f4496a0e3..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert from 'assert'; -import { URI } from '../../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { ChatPromptContentStore } from '../../../common/promptSyntax/chatPromptContentStore.js'; - -suite('ChatPromptContentStore', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let store: ChatPromptContentStore; - - setup(() => { - store = testDisposables.add(new ChatPromptContentStore()); - }); - - test('registerContent stores content retrievable by URI', () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-id'); - const content = '# Test Agent\nThis is test content'; - - const disposable = store.registerContent(uri, content); - testDisposables.add(disposable); - - const retrieved = store.getContent(uri); - assert.strictEqual(retrieved, content); - }); - - test('getContent returns undefined for unregistered URI', () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/unknown-id'); - - const retrieved = store.getContent(uri); - assert.strictEqual(retrieved, undefined); - }); - - test('registerContent returns disposable that removes content', () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/disposable-test'); - const content = 'Content to be disposed'; - - const disposable = store.registerContent(uri, content); - - // Content should exist before disposal - assert.strictEqual(store.getContent(uri), content); - - // Dispose and verify content is removed - disposable.dispose(); - assert.strictEqual(store.getContent(uri), undefined); - }); - - test('multiple registrations for different URIs are independent', () => { - const uri1 = URI.parse('vscode-chat-prompt:/.agent.md/id-1'); - const uri2 = URI.parse('vscode-chat-prompt:/.instructions.md/id-2'); - const content1 = 'Content 1'; - const content2 = 'Content 2'; - - const disposable1 = store.registerContent(uri1, content1); - const disposable2 = store.registerContent(uri2, content2); - testDisposables.add(disposable1); - testDisposables.add(disposable2); - - assert.strictEqual(store.getContent(uri1), content1); - assert.strictEqual(store.getContent(uri2), content2); - - // Disposing one should not affect the other - disposable1.dispose(); - assert.strictEqual(store.getContent(uri1), undefined); - assert.strictEqual(store.getContent(uri2), content2); - }); - - test('re-registering same URI overwrites content', () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/overwrite-test'); - const content1 = 'Original content'; - const content2 = 'Updated content'; - - const disposable1 = store.registerContent(uri, content1); - testDisposables.add(disposable1); - - assert.strictEqual(store.getContent(uri), content1); - - const disposable2 = store.registerContent(uri, content2); - testDisposables.add(disposable2); - - assert.strictEqual(store.getContent(uri), content2); - }); - - test('store disposal clears all content', () => { - const uri1 = URI.parse('vscode-chat-prompt:/.agent.md/clear-1'); - const uri2 = URI.parse('vscode-chat-prompt:/.agent.md/clear-2'); - - store.registerContent(uri1, 'Content 1'); - store.registerContent(uri2, 'Content 2'); - - assert.strictEqual(store.getContent(uri1), 'Content 1'); - assert.strictEqual(store.getContent(uri2), 'Content 2'); - - // Create a new store for this test that we can dispose independently - const localStore = new ChatPromptContentStore(); - const localUri = URI.parse('vscode-chat-prompt:/.agent.md/local'); - localStore.registerContent(localUri, 'Local content'); - - assert.strictEqual(localStore.getContent(localUri), 'Local content'); - - localStore.dispose(); - assert.strictEqual(localStore.getContent(localUri), undefined); - }); - - test('empty string content is stored correctly', () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/empty-content'); - - const disposable = store.registerContent(uri, ''); - testDisposables.add(disposable); - - const retrieved = store.getContent(uri); - assert.strictEqual(retrieved, ''); - }); - - test('content with special characters is stored correctly', () => { - const uri = URI.parse('vscode-chat-prompt:/.instructions.md/special-chars'); - const content = '# Test\n\nUnicode: 你好世界 🎉\nSpecial: ${{variable}} @mention #tag'; - - const disposable = store.registerContent(uri, content); - testDisposables.add(disposable); - - const retrieved = store.getContent(uri); - assert.strictEqual(retrieved, content); - }); - - test('URI comparison is string-based', () => { - // Same logical URI created two different ways - const uri1 = URI.parse('vscode-chat-prompt:/.agent.md/test'); - const uri2 = URI.from({ - scheme: 'vscode-chat-prompt', - path: '/.agent.md/test' - }); - - const content = 'Test content'; - const disposable = store.registerContent(uri1, content); - testDisposables.add(disposable); - - // Should be retrievable with equivalent URI - assert.strictEqual(store.getContent(uri2), content); - }); - - test('getContent normalizes URI by stripping query parameters', () => { - const baseUri = URI.parse('vscode-chat-prompt:/.agent.md/normalize-test'); - const content = 'Normalized content'; - - const disposable = store.registerContent(baseUri, content); - testDisposables.add(disposable); - - // Should retrieve content when queried with extra query parameters - const uriWithQuery = baseUri.with({ query: 'vscodeLinkType=prompt' }); - assert.strictEqual(store.getContent(uriWithQuery), content); - }); - - test('getContent normalizes URI by stripping fragment', () => { - const baseUri = URI.parse('vscode-chat-prompt:/.instructions.md/fragment-test'); - const content = 'Content with fragment lookup'; - - const disposable = store.registerContent(baseUri, content); - testDisposables.add(disposable); - - // Should retrieve content when queried with fragment - const uriWithFragment = baseUri.with({ fragment: 'section1' }); - assert.strictEqual(store.getContent(uriWithFragment), content); - }); - - test('getContent normalizes URI by stripping both query and fragment', () => { - const baseUri = URI.parse('vscode-chat-prompt:/.prompt.md/full-normalize'); - const content = 'Fully normalized content'; - - const disposable = store.registerContent(baseUri, content); - testDisposables.add(disposable); - - // Should retrieve content when queried with both query and fragment - const uriWithBoth = baseUri.with({ query: 'vscodeLinkType=skill&foo=bar', fragment: 'heading' }); - assert.strictEqual(store.getContent(uriWithBoth), content); - }); - - test('registerContent normalizes URI so content registered with query is found without it', () => { - const uriWithQuery = URI.parse('vscode-chat-prompt:/.agent.md/register-with-query?vscodeLinkType=agent'); - const content = 'Content registered with query'; - - const disposable = store.registerContent(uriWithQuery, content); - testDisposables.add(disposable); - - // Should retrieve content using base URI without query - const baseUri = uriWithQuery.with({ query: '' }); - assert.strictEqual(store.getContent(baseUri), content); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index af040c6e439..044a0f1db1a 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -1742,78 +1742,6 @@ suite('PromptsService', () => { assert.strictEqual(actualAfterDispose.length, 0); }); - test('Custom agent provider with isEditable', async () => { - const readonlyAgentUri = URI.parse('file://extensions/my-extension/readonlyAgent.agent.md'); - const editableAgentUri = URI.parse('file://extensions/my-extension/editableAgent.agent.md'); - const extension = { - identifier: { value: 'test.my-extension' }, - enabledApiProposals: ['chatParticipantPrivate'] - } as unknown as IExtensionDescription; - - // Mock the agent file content - await mockFiles(fileService, [ - { - path: readonlyAgentUri.path, - contents: [ - '---', - 'description: \'Readonly agent from provider\'', - '---', - 'I am a readonly agent.', - ] - }, - { - path: editableAgentUri.path, - contents: [ - '---', - 'description: \'Editable agent from provider\'', - '---', - 'I am an editable agent.', - ] - } - ]); - - const provider = { - providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { - return [ - { - uri: readonlyAgentUri, - isEditable: false - }, - { - uri: editableAgentUri, - isEditable: true - } - ]; - } - }; - - const registered = service.registerPromptFileProvider(extension, PromptsType.agent, provider); - - // Spy on updateReadonly to verify it's called correctly - const filesConfigService = instaService.get(IFilesConfigurationService); - const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); - - // List prompt files to trigger the readonly check - await service.listPromptFiles(PromptsType.agent, CancellationToken.None); - - // Verify updateReadonly was called only for the non-editable agent - assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); - assert.ok(updateReadonlySpy.calledWith(readonlyAgentUri, true), 'updateReadonly should be called with readonly agent URI and true'); - - const actual = await service.getCustomAgents(CancellationToken.None); - assert.strictEqual(actual.length, 2); - - const readonlyAgent = actual.find(a => a.name === 'readonlyAgent'); - const editableAgent = actual.find(a => a.name === 'editableAgent'); - - assert.ok(readonlyAgent, 'Readonly agent should be found'); - assert.ok(editableAgent, 'Editable agent should be found'); - assert.strictEqual(readonlyAgent!.description, 'Readonly agent from provider'); - assert.strictEqual(editableAgent!.description, 'Editable agent from provider'); - - registered.dispose(); - }); - test('Contributed agent file that does not exist should not crash', async () => { const nonExistentUri = URI.parse('file://extensions/my-extension/nonexistent.agent.md'); const existingUri = URI.parse('file://extensions/my-extension/existing.agent.md'); @@ -1911,68 +1839,6 @@ suite('PromptsService', () => { assert.strictEqual(foundAfterDispose, undefined); }); - test('Instructions provider with isEditable flag', async () => { - const readonlyInstructionUri = URI.parse('file://extensions/my-extension/readonly.instructions.md'); - const editableInstructionUri = URI.parse('file://extensions/my-extension/editable.instructions.md'); - const extension = { - identifier: { value: 'test.my-extension' }, - enabledApiProposals: ['chatParticipantPrivate'] - } as unknown as IExtensionDescription; - - // Mock the instruction file content - await mockFiles(fileService, [ - { - path: readonlyInstructionUri.path, - contents: [ - '# Readonly instruction content' - ] - }, - { - path: editableInstructionUri.path, - contents: [ - '# Editable instruction content' - ] - } - ]); - - const provider = { - providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { - return [ - { - uri: readonlyInstructionUri, - isEditable: false - }, - { - uri: editableInstructionUri, - isEditable: true - } - ]; - } - }; - - const registered = service.registerPromptFileProvider(extension, PromptsType.instructions, provider); - - // Spy on updateReadonly to verify it's called correctly - const filesConfigService = instaService.get(IFilesConfigurationService); - const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); - - // List prompt files to trigger the readonly check - await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - - // Verify updateReadonly was called only for the non-editable instruction - assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); - assert.ok(updateReadonlySpy.calledWith(readonlyInstructionUri, true), 'updateReadonly should be called with readonly instruction URI and true'); - - const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - const readonlyInstruction = actual.find(i => i.uri.toString() === readonlyInstructionUri.toString()); - const editableInstruction = actual.find(i => i.uri.toString() === editableInstructionUri.toString()); - - assert.ok(readonlyInstruction, 'Readonly instruction should be found'); - assert.ok(editableInstruction, 'Editable instruction should be found'); - - registered.dispose(); - }); - test('Prompt file provider', async () => { const promptUri = URI.parse('file://extensions/my-extension/myPrompt.prompt.md'); const extension = { @@ -2018,68 +1884,6 @@ suite('PromptsService', () => { assert.strictEqual(foundAfterDispose, undefined); }); - test('Prompt file provider with isEditable flag', async () => { - const readonlyPromptUri = URI.parse('file://extensions/my-extension/readonly.prompt.md'); - const editablePromptUri = URI.parse('file://extensions/my-extension/editable.prompt.md'); - const extension = { - identifier: { value: 'test.my-extension' }, - enabledApiProposals: ['chatParticipantPrivate'] - } as unknown as IExtensionDescription; - - // Mock the prompt file content - await mockFiles(fileService, [ - { - path: readonlyPromptUri.path, - contents: [ - '# Readonly prompt content' - ] - }, - { - path: editablePromptUri.path, - contents: [ - '# Editable prompt content' - ] - } - ]); - - const provider = { - providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { - return [ - { - uri: readonlyPromptUri, - isEditable: false - }, - { - uri: editablePromptUri, - isEditable: true - } - ]; - } - }; - - const registered = service.registerPromptFileProvider(extension, PromptsType.prompt, provider); - - // Spy on updateReadonly to verify it's called correctly - const filesConfigService = instaService.get(IFilesConfigurationService); - const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); - - // List prompt files to trigger the readonly check - await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); - - // Verify updateReadonly was called only for the non-editable prompt - assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); - assert.ok(updateReadonlySpy.calledWith(readonlyPromptUri, true), 'updateReadonly should be called with readonly prompt URI and true'); - - const actual = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); - const readonlyPrompt = actual.find(i => i.uri.toString() === readonlyPromptUri.toString()); - const editablePrompt = actual.find(i => i.uri.toString() === editablePromptUri.toString()); - - assert.ok(readonlyPrompt, 'Readonly prompt should be found'); - assert.ok(editablePrompt, 'Editable prompt should be found'); - - registered.dispose(); - }); - test('Skill file provider', async () => { const skillUri = URI.parse('file://extensions/my-extension/mySkill/SKILL.md'); const extension = { @@ -2129,76 +1933,6 @@ suite('PromptsService', () => { assert.strictEqual(foundAfterDispose, undefined); }); - test('Skill file provider with isEditable flag', async () => { - const readonlySkillUri = URI.parse('file://extensions/my-extension/readonlySkill/SKILL.md'); - const editableSkillUri = URI.parse('file://extensions/my-extension/editableSkill/SKILL.md'); - const extension = { - identifier: { value: 'test.my-extension' }, - enabledApiProposals: ['chatParticipantPrivate'] - } as unknown as IExtensionDescription; - - // Mock the skill file content - await mockFiles(fileService, [ - { - path: readonlySkillUri.path, - contents: [ - '---', - 'name: "Readonly Skill"', - 'description: "A readonly skill"', - '---', - 'Readonly skill content.', - ] - }, - { - path: editableSkillUri.path, - contents: [ - '---', - 'name: "Editable Skill"', - 'description: "An editable skill"', - '---', - 'Editable skill content.', - ] - } - ]); - - const provider = { - providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { - return [ - { - uri: readonlySkillUri, - isEditable: false - }, - { - uri: editableSkillUri, - isEditable: true - } - ]; - } - }; - - const registered = service.registerPromptFileProvider(extension, PromptsType.skill, provider); - - // Spy on updateReadonly to verify it's called correctly - const filesConfigService = instaService.get(IFilesConfigurationService); - const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); - - // List prompt files to trigger the readonly check - await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - - // Verify updateReadonly was called only for the non-editable skill - assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); - assert.ok(updateReadonlySpy.calledWith(readonlySkillUri, true), 'updateReadonly should be called with readonly skill URI and true'); - - const actual = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - const readonlySkill = actual.find(i => i.uri.toString() === readonlySkillUri.toString()); - const editableSkill = actual.find(i => i.uri.toString() === editableSkillUri.toString()); - - assert.ok(readonlySkill, 'Readonly skill should be found'); - assert.ok(editableSkill, 'Editable skill should be found'); - - registered.dispose(); - }); - suite('findAgentSkills', () => { teardown(() => { sinon.restore(); diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index f97cb106c10..e683d6ce600 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -9,99 +9,23 @@ declare module 'vscode' { // #region Resource Classes /** - * Describes a chat resource file. + * Represents a chat-related resource, such as a custom agent, instructions, prompt file, or skill. */ - export type ChatResourceDescriptor = - | Uri - | { uri: Uri; isEditable?: boolean } - | { - id: string; - content: string; - }; - - /** - * Represents a custom agent resource file (e.g., .agent.md). - */ - export class CustomAgentChatResource { + export interface ChatResource { /** - * The custom agent resource descriptor. + * Uri to the chat resource. This is typically a `.agent.md`, `.instructions.md`, `.prompt.md`, or `SKILL.md` file. */ - readonly resource: ChatResourceDescriptor; - - /** - * Creates a new custom agent resource from the specified resource. - * @param resource The chat resource descriptor. - */ - constructor(resource: ChatResourceDescriptor); - } - - /** - * Represents an instructions resource file. - */ - export class InstructionsChatResource { - /** - * The instructions resource descriptor. - */ - readonly resource: ChatResourceDescriptor; - - /** - * Creates a new instructions resource from the specified resource. - * @param resource The chat resource descriptor. - */ - constructor(resource: ChatResourceDescriptor); - } - - /** - * Represents a prompt file resource (e.g., .prompt.md). - */ - export class PromptFileChatResource { - /** - * The prompt file resource descriptor. - */ - readonly resource: ChatResourceDescriptor; - - /** - * Creates a new prompt file resource from the specified resource. - * @param resource The chat resource descriptor. - */ - constructor(resource: ChatResourceDescriptor); - } - - /** - * Represents a skill file resource (SKILL.md) - */ - export class SkillChatResource { - /** - * The skill resource descriptor. - */ - readonly resource: ChatResourceDescriptor; - - /** - * Creates a new skill resource from the specified resource URI pointing to SKILL.md. - * The parent folder name needs to match the name of the skill in the frontmatter. - * @param resource The chat resource descriptor. - */ - constructor(resource: ChatResourceDescriptor); + readonly uri: Uri; } // #endregion // #region Providers - /** - * Options for querying custom agents. - */ - export type CustomAgentContext = object; - /** * A provider that supplies custom agent resources (from .agent.md files) for repositories. */ - export interface CustomAgentProvider { - /** - * A human-readable label for this provider. - */ - readonly label: string; - + export interface ChatCustomAgentProvider { /** * An optional event to signal that custom agents have changed. */ @@ -109,30 +33,17 @@ declare module 'vscode' { /** * Provide the list of custom agents available. - * @param context Context for the query. + * @param context Context for the provide call. * @param token A cancellation token. * @returns An array of custom agents or a promise that resolves to such. */ - provideCustomAgents( - context: CustomAgentContext, - token: CancellationToken - ): ProviderResult; + provideCustomAgents(context: unknown, token: CancellationToken): ProviderResult; } - /** - * Context for querying instructions. - */ - export type InstructionsContext = object; - /** * A provider that supplies instructions resources for repositories. */ - export interface InstructionsProvider { - /** - * A human-readable label for this provider. - */ - readonly label: string; - + export interface ChatInstructionsProvider { /** * An optional event to signal that instructions have changed. */ @@ -140,30 +51,17 @@ declare module 'vscode' { /** * Provide the list of instructions available. - * @param context Context for the query. + * @param context Context for the provide call. * @param token A cancellation token. * @returns An array of instructions or a promise that resolves to such. */ - provideInstructions( - context: InstructionsContext, - token: CancellationToken - ): ProviderResult; + provideInstructions(context: unknown, token: CancellationToken): ProviderResult; } - /** - * Context for querying prompt files. - */ - export type PromptFileContext = object; - /** * A provider that supplies prompt file resources (from .prompt.md files) for repositories. */ - export interface PromptFileProvider { - /** - * A human-readable label for this provider. - */ - readonly label: string; - + export interface ChatPromptFileProvider { /** * An optional event to signal that prompt files have changed. */ @@ -171,34 +69,21 @@ declare module 'vscode' { /** * Provide the list of prompt files available. - * @param context Context for the query. + * @param context Context for the provide call. * @param token A cancellation token. * @returns An array of prompt files or a promise that resolves to such. */ - providePromptFiles( - context: PromptFileContext, - token: CancellationToken - ): ProviderResult; + providePromptFiles(context: unknown, token: CancellationToken): ProviderResult; } // #endregion // #region SkillProvider - /** - * Context for querying skills. - */ - export type SkillContext = object; - /** * A provider that supplies SKILL.md resources for agents. */ - export interface SkillProvider { - /** - * A human-readable label for this provider. - */ - readonly label: string; - + export interface ChatSkillProvider { /** * An optional event to signal that skills have changed. */ @@ -206,14 +91,11 @@ declare module 'vscode' { /** * Provide the list of skills available. - * @param context Context for the query. + * @param context Context for the provide call. * @param token A cancellation token. * @returns An array of skill resources or a promise that resolves to such. */ - provideSkills( - context: SkillContext, - token: CancellationToken - ): ProviderResult; + provideSkills(context: unknown, token: CancellationToken): ProviderResult; } // #endregion @@ -226,34 +108,28 @@ declare module 'vscode' { * @param provider The custom agent provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerCustomAgentProvider( - provider: CustomAgentProvider - ): Disposable; + export function registerCustomAgentProvider(provider: ChatCustomAgentProvider): Disposable; /** * Register a provider for instructions. * @param provider The instructions provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerInstructionsProvider( - provider: InstructionsProvider - ): Disposable; + export function registerInstructionsProvider(provider: ChatInstructionsProvider): Disposable; /** * Register a provider for prompt files. * @param provider The prompt file provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerPromptFileProvider( - provider: PromptFileProvider - ): Disposable; + export function registerPromptFileProvider(provider: ChatPromptFileProvider): Disposable; /** * Register a provider for skills. * @param provider The skill provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerSkillProvider(provider: SkillProvider): Disposable; + export function registerSkillProvider(provider: ChatSkillProvider): Disposable; } // #endregion From ee4eeae992b0c0c8f86dafbaea8358792d14c19b Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:53:17 -0800 Subject: [PATCH 18/21] address https://github.com/microsoft/vscode/pull/289506#discussion_r2714625082 --- .../experiments/agentTitleBarStatusWidget.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts index e4876fe58e7..968304cc64e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts @@ -739,8 +739,18 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { /** * Save the current user filter before we override it with a badge filter. + * Only saves if the current filter is NOT already a badge filter (unread or in-progress). + * This preserves the original user filter when switching between badge filters. */ private _saveUserFilter(): void { + const { isFilteredToUnread, isFilteredToInProgress } = this._getCurrentFilterState(); + + // Don't overwrite the saved filter if we're already in a badge-filtered state + // The previous user filter should already be saved + if (isFilteredToUnread || isFilteredToInProgress) { + return; + } + const currentFilter = this._getStoredFilter(); if (currentFilter) { this.storageService.store(PREVIOUS_FILTER_STORAGE_KEY, JSON.stringify(currentFilter), StorageScope.PROFILE, StorageTarget.USER); From 21e0fe5855308e932008da5629d53c6a187fefd5 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:55:31 -0800 Subject: [PATCH 19/21] chore: bump vscode/sqlite3 (#289483) --- package-lock.json | 106 +++------------------------------------------- package.json | 2 +- 2 files changed, 7 insertions(+), 101 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3932e294a5f..38a8cbefb8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@vscode/proxy-agent": "^0.36.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", - "@vscode/sqlite3": "5.1.11-vscode", + "@vscode/sqlite3": "5.1.12-vscode", "@vscode/sudo-prompt": "9.3.2", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", @@ -1301,7 +1301,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -1314,7 +1313,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -3414,48 +3412,14 @@ } }, "node_modules/@vscode/sqlite3": { - "version": "5.1.11-vscode", - "resolved": "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.1.11-vscode.tgz", - "integrity": "sha512-x2vBjFRZj/34Ji46lrxotjUtgljistPZU3cbxpckml3bMwF+Z0zbJYiplIeskHLo2g0Kj3kvR8MRRJ+o2nxNug==", + "version": "5.1.12-vscode", + "resolved": "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.1.12-vscode.tgz", + "integrity": "sha512-WLTftbMtK3Ni0s+q46qtKJ2CFtA3YrS5N4GcrETDCxqNTQAvk1LlYlG3RwGE6vZLcUqPt3TCHobijYeNUhEQ9Q==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "node-addon-api": "^8.2.0", - "tar": "^6.1.11" - } - }, - "node_modules/@vscode/sqlite3/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@vscode/sqlite3/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@vscode/sqlite3/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "tar": "^7.5.4" } }, "node_modules/@vscode/sqlite3/node_modules/node-addon-api": { @@ -3467,29 +3431,6 @@ "node": "^18 || ^20 || >= 21" } }, - "node_modules/@vscode/sqlite3/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@vscode/sqlite3/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/@vscode/sudo-prompt": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz", @@ -5240,7 +5181,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -8042,36 +7982,6 @@ "node": ">=14.14" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", @@ -12403,6 +12313,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -12411,7 +12322,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -12424,7 +12334,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -16464,7 +16373,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.4.tgz", "integrity": "sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16517,7 +16425,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -16527,7 +16434,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 5ab7a100a18..c3241820c69 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@vscode/proxy-agent": "^0.36.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", - "@vscode/sqlite3": "5.1.11-vscode", + "@vscode/sqlite3": "5.1.12-vscode", "@vscode/sudo-prompt": "9.3.2", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", From ec4208a29fc6beb3c250f5110d31a19cfa2586f5 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:55:57 -0800 Subject: [PATCH 20/21] chore: bump windows-ca-certs (#289468) --- package-lock.json | 6 +++--- remote/package-lock.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 38a8cbefb8f..a920485dbc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3615,9 +3615,9 @@ } }, "node_modules/@vscode/windows-ca-certs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.3.tgz", - "integrity": "sha512-C0Iq5RcH+H31GUZ8bsMORsX3LySVkGAqe4kQfUSVcCqJ0QOhXkhgwUMU7oCiqYLXaQWyXFp6Fj6eMdt05uK7VA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.4.tgz", + "integrity": "sha512-DcDLjBpu8srh6wUiZqEMyhXHzNDO81ecZOttL3+1u3Iht4CS6Qtxy5WkTPX/aDgbheASO/MK8yg6uLq58RzEWg==", "hasInstallScript": true, "license": "BSD", "optional": true, diff --git a/remote/package-lock.json b/remote/package-lock.json index 8ed8a663e27..c53948ab5ce 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -485,9 +485,9 @@ } }, "node_modules/@vscode/windows-ca-certs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.3.tgz", - "integrity": "sha512-C0Iq5RcH+H31GUZ8bsMORsX3LySVkGAqe4kQfUSVcCqJ0QOhXkhgwUMU7oCiqYLXaQWyXFp6Fj6eMdt05uK7VA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.4.tgz", + "integrity": "sha512-DcDLjBpu8srh6wUiZqEMyhXHzNDO81ecZOttL3+1u3Iht4CS6Qtxy5WkTPX/aDgbheASO/MK8yg6uLq58RzEWg==", "hasInstallScript": true, "license": "BSD", "optional": true, From 6f6a479500a30e097004a46769e0c10608a7ae9e Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:30:18 -0800 Subject: [PATCH 21/21] chore: bump tar (#289513) --- build/npm/gyp/package-lock.json | 33 ++++++++------------------------- package-lock.json | 6 +++--- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/build/npm/gyp/package-lock.json b/build/npm/gyp/package-lock.json index 19a8610c339..1ca858e42d2 100644 --- a/build/npm/gyp/package-lock.json +++ b/build/npm/gyp/package-lock.json @@ -682,9 +682,9 @@ "license": "ISC" }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -694,22 +694,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1085,17 +1069,16 @@ } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz", + "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { diff --git a/package-lock.json b/package-lock.json index a920485dbc7..532300c6956 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16370,9 +16370,9 @@ } }, "node_modules/tar": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.4.tgz", - "integrity": "sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz", + "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0",