From 9216191ee6a3eef15111465f230c0b904e435c1d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 24 Jan 2026 18:10:10 +0100 Subject: [PATCH 1/7] agent sessions - tweaks to default settings for sessions window (#290149) --- .../profiles/agent-sessions.code-profile | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/resources/profiles/agent-sessions.code-profile b/resources/profiles/agent-sessions.code-profile index 2df6531bfca81..815ec6a3bcbf1 100644 --- a/resources/profiles/agent-sessions.code-profile +++ b/resources/profiles/agent-sessions.code-profile @@ -2,27 +2,29 @@ "name": "Agent Sessions", "icon": "vscode", "settings": { - "workbench.statusBar.visible": false, - "workbench.activityBar.location": "hidden", - "workbench.sideBar.location": "right", - "workbench.secondarySideBar.defaultVisibility": "maximized", - "workbench.secondarySideBar.restoreMaximized": true, - "chat.restoreLastPanelSession": true, - "workbench.startupEditor": "none", - "workbench.editor.restoreEditors": false, - "chat.agentsControl.enabled": true, - "chat.unifiedAgentsBar.enabled": true, - "files.autoSave": "afterDelay", - "window.title": "Agent Sessions", - "workbench.editor.showTabs": "single", - "workbench.colorTheme": "2026-dark-experimental", - "chat.agentsControl.triStateToggle": true, - "workbench.tips.enabled": false, - "chat.agent.maxRequests": 1000, - "workbench.layoutControl.type": "toggles", - "inlineChat.affordance": "editor", - "inlineChat.renderMode": "hover", - "github.copilot.chat.claudeCode.enabled": true, - "github.copilot.chat.languageContext.typescript.enabled": true + "workbench.statusBar.visible": false, + "workbench.activityBar.location": "hidden", + "workbench.sideBar.location": "right", + "workbench.secondarySideBar.defaultVisibility": "maximized", + "workbench.secondarySideBar.restoreMaximized": true, + "chat.restoreLastPanelSession": true, + "workbench.startupEditor": "none", + "workbench.editor.restoreEditors": false, + "chat.agentsControl.enabled": true, + "chat.unifiedAgentsBar.enabled": true, + "files.autoSave": "afterDelay", + "window.title": "Agent Sessions", + "workbench.editor.showTabs": "single", + "workbench.colorTheme": "2026-dark-experimental", + "chat.agentsControl.triStateToggle": true, + "workbench.tips.enabled": false, + "chat.agent.maxRequests": 1000, + "workbench.layoutControl.type": "toggles", + "inlineChat.affordance": "editor", + "inlineChat.renderMode": "hover", + "github.copilot.chat.claudeCode.enabled": true, + "github.copilot.chat.languageContext.typescript.enabled": true, + "diffEditor.renderSideBySide": false, + "diffEditor.hideUnchangedRegions.enabled": true } } From 66431c046e8f06f4e600460e40c43185e5d68286 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 24 Jan 2026 18:43:45 +0100 Subject: [PATCH 2/7] Revert "Do not show copilot walkthrough if agents welcome view is enabled" (#290153) --- .../welcomeGettingStarted/browser/gettingStartedService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 2ae37f9fd8f51..d672ea7b95e75 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -437,8 +437,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this.storageService.store(walkthroughMetadataConfigurationKey, JSON.stringify([...this.metadata.entries()]), StorageScope.PROFILE, StorageTarget.USER); const hadLastFoucs = await this.hostService.hadLastFocus(); - const startupEditor = this.configurationService.getValue('workbench.startupEditor'); - if (hadLastFoucs && sectionToOpen && this.configurationService.getValue('workbench.welcomePage.walkthroughs.openOnInstall') && startupEditor !== 'agentSessionsWelcomePage') { + if (hadLastFoucs && sectionToOpen && this.configurationService.getValue('workbench.welcomePage.walkthroughs.openOnInstall')) { type GettingStartedAutoOpenClassification = { owner: 'lramos15'; comment: 'When a walkthrough is opened upon extension installation'; From 1830b747fea73febc121d0aef59f810732530e21 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:15:20 -0800 Subject: [PATCH 3/7] replace `optionsChangeCallback` with `onRequestNotifyExtension` to improve reliability (#290155) * replace optionsChangeCallback with onRequestNotifyExtension to improve reliability * code review --- .../api/browser/mainThreadChatSessions.ts | 11 +++++---- .../chatSessions/chatSessions.contribution.ts | 24 ++++++++----------- .../chat/common/chatSessionsService.ts | 21 +++++++++++----- .../test/common/mockChatSessionsService.ts | 17 ++++++------- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 3ec1bec01ca71..6a9340b4c1db7 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -348,14 +348,15 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat this._proxy = this._extHostContext.getProxy(ExtHostContext.ExtHostChatSessions); - this._chatSessionsService.setOptionsChangeCallback(async (sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>) => { + this._register(this._chatSessionsService.onRequestNotifyExtension(({ sessionResource, updates, waitUntil }) => { const handle = this._getHandleForSessionType(sessionResource.scheme); + this._logService.trace(`[MainThreadChatSessions] onRequestNotifyExtension received: scheme '${sessionResource.scheme}', handle ${handle}, ${updates.length} update(s)`); if (handle !== undefined) { - await this.notifyOptionsChange(handle, sessionResource, updates); + waitUntil(this.notifyOptionsChange(handle, sessionResource, updates)); } else { this._logService.warn(`[MainThreadChatSessions] Cannot notify option change for scheme '${sessionResource.scheme}': no provider registered. Registered schemes: [${Array.from(this._sessionTypeToHandle.keys()).join(', ')}]`); } - }); + })); this._register(this._agentSessionsService.model.onDidChangeSessionArchivedState(session => { for (const [handle, { provider }] of this._itemProvidersRegistrations) { @@ -709,10 +710,12 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat * Notify the extension about option changes for a session */ async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>): Promise { + this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: starting proxy call for handle ${handle}, sessionResource ${sessionResource}`); try { await this._proxy.$provideHandleOptionsChange(handle, sessionResource, updates, CancellationToken.None); + this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: proxy call completed for handle ${handle}, sessionResource ${sessionResource}`); } catch (error) { - this._logService.error(`Error notifying extension about options change for handle ${handle}, sessionResource ${sessionResource}:`, error); + this._logService.error(`[MainThreadChatSessions] notifyOptionsChange: error for handle ${handle}, sessionResource ${sessionResource}:`, error); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index ae320452a1d6d..d67e1a23322f6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -7,7 +7,7 @@ import { sep } from '../../../../../base/common/path.js'; import { raceCancellationError } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; +import { AsyncEmitter, Emitter, Event } from '../../../../../base/common/event.js'; import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; @@ -31,7 +31,7 @@ import { ExtensionsRegistry } from '../../../../services/extensions/common/exten import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, SessionOptionsChangedCallback } from '../../common/chatSessionsService.js'; +import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } from '../../common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; @@ -277,6 +277,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ public get onDidChangeSessionOptions() { return this._onDidChangeSessionOptions.event; } private readonly _onDidChangeOptionGroups = this._register(new Emitter()); public get onDidChangeOptionGroups() { return this._onDidChangeOptionGroups.event; } + private readonly _onRequestNotifyExtension = this._register(new AsyncEmitter()); + public get onRequestNotifyExtension() { return this._onRequestNotifyExtension.event; } private readonly inProgressMap: Map = new Map(); private readonly _sessionTypeOptions: Map = new Map(); @@ -1030,15 +1032,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return this._sessionTypeOptions.get(chatSessionType); } - private _optionsChangeCallback?: SessionOptionsChangedCallback; - - /** - * Set the callback for notifying extensions about option changes - */ - public setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void { - this._optionsChangeCallback = callback; - } - /** * Notify extension about option changes for a session */ @@ -1046,13 +1039,16 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ if (!updates.length) { return; } - if (this._optionsChangeCallback) { - await this._optionsChangeCallback(sessionResource, updates); - } + this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: starting for ${sessionResource}, ${updates.length} update(s): [${updates.map(u => u.optionId).join(', ')}]`); + // Fire event to notify MainThreadChatSessions (which forwards to extension host) + // Uses fireAsync to properly await async listener work via waitUntil pattern + await this._onRequestNotifyExtension.fireAsync({ sessionResource, updates }, CancellationToken.None); + this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: fireAsync completed for ${sessionResource}`); for (const u of updates) { this.setSessionOption(sessionResource, u.optionId, u.value); } this._onDidChangeSessionOptions.fire(sessionResource); + this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: finished for ${sessionResource}`); } /** diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index df1091c4dcf73..14fda9d587171 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Event } from '../../../../base/common/event.js'; +import { Event, IWaitUntil } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable } from '../../../../base/common/observable.js'; @@ -192,10 +192,14 @@ export interface IChatSessionContentProvider { provideChatSessionContent(sessionResource: URI, token: CancellationToken): Promise; } -export type SessionOptionsChangedCallback = (sessionResource: URI, updates: ReadonlyArray<{ - optionId: string; - value: string | IChatSessionProviderOptionItem; -}>) => Promise; +/** + * Event fired when session options need to be sent to the extension. + * Extends IWaitUntil to allow listeners to register async work that will be awaited. + */ +export interface IChatSessionOptionsWillNotifyExtensionEvent extends IWaitUntil { + readonly sessionResource: URI; + readonly updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>; +} export interface IChatSessionsService { readonly _serviceBrand: undefined; @@ -264,7 +268,12 @@ export interface IChatSessionsService { getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined; setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void; - setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void; + /** + * Event fired when session options change and need to be sent to the extension. + * MainThreadChatSessions subscribes to this to forward changes to the extension host. + * Uses IWaitUntil pattern to allow listeners to register async work. + */ + readonly onRequestNotifyExtension: Event; notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise; registerChatModelChangeListeners(chatService: IChatService, chatSessionType: string, onChange: () => void): IDisposable; diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index ea9db8d7c8c70..80e7de23580f2 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Emitter } from '../../../../../base/common/event.js'; +import { AsyncEmitter, Emitter } from '../../../../../base/common/event.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -12,7 +12,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { IChatAgentAttachmentCapabilities } from '../../common/participants/chatAgents.js'; import { IChatModel } from '../../common/model/chatModel.js'; import { IChatService } from '../../common/chatService/chatService.js'; -import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionsExtensionPoint, IChatSessionsService, SessionOptionsChangedCallback } from '../../common/chatSessionsService.js'; +import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; export class MockChatSessionsService implements IChatSessionsService { _serviceBrand: undefined; @@ -37,6 +37,9 @@ export class MockChatSessionsService implements IChatSessionsService { private readonly _onDidChangeOptionGroups = new Emitter(); readonly onDidChangeOptionGroups = this._onDidChangeOptionGroups.event; + private readonly _onRequestNotifyExtension = new AsyncEmitter(); + readonly onRequestNotifyExtension = this._onRequestNotifyExtension.event; + private sessionItemProviders = new Map(); private contentProviders = new Map(); private contributions: IChatSessionsExtensionPoint[] = []; @@ -160,14 +163,8 @@ export class MockChatSessionsService implements IChatSessionsService { } } - private optionsChangeCallback?: SessionOptionsChangedCallback; - - setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void { - this.optionsChangeCallback = callback; - } - - async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise { - await this.optionsChangeCallback?.(sessionResource, updates); + async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise { + await this._onRequestNotifyExtension.fireAsync({ sessionResource, updates }, CancellationToken.None); } notifySessionItemsChanged(chatSessionType: string): void { From ec69f294d9c5460843331b92a8eb99b29e853ac0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 24 Jan 2026 19:40:56 +0100 Subject: [PATCH 4/7] =?UTF-8?q?Revert=20"Revert=20"Do=20not=20show=20copil?= =?UTF-8?q?ot=20walkthrough=20if=20agents=20welcome=20view=20is=20ena?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 66431c046e8f06f4e600460e40c43185e5d68286. --- .../welcomeGettingStarted/browser/gettingStartedService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index d672ea7b95e75..2ae37f9fd8f51 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -437,7 +437,8 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this.storageService.store(walkthroughMetadataConfigurationKey, JSON.stringify([...this.metadata.entries()]), StorageScope.PROFILE, StorageTarget.USER); const hadLastFoucs = await this.hostService.hadLastFocus(); - if (hadLastFoucs && sectionToOpen && this.configurationService.getValue('workbench.welcomePage.walkthroughs.openOnInstall')) { + const startupEditor = this.configurationService.getValue('workbench.startupEditor'); + if (hadLastFoucs && sectionToOpen && this.configurationService.getValue('workbench.welcomePage.walkthroughs.openOnInstall') && startupEditor !== 'agentSessionsWelcomePage') { type GettingStartedAutoOpenClassification = { owner: 'lramos15'; comment: 'When a walkthrough is opened upon extension installation'; From dd29fc3b0c1b27228e5cb88cf80669b024b1e951 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 24 Jan 2026 19:42:12 +0100 Subject: [PATCH 5/7] Revert "Agents welcome page default in insiders" --- .../browser/gettingStarted.contribution.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index d13c53c9927cd..d5ff017820993 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -32,7 +32,6 @@ import { Categories } from '../../../../platform/action/common/actionCommonCateg import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { GettingStartedAccessibleView } from './gettingStartedAccessibleView.js'; -import product from '../../../../platform/product/common/product.js'; import { AgentSessionsWelcomePage } from '../../welcomeAgentSessions/browser/agentSessionsWelcome.js'; export * as icons from './gettingStartedIcons.js'; @@ -326,7 +325,7 @@ configurationRegistry.registerConfiguration({ localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.terminal' }, "Open a new terminal in the editor area."), localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.agentSessionsWelcomePage' }, "Open the Agent Sessions Welcome page."), ], - 'default': typeof product.quality === 'string' && product.quality !== 'stable' ? 'agentSessionsWelcomePage' : 'welcomePage', + 'default': 'welcomePage', 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") }, 'workbench.welcomePage.preferReducedMotion': { From 1643010a3bf93f117c0d724bf6cb62d525ee8732 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 24 Jan 2026 12:42:26 -0800 Subject: [PATCH 6/7] Render edits in subagent container (#290171) * Render edits in subagent container #286606 * Test fixes * Fix --- .../chatSubagentContentPart.ts | 92 +++++++- .../chat/browser/widget/chatListRenderer.ts | 17 +- .../chat/common/chatService/chatService.ts | 1 + .../tools/builtinTools/runSubagentTool.ts | 11 +- .../contrib/chat/common/widget/annotations.ts | 31 ++- .../common/widget/codeBlockModelCollection.ts | 4 + .../chatSubagentContentPart.test.ts | 199 +++++++++++++++++- .../test/common/widget/annotations.test.ts | 105 ++++++++- 8 files changed, 443 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index c015c2020aaea..f57073fdf25e2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -6,6 +6,7 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { $, AnimationFrameScheduler, DisposableResizeObserver } from '../../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { rcut } from '../../../../../../base/common/strings.js'; import { localize } from '../../../../../../nls.js'; @@ -17,7 +18,7 @@ import { ChatTreeItem } from '../../chat.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js'; import { ChatCollapsibleMarkdownContentPart } from './chatCollapsibleMarkdownContentPart.js'; -import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; +import { IChatMarkdownContent, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; import { IRunSubagentToolInputParams, RunSubagentTool } from '../../../common/tools/builtinTools/runSubagentTool.js'; import { autorun } from '../../../../../../base/common/observable.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; @@ -36,11 +37,22 @@ const MAX_TITLE_LENGTH = 100; * Represents a lazy tool item that will be created when the subagent section is expanded. */ interface ILazyToolItem { + kind: 'tool'; lazy: Lazy; toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized; codeBlockStartIndex: number; } +/** + * Represents a lazy markdown item (e.g., edit pill) that will be rendered when expanded. + */ +interface ILazyMarkdownItem { + kind: 'markdown'; + lazy: Lazy<{ domNode: HTMLElement; disposable?: IDisposable }>; +} + +type ILazyItem = ILazyToolItem | ILazyMarkdownItem; + /** * This is generally copied from ChatThinkingContentPart. We are still experimenting with both UIs so I'm not * trying to refactor to share code. Both could probably be simplified when stable. @@ -59,7 +71,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen private prompt: string | undefined; // Lazy rendering support - private readonly lazyItems: ILazyToolItem[] = []; + private readonly lazyItems: ILazyItem[] = []; private hasExpandedOnce: boolean = false; private pendingPromptRender: boolean = false; private pendingResultText: string | undefined; @@ -500,6 +512,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } else { // Defer rendering until expanded const item: ILazyToolItem = { + kind: 'tool', lazy: new Lazy(() => this.createToolPart(toolInvocation, codeBlockStartIndex)), toolInvocation, codeBlockStartIndex, @@ -508,6 +521,61 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } } + /** + * Appends a markdown item (e.g., an edit pill) to the subagent content part. + * This is used to route codeblockUri parts with subAgentInvocationId to this subagent's container. + */ + public appendMarkdownItem( + factory: () => { domNode: HTMLElement; disposable?: IDisposable }, + _codeblocksPartId: string | undefined, + _markdown: IChatMarkdownContent, + _originalParent?: HTMLElement + ): void { + // If expanded or has been expanded once, render immediately + if (this.isExpanded() || this.hasExpandedOnce) { + const result = factory(); + this.appendMarkdownItemToDOM(result.domNode); + if (result.disposable) { + this._register(result.disposable); + } + } else { + // Defer rendering until expanded + const item: ILazyMarkdownItem = { + kind: 'markdown', + lazy: new Lazy(factory), + }; + this.lazyItems.push(item); + } + } + + /** + * Appends a markdown item's DOM node to the wrapper. + */ + private appendMarkdownItemToDOM(domNode: HTMLElement): void { + if (!domNode.hasChildNodes() || domNode.textContent?.trim() === '') { + return; + } + + // Wrap with icon like other items + const itemWrapper = $('.chat-thinking-tool-wrapper'); + const iconElement = createThinkingIcon(Codicon.edit); + itemWrapper.appendChild(domNode); + itemWrapper.insertBefore(iconElement, itemWrapper.firstChild); + + // Insert before result container if it exists, otherwise append + if (this.wrapper) { + if (this.resultContainer) { + this.wrapper.insertBefore(itemWrapper, this.resultContainer); + } else { + this.wrapper.appendChild(itemWrapper); + } + } + this.lastItemWrapper = itemWrapper; + + // Schedule layout to measure last item and scroll + this.layoutScheduler.schedule(); + } + protected override shouldInitEarly(): boolean { // Never init early - subagent is collapsed while running, content only shown on expand return false; @@ -582,15 +650,23 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } /** - * Materializes a lazy tool item by creating the tool part and adding it to the DOM. + * Materializes a lazy item by creating the content and adding it to the DOM. */ - private materializeLazyItem(item: ILazyToolItem): void { + private materializeLazyItem(item: ILazyItem): void { if (item.lazy.hasValue) { return; // Already materialized } - const part = item.lazy.value; - this.appendToolPartToDOM(part, item.toolInvocation); + if (item.kind === 'tool') { + const part = item.lazy.value; + this.appendToolPartToDOM(part, item.toolInvocation); + } else if (item.kind === 'markdown') { + const result = item.lazy.value; + this.appendMarkdownItemToDOM(result.domNode); + if (result.disposable) { + this._register(result.disposable); + } + } } /** @@ -640,6 +716,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], _element: ChatTreeItem): boolean { + if (other.kind === 'markdownContent') { + return true; + } + // Match subagent tool invocations with the same subAgentInvocationId to keep them grouped if ((other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') && (other.subAgentInvocationId || other.toolId === RunSubagentTool.Id)) { // For runSubagent tool, use toolCallId as the effective ID diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 59887e7599c40..e10f47e4ca002 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -48,7 +48,7 @@ import { IThemeService } from '../../../../../platform/theme/common/themeService import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { IWorkbenchIssueService } from '../../../issue/common/issue.js'; import { CodiconActionViewItem } from '../../../notebook/browser/view/cellParts/cellActionView.js'; -import { annotateSpecialMarkdownContent, hasCodeblockUriTag } from '../../common/widget/annotations.js'; +import { annotateSpecialMarkdownContent, extractSubAgentInvocationIdFromText, hasCodeblockUriTag } from '../../common/widget/annotations.js'; import { checkModeOption } from '../../common/chat.js'; import { IChatAgentMetadata } from '../../common/participants/chatAgents.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; @@ -1939,6 +1939,21 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer ({ domNode: markdownPart.domNode, disposable: markdownPart }), + markdownPart.codeblocksPartId, + markdown, + templateData.value + ); + return subagentPart; + } + } + // create thinking part if it doesn't exist yet const lastThinking = this.getLastThinkingPart(templateData.renderedParts); if (!lastThinking && markdownPart?.domNode && this.shouldPinPart(markdown, context.element) && collapsedToolsMode === CollapsedToolsDisplayMode.Always && isComplete) { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index b5a13cea9734d..40d74b4b25249 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -272,6 +272,7 @@ export interface IChatResponseCodeblockUriPart { uri: URI; isEdit?: boolean; undoStopId?: string; + subAgentInvocationId?: string; } export interface IChatAgentMarkdownContentWithVulnerability { diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 45e8043847b07..646f3d935169f 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -7,6 +7,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js import { Codicon } from '../../../../../../base/common/codicons.js'; import { Event } from '../../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { generateUuid } from '../../../../../../base/common/uuid.js'; import { IJSONSchema, IJSONSchemaMap } from '../../../../../../base/common/jsonSchema.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; @@ -187,6 +188,9 @@ export class RunSubagentTool extends Disposable implements IToolImpl { // Track whether we should collect markdown (after the last tool invocation) const markdownParts: string[] = []; + // Generate a stable subAgentInvocationId for routing edits to this subagent's content part + const subAgentInvocationId = invocation.callId ?? `subagent-${generateUuid()}`; + let inEdit = false; const progressCallback = (parts: IChatProgress[]) => { for (const part of parts) { @@ -196,7 +200,12 @@ export class RunSubagentTool extends Disposable implements IToolImpl { inEdit = true; model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('```\n') }); } - model.acceptResponseProgress(request, part); + // Attach subAgentInvocationId to codeblockUri parts so they can be routed to the subagent content part + if (part.kind === 'codeblockUri') { + model.acceptResponseProgress(request, { ...part, subAgentInvocationId }); + } else { + model.acceptResponseProgress(request, part); + } // When we see a tool invocation starting, reset markdown collection if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') { diff --git a/src/vs/workbench/contrib/chat/common/widget/annotations.ts b/src/vs/workbench/contrib/chat/common/widget/annotations.ts index a1dbed9eb8968..c22c60ca4302b 100644 --- a/src/vs/workbench/contrib/chat/common/widget/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/widget/annotations.ts @@ -60,7 +60,8 @@ export function annotateSpecialMarkdownContent(response: Iterable${item.uri.toString()}`; + const subAgentText = item.subAgentInvocationId ? ` subAgentInvocationId="${encodeURIComponent(item.subAgentInvocationId)}"` : ''; + const markdownText = `${item.uri.toString()}`; const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); // delete the previous and append to ensure that we don't reorder the edit before the undo stop containing it result.splice(previousItemIndex, 1); @@ -79,14 +80,34 @@ export interface IMarkdownVulnerability { readonly description: string; readonly range: IRange; } -export function extractCodeblockUrisFromText(text: string): { uri: URI; isEdit?: boolean; textWithoutResult: string } | undefined { - const match = /(.*?)<\/vscode_codeblock_uri>/ms.exec(text); +export function extractCodeblockUrisFromText(text: string): { uri: URI; isEdit?: boolean; subAgentInvocationId?: string; textWithoutResult: string } | undefined { + const match = /([\s\S]*?)<\/vscode_codeblock_uri>/ms.exec(text); if (match) { - const [all, isEdit, uriString] = match; + const [all, isEdit, , encodedSubAgentId, uriString] = match; if (uriString) { const result = URI.parse(uriString); const textWithoutResult = text.substring(0, match.index) + text.substring(match.index + all.length); - return { uri: result, textWithoutResult, isEdit: !!isEdit }; + let subAgentInvocationId: string | undefined; + if (encodedSubAgentId) { + try { + subAgentInvocationId = decodeURIComponent(encodedSubAgentId); + } catch { + subAgentInvocationId = encodedSubAgentId; + } + } + return { uri: result, textWithoutResult, isEdit: !!isEdit, subAgentInvocationId }; + } + } + return undefined; +} + +export function extractSubAgentInvocationIdFromText(text: string): string | undefined { + const match = /]* subAgentInvocationId="([^"]*)"/ms.exec(text); + if (match) { + try { + return decodeURIComponent(match[1]); + } catch { + return match[1]; } } return undefined; diff --git a/src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts index b6396b2105047..5236ff30add5e 100644 --- a/src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts @@ -29,6 +29,7 @@ export interface CodeBlockEntry { readonly vulns: readonly IMarkdownVulnerability[]; readonly codemapperUri?: URI; readonly isEdit?: boolean; + readonly subAgentInvocationId?: string; } export class CodeBlockModelCollection extends Disposable { @@ -39,6 +40,7 @@ export class CodeBlockModelCollection extends Disposable { inLanguageId: string | undefined; codemapperUri?: URI; isEdit?: boolean; + subAgentInvocationId?: string; }>(); /** @@ -85,6 +87,7 @@ export class CodeBlockModelCollection extends Disposable { vulns: entry.vulns, codemapperUri: entry.codemapperUri, isEdit: entry.isEdit, + subAgentInvocationId: entry.subAgentInvocationId, }; } @@ -195,6 +198,7 @@ export class CodeBlockModelCollection extends Disposable { if (entry) { entry.codemapperUri = codeblockUri.uri; entry.isEdit = codeblockUri.isEdit; + entry.subAgentInvocationId = codeblockUri.subAgentInvocationId; } newText = codeblockUri.textWithoutResult; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts index 7a58f61d8d9e5..2884683f1a6d5 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts @@ -509,7 +509,7 @@ suite('ChatSubagentContentPart', () => { assert.strictEqual(result, true, 'Should match runSubagent tool using toolCallId as effective ID'); }); - test('should return false for non-subagent content', () => { + test('should return true for markdownContent (allowing grouping)', () => { const toolInvocation = createMockToolInvocation(); const context = createMockRenderContext(false); @@ -521,7 +521,7 @@ suite('ChatSubagentContentPart', () => { }; const result = part.hasSameContent(markdownContent, [], context.element); - assert.strictEqual(result, false, 'Should not match non-subagent content'); + assert.strictEqual(result, true, 'Should match markdownContent to allow grouping'); }); }); @@ -895,6 +895,201 @@ suite('ChatSubagentContentPart', () => { }); }); + suite('appendMarkdownItem', () => { + test('should append markdown item to expanded subagent part', () => { + const toolInvocation = createMockToolInvocation({ + subAgentInvocationId: 'test-subagent-id', + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Expand the part first + const button = getCollapseButton(part); + button?.click(); + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, 'Should be expanded'); + + // Create a mock markdown content with edit pill + const markdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: { value: 'Edited file.ts' } + }; + + // Create a mock DOM node for the markdown + const markdownDomNode = mainWindow.document.createElement('div'); + markdownDomNode.className = 'chat-codeblock-button'; + markdownDomNode.textContent = 'file.ts'; + + let disposeCallCount = 0; + const mockDisposable = { dispose: () => { disposeCallCount++; } }; + + // Append markdown item + part.appendMarkdownItem( + () => ({ domNode: markdownDomNode, disposable: mockDisposable }), + 'codeblock-123', + markdownContent, + undefined + ); + + // Verify the markdown was appended + const wrapper = getWrapperElement(part); + assert.ok(wrapper, 'Wrapper should exist'); + const appendedElement = wrapper.querySelector('.chat-codeblock-button'); + assert.ok(appendedElement, 'Appended markdown element should exist in wrapper'); + assert.strictEqual(appendedElement.textContent, 'file.ts', 'Should have correct content'); + }); + + test('should not render markdown item when part is collapsed', () => { + const toolInvocation = createMockToolInvocation({ + subAgentInvocationId: 'test-subagent-defer', + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Part is collapsed by default + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should start collapsed'); + + const markdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: { value: 'Deferred edit' } + }; + + let factoryCalled = false; + const markdownDomNode = mainWindow.document.createElement('div'); + markdownDomNode.className = 'deferred-edit'; + markdownDomNode.textContent = 'deferred.ts'; + + const mockDisposable = { dispose: () => { } }; + + // Append markdown item while collapsed - factory should not be called + part.appendMarkdownItem( + () => { + factoryCalled = true; + return { domNode: markdownDomNode, disposable: mockDisposable }; + }, + 'codeblock-deferred', + markdownContent, + undefined + ); + + // Factory should not be called when collapsed + assert.strictEqual(factoryCalled, false, 'Factory should not be called when collapsed'); + }); + + test('should append multiple markdown items with same codeblock ID', () => { + const toolInvocation = createMockToolInvocation({ + subAgentInvocationId: 'test-subagent-dedup', + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Expand the part + const button = getCollapseButton(part); + button?.click(); + + const markdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: { value: 'Same codeblock' } + }; + + const sharedCodeblockId = 'codeblock-same-id'; + + // Append first item + const firstNode = mainWindow.document.createElement('div'); + firstNode.className = 'first-item'; + firstNode.textContent = 'first item content'; + part.appendMarkdownItem( + () => ({ domNode: firstNode, disposable: { dispose: () => { } } }), + sharedCodeblockId, + markdownContent, + undefined + ); + + // Append second item with same codeblock ID + const secondNode = mainWindow.document.createElement('div'); + secondNode.className = 'second-item'; + secondNode.textContent = 'second item content'; + part.appendMarkdownItem( + () => ({ domNode: secondNode, disposable: { dispose: () => { } } }), + sharedCodeblockId, + markdownContent, + undefined + ); + + // Both items are added (no built-in deduplication by codeblock ID) + const wrapper = getWrapperElement(part); + assert.ok(wrapper, 'Wrapper should exist'); + const firstItems = wrapper.querySelectorAll('.first-item'); + const secondItems = wrapper.querySelectorAll('.second-item'); + // Implementation does not deduplicate - both items exist + assert.strictEqual(firstItems.length, 1, 'First item should exist'); + assert.strictEqual(secondItems.length, 1, 'Second item should exist'); + }); + + test('should handle multiple different codeblock IDs', () => { + const toolInvocation = createMockToolInvocation({ + subAgentInvocationId: 'test-subagent-multi', + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Expand the part + const button = getCollapseButton(part); + button?.click(); + + // Append first item + const firstNode = mainWindow.document.createElement('div'); + firstNode.className = 'item-one'; + firstNode.textContent = 'first item content'; + part.appendMarkdownItem( + () => ({ domNode: firstNode, disposable: { dispose: () => { } } }), + 'codeblock-1', + { kind: 'markdownContent', content: { value: 'First' } }, + undefined + ); + + // Append second item with different ID + const secondNode = mainWindow.document.createElement('div'); + secondNode.className = 'item-two'; + secondNode.textContent = 'second item content'; + part.appendMarkdownItem( + () => ({ domNode: secondNode, disposable: { dispose: () => { } } }), + 'codeblock-2', + { kind: 'markdownContent', content: { value: 'Second' } }, + undefined + ); + + // Both should exist + const wrapper = getWrapperElement(part); + assert.ok(wrapper, 'Wrapper should exist'); + assert.ok(wrapper.querySelector('.item-one'), 'First item should exist'); + assert.ok(wrapper.querySelector('.item-two'), 'Second item should exist'); + }); + }); + suite('Auto-expand on confirmation', () => { test('should auto-expand when tool state becomes WaitingForConfirmation', () => { const toolInvocation = createMockToolInvocation({ diff --git a/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts b/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts index 18e36131d3d09..65e1db898f4e9 100644 --- a/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { URI } from '../../../../../../base/common/uri.js'; import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { IChatMarkdownContent } from '../../../common/chatService/chatService.js'; -import { annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from '../../../common/widget/annotations.js'; +import { IChatMarkdownContent, IChatResponseCodeblockUriPart } from '../../../common/chatService/chatService.js'; +import { annotateSpecialMarkdownContent, extractCodeblockUrisFromText, extractSubAgentInvocationIdFromText, extractVulnerabilitiesFromText } from '../../../common/widget/annotations.js'; function content(str: string): IChatMarkdownContent { return { kind: 'markdownContent', content: new MarkdownString(str) }; @@ -58,4 +60,103 @@ suite('Annotations', function () { await assertSnapshot(result); }); }); + + suite('extractSubAgentInvocationIdFromText', () => { + test('extracts subAgentInvocationId from codeblock uri tag', () => { + const subAgentId = 'test-agent-123'; + const uri = URI.parse('file:///test.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true, + subAgentInvocationId: subAgentId + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const result = extractSubAgentInvocationIdFromText(markdown.content.value); + assert.strictEqual(result, subAgentId); + }); + + test('returns undefined when no subAgentInvocationId', () => { + const uri = URI.parse('file:///test.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const result = extractSubAgentInvocationIdFromText(markdown.content.value); + assert.strictEqual(result, undefined); + }); + + test('returns undefined for text without codeblock uri tag', () => { + const result = extractSubAgentInvocationIdFromText('some random text'); + assert.strictEqual(result, undefined); + }); + + test('handles special characters in subAgentInvocationId via URL encoding', () => { + const subAgentId = 'agent-with-special&chars=value'; + const uri = URI.parse('file:///test.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true, + subAgentInvocationId: subAgentId + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const result = extractSubAgentInvocationIdFromText(markdown.content.value); + assert.strictEqual(result, subAgentId); + }); + + test('handles malformed URL encoding gracefully', () => { + // Manually construct a malformed tag with invalid URL encoding + const malformedTag = 'file:///test.ts'; + const result = extractSubAgentInvocationIdFromText(malformedTag); + // Should return the raw value when decoding fails + assert.strictEqual(result, '%ZZ'); + }); + }); + + suite('extractCodeblockUrisFromText with subAgentInvocationId', () => { + test('extracts subAgentInvocationId from codeblock uri', () => { + const subAgentId = 'test-subagent-456'; + const uri = URI.parse('file:///example.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true, + subAgentInvocationId: subAgentId + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const result = extractCodeblockUrisFromText(markdown.content.value); + assert.ok(result); + assert.strictEqual(result.subAgentInvocationId, subAgentId); + assert.strictEqual(result.uri.toString(), uri.toString()); + assert.strictEqual(result.isEdit, true); + }); + + test('round-trip encoding/decoding with special characters', () => { + const subAgentId = 'agent/with spaces&special=chars?more'; + const uri = URI.parse('file:///path/to/file.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true, + subAgentInvocationId: subAgentId + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const extracted = extractCodeblockUrisFromText(markdown.content.value); + assert.ok(extracted); + assert.strictEqual(extracted.subAgentInvocationId, subAgentId); + }); + }); }); From b7ec2ded679714c543356e61f7c41da6ad817682 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 24 Jan 2026 14:03:24 -0800 Subject: [PATCH 7/7] Fix shifting/line-height changes in rendered tools (#290179) Fix #274430 We are applying line-height and font-size in an inconsistent way based on ems that leads to these being rendered differently when they are running vs complete. This tries to align them, but it's quite complicated due to tons of overlapping css rules. --- .../widget/chatContentParts/media/chatThinkingContent.css | 2 +- .../workbench/contrib/chat/browser/widget/media/chat.css | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index 9bf3894f258ef..a2fb8b710b1c4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -242,5 +242,5 @@ .interactive-session .interactive-response .value .chat-thinking-box .chat-used-context-label code { display: inline; line-height: inherit; - padding: 0 4px; + padding: 1px 3px; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index a4010246d0484..9521631d75588 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -2076,6 +2076,7 @@ have to be updated for changes to the rules above, or to support more deeply nes display: flex; flex-direction: column; gap: 2px; + line-height: 1.5em; } .interactive-response-progress-tree, @@ -2173,6 +2174,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-used-context-label .monaco-button { padding: 2px 6px 2px 2px; font-size: var(--vscode-chat-font-size-body-m); + line-height: unset; } .interactive-session .chat-file-changes-label .monaco-button:hover { @@ -2204,8 +2206,8 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-item-container .progress-container { display: flex; align-items: center; - gap: 7px; - margin: 0 0 6px 4px; + gap: 4px; + margin: 0 0 6px 2px; /* Tool calls transition from a progress to a collapsible list part, which needs to have this top padding. The working progress also can be replaced by a tool progress part. So align this padding so the text doesn't appear to shift. */ @@ -2222,6 +2224,8 @@ have to be updated for changes to the rules above, or to support more deeply nes .codicon { /* Very aggressive list styles try to apply focus colors to every codicon in a list row. */ color: var(--vscode-icon-foreground) !important; + /* Matching the margin on all .monaco-text-button .codicon */ + margin: 0 .2em; &.codicon-error { color: var(--vscode-editorError-foreground) !important;