Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}
Expand All @@ -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();
Expand All @@ -539,13 +564,31 @@ 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 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 localize('chat.subagent.ariaLabel', "{0}: {1}{2}{3}", prefix, this.description, modelFragment, toolFragment);
}

private updateHover(): void {
if (!this.modelName || !this._collapseButton) {
if (!this._collapseButton) {
return;
}

if (!this.modelName) {
this._hoverDisposable.clear();
return;
}

Expand Down Expand Up @@ -754,17 +797,22 @@ 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 !== this.modelName;
if (descriptionChanged || agentNameChanged || modelNameChanged) {
Comment thread
sukumarp2022 marked this conversation as resolved.
if (descriptionChanged) {
this.description = description;
this._isDefaultDescription = isDefaultDescription;
}
if (agentNameChanged) {
this.agentName = agentName;
}
if (modelNameChanged) {
this.modelName = modelName;
this.updateHover();
}
this.updateTitle();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof observableValue<IChatToolInvocation.State>>;
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<typeof observableValue<IChatToolInvocation.State>>;
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');
});
});
});