From bea5cc48dca619f24475206e4da23812facea353 Mon Sep 17 00:00:00 2001 From: SukumarP Date: Fri, 19 Jun 2026 12:30:07 +0530 Subject: [PATCH 1/3] Display model name in subagent session --- .../chatCollapsibleContentPart.ts | 2 +- .../chatSubagentContentPart.ts | 55 ++++- .../media/chatSubagentContent.css | 6 + .../chatSubagentContentPart.test.ts | 222 ++++++++++++++++++ 4 files changed, 278 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts index 0d171b8eb16c88..6347062a5ff15b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts @@ -143,7 +143,7 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I abstract hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean; - private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { + protected updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { element.ariaLabel = label; element.ariaExpanded = String(expanded); } 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 7e2385f926c06a..ab93b4a3ac795f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -103,10 +103,11 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen // Current tool message for collapsed title (persists even after tool completes) private currentRunningToolMessage: string | undefined; - // Model name used by this subagent for hover tooltip + // Model name used by this subagent private modelName: string | undefined; private _isDefaultDescription: boolean; private readonly _hoverDisposable = this._register(new MutableDisposable()); + private titleModelSpan: HTMLElement | undefined; // Confirmation auto-expand tracking private toolsWaitingForConfirmation: number = 0; @@ -283,6 +284,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen // Set up hover tooltip with model name if available this.updateHover(); + this.updateTitle(); // Render the prompt section at the start if available (must be after wrapper is initialized) this.renderPromptSection(); @@ -490,6 +492,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen if (!this.isActive) { labelElement.textContent = ''; this.titleShimmerSpan = undefined; + this.titleModelSpan = undefined; this._titleDetailRendered.clear(); this._titleFileWidgetStore.clear(); @@ -503,7 +506,13 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen descSpan.textContent = ` ${this.description}`; labelElement.appendChild(descSpan); - this._collapseButton.element.ariaLabel = shimmerText; + if (this.modelName) { + this.titleModelSpan = $('span.chat-subagent-model-label'); + this.titleModelSpan.textContent = ` \u2014 ${this.modelName}`; + labelElement.appendChild(this.titleModelSpan); + } + + this._collapseButton.element.ariaLabel = this.getTitleAriaLabel(); this._collapseButton.element.ariaExpanded = String(this.isExpanded()); return; } @@ -513,9 +522,25 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen labelElement.textContent = ''; this.titleShimmerSpan = $('span.chat-thinking-title-shimmer'); labelElement.appendChild(this.titleShimmerSpan); + this.titleModelSpan = undefined; + this.titleDetailContainer = undefined; } this.titleShimmerSpan.textContent = shimmerText; + // Show model name inline during active state if available + if (this.modelName) { + if (this.titleModelSpan) { + this.titleModelSpan.textContent = ` \u2014 ${this.modelName}`; + } else { + this.titleModelSpan = $('span.chat-subagent-model-label'); + this.titleModelSpan.textContent = ` \u2014 ${this.modelName}`; + this.titleShimmerSpan.after(this.titleModelSpan); + } + } else if (this.titleModelSpan) { + this.titleModelSpan.remove(); + this.titleModelSpan = undefined; + } + // Dispose previous detail rendering this._titleDetailRendered.clear(); this._titleFileWidgetStore.clear(); @@ -539,11 +564,24 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.titleDetailContainer = result.element; } - const fullLabel = `${shimmerText}${toolCallText}`; - this._collapseButton.element.ariaLabel = fullLabel; + this._collapseButton.element.ariaLabel = this.getTitleAriaLabel(); this._collapseButton.element.ariaExpanded = String(this.isExpanded()); } + protected override updateAriaLabel(element: HTMLElement, _label: string, expanded?: boolean): void { + element.ariaLabel = this.getTitleAriaLabel(); + element.ariaExpanded = String(expanded); + } + + private getTitleAriaLabel(): string { + const rawName = this.agentName || localize('chat.subagent.prefix', 'Subagent'); + const prefix = rawName.charAt(0).toUpperCase() + rawName.slice(1); + const modelText = this.modelName ? ` \u2014 ${this.modelName}` : ''; + const toolCallText = this.currentRunningToolMessage && this.isActive ? ` \u2014 ${this.currentRunningToolMessage}` : ``; + + return `${prefix}: ${this.description}${modelText}${toolCallText}`; + } + private updateHover(): void { if (!this.modelName || !this._collapseButton) { return; @@ -754,10 +792,11 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen // after the part was first constructed in PendingConfirmation). // Re-read metadata and update the title if real values are // now available that we didn't have before. - const { description, isDefaultDescription, agentName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); + const { description, isDefaultDescription, agentName, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); const descriptionChanged = this._isDefaultDescription && !isDefaultDescription; const agentNameChanged = !!agentName && agentName !== this.agentName; - if (descriptionChanged || agentNameChanged) { + const modelNameChanged = !!modelName && modelName !== this.modelName; + if (descriptionChanged || agentNameChanged || modelNameChanged) { if (descriptionChanged) { this.description = description; this._isDefaultDescription = isDefaultDescription; @@ -765,6 +804,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen if (agentNameChanged) { this.agentName = agentName; } + if (modelNameChanged) { + this.modelName = modelName; + this.updateHover(); + } this.updateTitle(); } } 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 1db44f94d05b30..4509eae06d19fa 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 @@ -19,6 +19,12 @@ overflow: visible; } + /* Model label shown in subagent title */ + .chat-subagent-model-label { + color: var(--vscode-descriptionForeground); + font-weight: normal; + } + /* Prompt and result section styling */ .chat-subagent-section { padding: 4px 12px 4px 18px; 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 69a1b1fdcef2bf..18b81ec5219eca 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 @@ -1615,5 +1615,227 @@ suite('ChatSubagentContentPart', () => { const modelHover = setupDelayedHoverCalls.find(c => c.content.includes('Claude Sonnet 4')); assert.ok(modelHover, 'Should set up hover with model name after completion'); }); + + test('should show model name in title for serialized invocations', () => { + const serializedInvocation = createMockSerializedToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Completed task', + agentName: 'TestAgent', + prompt: 'Do the thing', + result: 'Done', + modelName: 'GPT-4o' + } + }); + const context = createMockRenderContext(true); + + const part = createPart(serializedInvocation, context); + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + + const label = getCollapseButtonLabel(button); + assert.ok(label, 'Should have label'); + + const modelLabel = label.querySelector('.chat-subagent-model-label'); + assert.ok(modelLabel, 'Should have model label element in title'); + assert.ok(modelLabel.textContent?.includes('GPT-4o'), 'Model label should contain model name'); + }); + + test('should not show model name in title when no model name is available', () => { + const serializedInvocation = createMockSerializedToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Completed task', + agentName: 'TestAgent', + prompt: 'Do the thing', + result: 'Done', + // no modelName + } + }); + const context = createMockRenderContext(true); + + const part = createPart(serializedInvocation, context); + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + + const label = getCollapseButtonLabel(button); + assert.ok(label, 'Should have label'); + + const modelLabel = label.querySelector('.chat-subagent-model-label'); + assert.strictEqual(modelLabel, null, 'Should not have model label when no model name'); + }); + + test('should show model name in title after tool completion', () => { + const toolSpecificData: IChatSubagentToolInvocationData = { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent', + prompt: 'Do stuff', + }; + + const toolInvocation = createMockToolInvocation({ + toolSpecificData, + stateType: IChatToolInvocation.StateKind.Executing, + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + + // Simulate invoke() setting modelName on toolSpecificData + toolSpecificData.modelName = 'Claude Sonnet 4'; + + // Simulate tool completion + const state = toolInvocation.state as ReturnType>; + state.set(createState(IChatToolInvocation.StateKind.Completed), undefined); + + const label = getCollapseButtonLabel(button); + assert.ok(label, 'Should have label'); + + const modelLabel = label.querySelector('.chat-subagent-model-label'); + assert.ok(modelLabel, 'Should have model label element after completion'); + assert.ok(modelLabel.textContent?.includes('Claude Sonnet 4'), 'Model label should contain resolved model name'); + }); + + test('should include model name in aria-label for completed subagents', () => { + const serializedInvocation = createMockSerializedToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Completed task', + agentName: 'TestAgent', + prompt: 'Do the thing', + result: 'Done', + modelName: 'GPT-4o' + } + }); + const context = createMockRenderContext(true); + + const part = createPart(serializedInvocation, context); + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + + const ariaLabel = button.getAttribute('aria-label') ?? ''; + assert.ok(ariaLabel.includes('GPT-4o'), 'aria-label should include model name for completed subagent'); + }); + + test('should not duplicate model label on repeated updateTitle calls', () => { + const toolSpecificData: IChatSubagentToolInvocationData = { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent', + prompt: 'Do stuff', + modelName: 'Claude Sonnet 4', + }; + + const toolInvocation = createMockToolInvocation({ + toolSpecificData, + stateType: IChatToolInvocation.StateKind.Executing, + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Force multiple title updates by tracking tool invocations + const childTool = createMockToolInvocation({ + stateType: IChatToolInvocation.StateKind.Executing, + invocationMessage: 'Reading file', + }); + part.trackToolState(childTool); + + const childTool2 = createMockToolInvocation({ + stateType: IChatToolInvocation.StateKind.Executing, + invocationMessage: 'Writing file', + }); + part.trackToolState(childTool2); + + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + const label = getCollapseButtonLabel(button); + assert.ok(label, 'Should have label'); + + const modelLabels = label.querySelectorAll('.chat-subagent-model-label'); + assert.strictEqual(modelLabels.length, 1, 'Should have exactly one model label after repeated updates'); + }); + + test('should show model name in title for active invocations with initial modelName', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent', + prompt: 'Do stuff', + modelName: 'GPT-4o', + }, + stateType: IChatToolInvocation.StateKind.Executing, + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + + const label = getCollapseButtonLabel(button); + assert.ok(label, 'Should have label'); + const modelLabel = label.querySelector('.chat-subagent-model-label'); + assert.ok(modelLabel, 'Should have model label element while active'); + assert.ok(modelLabel.textContent?.includes('GPT-4o'), 'Model label should contain initial model name'); + + const ariaLabel = button.getAttribute('aria-label') ?? ''; + assert.ok(ariaLabel.includes('GPT-4o'), 'aria-label should include model name while active'); + }); + + test('should preserve model name in aria-label after expand and collapse', () => { + const serializedInvocation = createMockSerializedToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Completed task', + agentName: 'TestAgent', + prompt: 'Do the thing', + result: 'Done', + modelName: 'GPT-4o' + } + }); + const context = createMockRenderContext(true); + + const part = createPart(serializedInvocation, context); + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + + button.click(); + assert.ok(button.getAttribute('aria-label')?.includes('GPT-4o'), 'aria-label should include model name after expand'); + + button.click(); + assert.ok(button.getAttribute('aria-label')?.includes('GPT-4o'), 'aria-label should include model name after collapse'); + }); + + test('should show model name when active metadata updates later', () => { + const toolSpecificData: IChatSubagentToolInvocationData = { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent', + prompt: 'Do stuff', + }; + const toolInvocation = createMockToolInvocation({ + toolSpecificData, + stateType: IChatToolInvocation.StateKind.Executing, + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + + toolSpecificData.modelName = 'Claude Sonnet 4'; + const state = toolInvocation.state as ReturnType>; + state.set(createState(IChatToolInvocation.StateKind.Executing), undefined); + + const label = getCollapseButtonLabel(button); + assert.ok(label, 'Should have label'); + const modelLabel = label.querySelector('.chat-subagent-model-label'); + assert.ok(modelLabel, 'Should have model label element after metadata update'); + assert.ok(modelLabel.textContent?.includes('Claude Sonnet 4'), 'Model label should contain late model name'); + assert.ok(button.getAttribute('aria-label')?.includes('Claude Sonnet 4'), 'aria-label should include late model name'); + }); }); }); From aaa46d663fe17d7c6dc2fbfeb53fbb9bccdf184a Mon Sep 17 00:00:00 2001 From: SukumarP Date: Fri, 19 Jun 2026 12:54:06 +0530 Subject: [PATCH 2/3] fix: handle cleared modelName to prevent stale UI state Address review comment: modelNameChanged now detects when modelName is removed/cleared (not just when a new value arrives), and updateHover() properly disposes the hover tooltip when modelName becomes undefined. --- .../widget/chatContentParts/chatSubagentContentPart.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 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 ab93b4a3ac795f..537edd792eb5c6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -583,7 +583,12 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } private updateHover(): void { - if (!this.modelName || !this._collapseButton) { + if (!this._collapseButton) { + return; + } + + if (!this.modelName) { + this._hoverDisposable.clear(); return; } @@ -795,7 +800,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen const { description, isDefaultDescription, agentName, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); const descriptionChanged = this._isDefaultDescription && !isDefaultDescription; const agentNameChanged = !!agentName && agentName !== this.agentName; - const modelNameChanged = !!modelName && modelName !== this.modelName; + const modelNameChanged = modelName !== this.modelName; if (descriptionChanged || agentNameChanged || modelNameChanged) { if (descriptionChanged) { this.description = description; From 96705172a0251a8e728cf9fd91fe65f6d9505249 Mon Sep 17 00:00:00 2001 From: SukumarP Date: Fri, 19 Jun 2026 13:29:46 +0530 Subject: [PATCH 3/3] fix: update getTitleAriaLabel method in ChatSubagentContentPart --- .../widget/chatContentParts/chatSubagentContentPart.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 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 537edd792eb5c6..6eff37b0ec597f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -576,10 +576,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen private getTitleAriaLabel(): string { const rawName = this.agentName || localize('chat.subagent.prefix', 'Subagent'); const prefix = rawName.charAt(0).toUpperCase() + rawName.slice(1); - const modelText = this.modelName ? ` \u2014 ${this.modelName}` : ''; - const toolCallText = this.currentRunningToolMessage && this.isActive ? ` \u2014 ${this.currentRunningToolMessage}` : ``; + const modelFragment = this.modelName ? localize('chat.subagent.ariaLabel.modelFragment', " \u2014 {0}", this.modelName) : ''; + const toolFragment = (this.currentRunningToolMessage && this.isActive) ? localize('chat.subagent.ariaLabel.toolFragment', " \u2014 {0}", this.currentRunningToolMessage) : ''; - return `${prefix}: ${this.description}${modelText}${toolCallText}`; + return localize('chat.subagent.ariaLabel', "{0}: {1}{2}{3}", prefix, this.description, modelFragment, toolFragment); } private updateHover(): void {