Subagents render as a single line, similar to 'collapsed' thinking mode (#290059)

* Subagents render as a single line, similar to 'collapsed' thinking mode

* Remove now-unused eslint disable directives
This commit is contained in:
Rob Lourens
2026-01-23 18:14:18 -08:00
committed by GitHub
parent 1fb6cf9f93
commit 1c41206f8a
16 changed files with 742 additions and 57 deletions

View File

@@ -2120,4 +2120,21 @@ export default tseslint.config(
'@typescript-eslint/consistent-generic-constructors': ['warn', 'constructor'],
}
},
// Allow querySelector/querySelectorAll in test files - it's acceptable for test assertions
{
files: [
'src/**/test/**/*.ts',
'extensions/**/test/**/*.ts',
],
rules: {
'no-restricted-syntax': [
'warn',
// Keep the Intl helper restriction even in tests
{
'selector': `NewExpression[callee.object.name='Intl']`,
'message': 'Use safeIntl helper instead for safe and lazy use of potentially expensive Intl methods.'
},
],
}
},
);

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */
import assert from 'assert';
import { fillInIncompleteTokens, renderMarkdown, renderAsPlaintext } from '../../browser/markdownRenderer.js';
import { IMarkdownString, MarkdownString } from '../../common/htmlContent.js';

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */
import assert from 'assert';
import { Sash, SashState } from '../../../../browser/ui/sash/sash.js';
import { IView, LayoutPriority, Sizing, SplitView } from '../../../../browser/ui/splitview/splitview.js';

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */
import assert from 'assert';
import { IIdentityProvider, IListVirtualDelegate } from '../../../../browser/ui/list/list.js';
import { AsyncDataTree, CompressibleAsyncDataTree, ITreeCompressionDelegate } from '../../../../browser/ui/tree/asyncDataTree.js';

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */
import assert from 'assert';
import { IIdentityProvider, IListVirtualDelegate } from '../../../../browser/ui/list/list.js';
import { ICompressedTreeNode } from '../../../../browser/ui/tree/compressedObjectTreeModel.js';

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */
import assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { DisposableStore } from '../../../../../base/common/lifecycle.js';

View File

@@ -546,7 +546,6 @@ suite('HoverService', () => {
hoverService.showAndFocusLastHover();
// Verify there is a hover in the DOM (it's a new hover instance)
// eslint-disable-next-line no-restricted-syntax
const hoverElements = mainWindow.document.querySelectorAll('.monaco-hover');
assert.ok(hoverElements.length > 0, 'A hover should be recreated and in the DOM');
@@ -554,10 +553,8 @@ suite('HoverService', () => {
hoverService.hideHover(true);
// Verify cleanup
// eslint-disable-next-line no-restricted-syntax
const remainingHovers = mainWindow.document.querySelectorAll('.monaco-hover');
assert.strictEqual(remainingHovers.length, 0, 'No hovers should remain in DOM after cleanup');
});
});
});

View File

@@ -26,6 +26,8 @@ import { CollapsibleListPool } from './chatReferencesContentPart.js';
import { EditorPool } from './chatContentCodePools.js';
import { CodeBlockModelCollection } from '../../../common/widget/codeBlockModelCollection.js';
import { ChatToolInvocationPart } from './toolInvocationParts/chatToolInvocationPart.js';
import { IChatMarkdownAnchorService } from './chatMarkdownAnchorService.js';
import { MarkdownString } from '../../../../../../base/common/htmlContent.js';
import './media/chatSubagentContent.css';
const MAX_TITLE_LENGTH = 100;
@@ -62,6 +64,14 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
private pendingPromptRender: boolean = false;
private pendingResultText: string | undefined;
// Current tool message for collapsed title (persists even after tool completes)
private currentRunningToolMessage: string | undefined;
// Confirmation auto-expand tracking
private toolsWaitingForConfirmation: number = 0;
private userManuallyExpanded: boolean = false;
private autoExpandedForConfirmation: boolean = false;
/**
* Extracts subagent info (description, agentName, prompt) from a tool invocation.
*/
@@ -108,6 +118,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
private readonly codeBlockModelCollection: CodeBlockModelCollection,
private readonly announcedToolProgressKeys: Set<string>,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IChatMarkdownAnchorService private readonly chatMarkdownAnchorService: IChatMarkdownAnchorService,
@IHoverService hoverService: IHoverService,
) {
// Extract description, agentName, and prompt from toolInvocation
@@ -135,8 +146,8 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
this._register(autorun(r => {
this.expanded.read(r);
if (this._collapseButton && this.wrapper) {
if (this.wrapper.classList.contains('chat-thinking-streaming') && !this.element.isComplete && this.isActive) {
if (this._collapseButton) {
if (!this.element.isComplete && this.isActive) {
this._collapseButton.icon = ThemeIcon.modify(Codicon.loading, 'spin');
} else {
this._collapseButton.icon = Codicon.check;
@@ -155,6 +166,27 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
// Start collapsed - fixed scrolling mode shows limited height when collapsed
this.setExpanded(false);
// Track user manual expansion
// If the user expands (not via auto-expand for confirmation), mark it as manual
// Only clear autoExpandedForConfirmation when user collapses, so re-expand is detected as manual
this._register(autorun(r => {
const expanded = this._isExpanded.read(r);
if (expanded) {
if (!this.autoExpandedForConfirmation) {
this.userManuallyExpanded = true;
}
} else {
// User collapsed - reset flags so next confirmation cycle can auto-collapse again
if (this.autoExpandedForConfirmation) {
this.autoExpandedForConfirmation = false;
}
// Reset manual expansion flag when user collapses, so future confirmation cycles can auto-collapse
if (this.userManuallyExpanded) {
this.userManuallyExpanded = false;
}
}
}));
// Scheduler for coalescing layout operations
this.layoutScheduler = this._register(new AnimationFrameScheduler(this.domNode, () => this.performLayout()));
@@ -166,17 +198,17 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
}
protected override initContent(): HTMLElement {
const baseClasses = '.chat-used-context-list.chat-thinking-collapsible';
const classes = this.isInitiallyComplete
? baseClasses
: `${baseClasses}.chat-thinking-streaming`;
this.wrapper = $(classes);
this.wrapper = $('.chat-used-context-list.chat-thinking-collapsible');
// Hide initially until there are tool calls
if (!this.hasToolItems) {
this.wrapper.style.display = 'none';
}
// Materialize any deferred content now that wrapper exists
// This handles the case where the subclass autorun ran before this base class autorun
this.materializePendingContent();
// Use ResizeObserver to trigger layout when wrapper content changes
const resizeObserver = this._register(new DisposableResizeObserver(() => this.layoutScheduler.schedule()));
this._register(resizeObserver.observe(this.wrapper));
@@ -186,15 +218,16 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
/**
* Renders the prompt as a collapsible section at the start of the content.
* If the subagent is initially complete (old/restored), this is deferred until expanded.
* If the wrapper doesn't exist yet (lazy init) or subagent is initially complete,
* this is deferred until expanded.
*/
private renderPromptSection(): void {
if (!this.prompt || this.promptContainer) {
return;
}
// Defer rendering for old completed subagents until expanded
if (this.isInitiallyComplete && !this.isExpanded() && !this.hasExpandedOnce) {
// Defer rendering when wrapper doesn't exist yet (lazy init) or for old completed subagents until expanded
if (!this.wrapper || (this.isInitiallyComplete && !this.isExpanded() && !this.hasExpandedOnce)) {
this.pendingPromptRender = true;
return;
}
@@ -245,6 +278,11 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
} else {
dom.append(this.wrapper, this.promptContainer);
}
// Show the container if it was hidden (no tool items yet)
if (this.wrapper.style.display === 'none') {
this.wrapper.style.display = '';
}
}
}
@@ -254,10 +292,6 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
public markAsInactive(): void {
this.isActive = false;
// With lazy rendering, wrapper may not be created yet if content hasn't been expanded
if (this.wrapper) {
this.wrapper.classList.remove('chat-thinking-streaming');
}
if (this._collapseButton) {
this._collapseButton.icon = Codicon.check;
}
@@ -274,11 +308,59 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
}
private updateTitle(): void {
if (this._collapseButton) {
const prefix = this.agentName || localize('chat.subagent.prefix', 'Subagent');
const finalLabel = `${prefix}: ${this.description}`;
this._collapseButton.label = finalLabel;
const prefix = this.agentName || localize('chat.subagent.prefix', 'Subagent');
let finalLabel = `${prefix}: ${this.description}`;
if (this.currentRunningToolMessage && this.isActive) {
finalLabel += ` \u2014 ${this.currentRunningToolMessage}`;
}
this.setTitleWithWidgets(new MarkdownString(finalLabel), this.instantiationService, this.chatMarkdownAnchorService, this.chatContentMarkdownRenderer);
}
/**
* Tracks a tool invocation's state for:
* 1. Updating the title with the current tool message (persists even after completion)
* 2. Auto-expanding when a tool is waiting for confirmation
* 3. Auto-collapsing when the confirmation is addressed
* This method is public to support testing.
*/
public trackToolState(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): void {
// Only track live tool invocations
if (toolInvocation.kind !== 'toolInvocation') {
return;
}
// Set the title immediately when tool is added - like thinking part does
const message = toolInvocation.invocationMessage;
const messageText = typeof message === 'string' ? message : message.value;
this.currentRunningToolMessage = messageText;
this.updateTitle();
let wasWaitingForConfirmation = false;
this._register(autorun(r => {
const state = toolInvocation.state.read(r);
// Track confirmation state changes
const isWaitingForConfirmation = state.type === IChatToolInvocation.StateKind.WaitingForConfirmation;
if (isWaitingForConfirmation && !wasWaitingForConfirmation) {
// Tool just started waiting for confirmation
this.toolsWaitingForConfirmation++;
if (!this.isExpanded()) {
this.autoExpandedForConfirmation = true;
this.setExpanded(true);
}
} else if (!isWaitingForConfirmation && wasWaitingForConfirmation) {
// Tool is no longer waiting for confirmation
this.toolsWaitingForConfirmation--;
if (this.toolsWaitingForConfirmation === 0 && this.autoExpandedForConfirmation && !this.userManuallyExpanded) {
// Auto-collapse only if we auto-expanded and user didn't manually expand
this.autoExpandedForConfirmation = false;
this.setExpanded(false);
}
}
wasWaitingForConfirmation = isWaitingForConfirmation;
}));
}
/**
@@ -329,15 +411,16 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
/**
* Renders the result text as a collapsible section.
* If the subagent is initially complete (old/restored), this is deferred until expanded.
* If the wrapper doesn't exist yet (lazy init) or subagent is initially complete,
* this is deferred until expanded.
*/
public renderResultText(resultText: string): void {
if (this.resultContainer || !resultText) {
return; // Already rendered or no content
}
// Defer rendering for old completed subagents until expanded
if (this.isInitiallyComplete && !this.isExpanded() && !this.hasExpandedOnce) {
// Defer rendering when wrapper doesn't exist yet (lazy init) or for old completed subagents until expanded
if (!this.wrapper || (this.isInitiallyComplete && !this.isExpanded() && !this.hasExpandedOnce)) {
this.pendingResultText = resultText;
return;
}
@@ -406,15 +489,15 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
}
}
// Render immediately if:
// - The section is expanded
// - It has been expanded once before
// - It's actively streaming (not an old completed subagent being restored)
if (this.isExpanded() || this.hasExpandedOnce || !this.isInitiallyComplete) {
// Track tool state for title updates and auto-expand/collapse on confirmation
this.trackToolState(toolInvocation);
// Render immediately only if already expanded or has been expanded before
if (this.isExpanded() || this.hasExpandedOnce) {
const part = this.createToolPart(toolInvocation, codeBlockStartIndex);
this.appendToolPartToDOM(part, toolInvocation);
} else {
// Defer rendering until expanded (for old completed subagents)
// Defer rendering until expanded
const item: ILazyToolItem = {
lazy: new Lazy(() => this.createToolPart(toolInvocation, codeBlockStartIndex)),
toolInvocation,
@@ -425,7 +508,8 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
}
protected override shouldInitEarly(): boolean {
return !this.isInitiallyComplete;
// Never init early - subagent is collapsed while running, content only shown on expand
return false;
}
/**
@@ -494,8 +578,15 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
/**
* Materializes all pending lazy content (prompt, tool items, result) when the section is expanded.
* This is called when first expanded, but the wrapper must exist (created by base class initContent).
*/
private materializePendingContent(): void {
// Wrapper may not be created yet if this autorun runs before the base class autorun
// that calls initContent(). In that case, initContent() will call this logic.
if (!this.wrapper) {
return;
}
// Render pending prompt section
if (this.pendingPromptRender) {
this.pendingPromptRender = false;

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */
import assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';
import { renderFileWidgets } from '../../../../browser/widget/chatContentParts/chatInlineAnchorWidget.js';
@@ -145,4 +143,3 @@ suite('ChatInlineAnchorWidget Metadata Validation', () => {
assert.ok(!widget, 'Widget should not be rendered for malformed URI');
});
});

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */ // Tests legitimately need querySelector/querySelectorAll for DOM assertions
import assert from 'assert';
import { mainWindow } from '../../../../../../../base/browser/window.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';

View File

@@ -95,6 +95,10 @@ suite('ChatSubagentContentPart', () => {
return {
type: IChatToolInvocation.StateKind.WaitingForConfirmation,
parameters,
confirmationMessages: {
title: 'Confirm action',
message: 'Are you sure you want to proceed?'
},
confirm: () => { }
};
case IChatToolInvocation.StateKind.WaitingForPostApproval:
@@ -117,13 +121,16 @@ suite('ChatSubagentContentPart', () => {
function createMockToolInvocation(options: {
toolId?: string;
toolCallId?: string;
subAgentInvocationId?: string;
toolSpecificData?: IChatSubagentToolInvocationData;
stateType?: IChatToolInvocation.StateKind;
parameters?: ToolInvocationParameters;
invocationMessage?: string;
} = {}): IChatToolInvocation {
const stateType = options.stateType ?? IChatToolInvocation.StateKind.Streaming;
const stateValue = createState(stateType, options.parameters);
const toolCallId = options.toolCallId ?? 'tool-call-' + Math.random().toString(36).substring(7);
const toolInvocation: IChatToolInvocation = {
presentation: undefined,
@@ -134,11 +141,11 @@ suite('ChatSubagentContentPart', () => {
prompt: 'Test prompt'
},
originMessage: undefined,
invocationMessage: 'Running subagent...',
invocationMessage: options.invocationMessage ?? 'Running subagent...',
pastTenseMessage: undefined,
source: ToolDataSource.Internal,
toolId: options.toolId ?? RunSubagentTool.Id,
toolCallId: options.subAgentInvocationId ?? 'test-tool-call-id',
toolCallId: toolCallId,
subAgentInvocationId: options.subAgentInvocationId ?? 'test-subagent-id',
state: observableValue('state', stateValue),
kind: 'toolInvocation',
@@ -482,8 +489,10 @@ suite('ChatSubagentContentPart', () => {
});
test('should return true for runSubagent tool using toolCallId as effective ID', () => {
const sharedToolCallId = 'shared-tool-call-id';
const toolInvocation = createMockToolInvocation({
toolId: RunSubagentTool.Id,
toolCallId: sharedToolCallId,
subAgentInvocationId: 'call-abc'
});
const context = createMockRenderContext(false);
@@ -492,6 +501,7 @@ suite('ChatSubagentContentPart', () => {
const otherInvocation = createMockToolInvocation({
toolId: RunSubagentTool.Id,
toolCallId: sharedToolCallId,
subAgentInvocationId: 'call-abc'
});
@@ -575,4 +585,598 @@ suite('ChatSubagentContentPart', () => {
assert.strictEqual(button.getAttribute('aria-expanded'), 'true', 'Should have aria-expanded="true" when expanded');
});
});
suite('Lazy rendering', () => {
test('should defer prompt/result rendering until expanded when initially complete', () => {
const serializedInvocation = createMockSerializedToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Completed task',
agentName: 'FinishedAgent',
prompt: 'Original prompt for the task',
result: 'Task completed successfully'
}
});
const context = createMockRenderContext(true); // isComplete = true
const part = createPart(serializedInvocation, context);
// Content should be collapsed - no wrapper content initially visible
// Just verify that the domNode has the collapsed class
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed initially');
// Expand to trigger lazy rendering
const button = getCollapseButton(part);
assert.ok(button, 'Expand button should exist');
button.click();
// After expanding, the content containers should be rendered
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, 'Should be expanded');
// Verify prompt and result sections exist in the expanded content
const wrapperContent = part.domNode.querySelector('.chat-used-context-list');
assert.ok(wrapperContent, 'Wrapper content should exist after expand');
// Check that sections were inserted
const sections = wrapperContent.querySelectorAll('.chat-subagent-section');
assert.ok(sections.length >= 2, 'Should have prompt and result sections after expand');
});
test('should not render wrapper content while subagent is running (truly collapsed)', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Running task',
agentName: 'RunningAgent',
prompt: 'Prompt text'
},
stateType: IChatToolInvocation.StateKind.Streaming
});
const context = createMockRenderContext(false); // Not complete
const part = createPart(toolInvocation, context);
// Should be collapsed with just the title visible
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed while running');
// Wrapper content should not be initialized yet (lazy)
const wrapperContent = part.domNode.querySelector('.chat-used-context-list');
assert.strictEqual(wrapperContent, null, 'Wrapper content should not be rendered while running and collapsed');
});
test('should show prompt on expand when no tool items yet', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Starting task',
agentName: 'RunningAgent',
prompt: 'This is the prompt to execute'
},
stateType: IChatToolInvocation.StateKind.Streaming
});
const context = createMockRenderContext(false); // Not complete
const part = createPart(toolInvocation, context);
// Initially collapsed with no content
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed initially');
let wrapperContent = part.domNode.querySelector('.chat-used-context-list');
assert.strictEqual(wrapperContent, null, 'Wrapper should not exist initially');
// Expand
const button = getCollapseButton(part);
assert.ok(button, 'Expand button should exist');
button.click();
// Wrapper should now exist and be visible
wrapperContent = part.domNode.querySelector('.chat-used-context-list');
assert.ok(wrapperContent, 'Wrapper should exist after expand');
// Prompt section should be rendered
const promptSection = wrapperContent.querySelector('.chat-subagent-section');
assert.ok(promptSection, 'Prompt section should be visible after expand');
});
});
suite('Current running tool in title', () => {
test('should update title with current running tool invocation message', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// Add a child tool invocation
const childTool = createMockToolInvocation({
toolId: 'readFile',
subAgentInvocationId: toolInvocation.subAgentInvocationId,
stateType: IChatToolInvocation.StateKind.Executing,
invocationMessage: 'Reading config.ts'
});
part.appendToolInvocation(childTool, 0);
// The title should include the current running tool message
const button = getCollapseButton(part);
assert.ok(button, 'Should have collapse button');
const labelElement = getCollapseButtonLabel(button);
const buttonText = labelElement?.textContent ?? button.textContent ?? '';
assert.ok(buttonText.includes('Reading config.ts'), 'Title should include current running tool message');
});
test('should show latest tool when multiple tools are added', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// Add first tool
const firstTool = createMockToolInvocation({
toolId: 'readFile',
subAgentInvocationId: toolInvocation.subAgentInvocationId,
stateType: IChatToolInvocation.StateKind.Executing,
invocationMessage: 'Reading file1.ts'
});
part.appendToolInvocation(firstTool, 0);
// Add second tool
const secondTool = createMockToolInvocation({
toolId: 'searchFiles',
subAgentInvocationId: toolInvocation.subAgentInvocationId,
stateType: IChatToolInvocation.StateKind.Executing,
invocationMessage: 'Searching for patterns'
});
part.appendToolInvocation(secondTool, 1);
const button = getCollapseButton(part);
assert.ok(button, 'Should have collapse button');
const labelElement = getCollapseButtonLabel(button);
const buttonText = labelElement?.textContent ?? button.textContent ?? '';
// Should show the latest tool message
assert.ok(buttonText.includes('Searching for patterns'), 'Title should include latest tool message');
});
test('should keep showing running tool when another tool completes', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// Add first tool (will complete)
const firstToolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing));
const firstTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'readFile',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: firstToolState,
invocationMessage: 'Reading file1.ts'
};
part.trackToolState(firstTool);
// Add second tool (will keep running)
const secondToolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing));
const secondTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'searchFiles',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: secondToolState,
invocationMessage: 'Searching for patterns'
};
part.trackToolState(secondTool);
// Verify title shows second tool
const button = getCollapseButton(part);
assert.ok(button, 'Button should exist');
const labelElement = getCollapseButtonLabel(button);
let buttonText = labelElement?.textContent ?? button?.textContent ?? '';
assert.ok(buttonText.includes('Searching for patterns'), 'Title should show second tool');
// Complete the first tool
firstToolState.set(createState(IChatToolInvocation.StateKind.Completed), undefined);
// Title should still show the second tool (which is still running and owns the title)
buttonText = labelElement?.textContent ?? button?.textContent ?? '';
assert.ok(buttonText.includes('Searching for patterns'), 'Title should still show second tool after first completes');
});
test('should keep title when tool is cancelled', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// Add a tool that will be cancelled
const toolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing));
const childTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'readFile',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: toolState,
invocationMessage: 'Reading file.ts'
};
part.trackToolState(childTool);
// Verify title includes tool message
const button = getCollapseButton(part);
assert.ok(button, 'Button should exist');
const labelElement = getCollapseButtonLabel(button);
let buttonText = labelElement?.textContent ?? button?.textContent ?? '';
assert.ok(buttonText.includes('Reading file.ts'), 'Title should include tool message while running');
// Cancel the tool
toolState.set(createState(IChatToolInvocation.StateKind.Cancelled), undefined);
// Title should still include the tool message (persists like thinking part)
buttonText = labelElement?.textContent ?? button?.textContent ?? '';
assert.ok(buttonText.includes('Reading file.ts'),
'Title should still include tool message after cancellation');
});
test('should keep showing last tool message when that tool completes', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// First tool starts
const firstToolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing));
const firstTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'readFile',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: firstToolState,
invocationMessage: 'Reading file1.ts'
};
part.trackToolState(firstTool);
// Verify title shows first tool
const button = getCollapseButton(part);
assert.ok(button, 'Button should exist');
const labelElement = getCollapseButtonLabel(button);
let buttonText = labelElement?.textContent ?? button?.textContent ?? '';
assert.ok(buttonText.includes('Reading file1.ts'), 'Title should show first tool');
// Second tool starts and becomes the current title
const secondToolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing));
const secondTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'searchFiles',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: secondToolState,
invocationMessage: 'Searching for patterns'
};
part.trackToolState(secondTool);
// Verify title shows second tool
buttonText = labelElement?.textContent ?? button?.textContent ?? '';
assert.ok(buttonText.includes('Searching for patterns'), 'Title should show second tool');
// Second tool completes
secondToolState.set(createState(IChatToolInvocation.StateKind.Completed), undefined);
// Title should still show second tool (persists like thinking part)
buttonText = labelElement?.textContent ?? button?.textContent ?? '';
assert.ok(buttonText.includes('Searching for patterns'),
'Title should still show last tool message after completion');
});
});
suite('Auto-expand on confirmation', () => {
test('should auto-expand when tool state becomes WaitingForConfirmation', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// Verify initially collapsed
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should start collapsed');
// Create a tool invocation that starts in executing state, then changes to WaitingForConfirmation
const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.Executing));
const childTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'readFile',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: stateObservable,
invocationMessage: 'Reading file'
};
// Track this tool's state (this registers observers)
part.trackToolState(childTool);
// Should still be collapsed since tool is executing, not waiting for confirmation
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should still be collapsed when tool is executing');
// Now change state to WaitingForConfirmation
stateObservable.set(createState(IChatToolInvocation.StateKind.WaitingForConfirmation), undefined);
// Should auto-expand when tool needs confirmation
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should auto-expand when tool needs confirmation');
});
test('should auto-collapse when confirmation is addressed', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// Create a tool invocation that is waiting for confirmation
const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.WaitingForConfirmation));
const childTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'runInTerminal',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: stateObservable,
invocationMessage: 'Run npm install'
};
// Track this tool's state
part.trackToolState(childTool);
// Should be expanded now
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should be expanded when waiting for confirmation');
// Now simulate confirmation being addressed (tool moves to executing)
stateObservable.set(createState(IChatToolInvocation.StateKind.Executing), undefined);
// Should auto-collapse after confirmation is addressed
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'),
'Should auto-collapse after confirmation is addressed');
});
test('should not auto-collapse if user manually expanded', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// User manually expands
const button = getCollapseButton(part);
button?.click();
// Should be expanded
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, 'Should be expanded after user click');
// Create a tool that goes through confirmation cycle
const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.WaitingForConfirmation));
const childTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'runInTerminal',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: stateObservable,
invocationMessage: 'Run npm install'
};
// Track this tool's state
part.trackToolState(childTool);
// Confirm the tool (move to executing)
stateObservable.set(createState(IChatToolInvocation.StateKind.Executing), undefined);
// Since user manually expanded, it should stay expanded
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should stay expanded when user manually expanded');
});
test('should respect manual expansion after auto-expand', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// Verify initially collapsed
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should start collapsed');
// Create a tool that needs confirmation
const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.WaitingForConfirmation));
const childTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'runInTerminal',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: stateObservable,
invocationMessage: 'Run npm install'
};
part.trackToolState(childTool);
// Should auto-expand
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should auto-expand for confirmation');
// User manually collapses
const button = getCollapseButton(part);
button?.click();
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should collapse after user click');
// User manually expands again
button?.click();
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should expand after second user click');
// Confirm the tool (move to executing)
stateObservable.set(createState(IChatToolInvocation.StateKind.Executing), undefined);
// Since user manually re-expanded after auto-expand, should stay expanded
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should stay expanded when user manually re-expanded after auto-expand');
});
test('should resume auto-collapse after user manually expands then collapses', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// First confirmation cycle - user manually expands
const stateObservable1 = observableValue('state1', createState(IChatToolInvocation.StateKind.WaitingForConfirmation));
const childTool1: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'runInTerminal',
toolCallId: 'tool1',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: stateObservable1,
invocationMessage: 'First tool'
};
part.trackToolState(childTool1);
// Should auto-expand for first confirmation
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should auto-expand for first confirmation');
// User manually collapses
const button = getCollapseButton(part);
button?.click();
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should collapse after user click');
// User manually expands (this sets userManuallyExpanded = true)
button?.click();
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should expand after user re-expands');
// Complete first tool (should not auto-collapse since user manually expanded)
stateObservable1.set(createState(IChatToolInvocation.StateKind.Completed), undefined);
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should stay expanded after first tool completes (user manually expanded)');
// User manually collapses again (this resets userManuallyExpanded)
button?.click();
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should collapse after user manually collapses');
// Second confirmation cycle - should auto-collapse now since userManuallyExpanded was reset
const stateObservable2 = observableValue('state2', createState(IChatToolInvocation.StateKind.WaitingForConfirmation));
const childTool2: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'runInTerminal',
toolCallId: 'tool2',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: stateObservable2,
invocationMessage: 'Second tool'
};
part.trackToolState(childTool2);
// Should auto-expand for second confirmation
assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false,
'Should auto-expand for second confirmation');
// Complete second tool - should auto-collapse since userManuallyExpanded was reset by the earlier collapse
stateObservable2.set(createState(IChatToolInvocation.StateKind.Executing), undefined);
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'),
'Should auto-collapse after second confirmation is addressed (userManuallyExpanded was reset)');
});
test('should clear current running tool message when tool completes', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Working on task',
agentName: 'TestAgent'
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// Create a tool that will complete
const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.Executing));
const childTool: IChatToolInvocation = {
...createMockToolInvocation({
toolId: 'readFile',
subAgentInvocationId: toolInvocation.subAgentInvocationId
}),
state: stateObservable,
invocationMessage: 'Reading config.ts'
};
part.trackToolState(childTool);
// Verify title includes tool message
const button = getCollapseButton(part);
assert.ok(button, 'Button should exist');
const labelElement = getCollapseButtonLabel(button);
let buttonText = labelElement?.textContent ?? button?.textContent ?? '';
assert.ok(buttonText.includes('Reading config.ts'), 'Title should include tool message while running');
// Complete the tool
stateObservable.set(createState(IChatToolInvocation.StateKind.Completed), undefined);
// Title should still include the tool message (persists like thinking part)
buttonText = labelElement?.textContent ?? button?.textContent ?? '';
assert.ok(buttonText.includes('Reading config.ts'),
'Title should still include tool message after completion');
});
});
});

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */
import assert from 'assert';
import { $ } from '../../../../../../../base/browser/dom.js';
import { Event } from '../../../../../../../base/common/event.js';

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */
import assert from 'assert';
import { Event } from '../../../../../../../base/common/event.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';

View File

@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable no-restricted-syntax */
import assert from 'assert';
import * as dom from '../../../../../base/browser/dom.js';
import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';

View File

@@ -58,7 +58,6 @@ class TestObjectTree<T> extends ObjectTree<T, any> {
}
public getRendered(getProperty?: string) {
// eslint-disable-next-line no-restricted-syntax
const elements = element.querySelectorAll<HTMLElement>('.monaco-tl-contents');
const sorted = [...elements].sort((a, b) => pos(a) - pos(b));
const chain: SerializedTree[] = [{ e: '', children: [] }];

View File

@@ -15,8 +15,6 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/comm
import { DisposableStore } from '../../../base/common/lifecycle.js';
import { mainWindow } from '../../../base/browser/window.js';
/* eslint-disable no-restricted-syntax */
suite('Workbench parts', () => {
const disposables = new DisposableStore();