diff --git a/eslint.config.js b/eslint.config.js index af29b3dba74..47eeebf7347 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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.' + }, + ], + } + }, ); diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index 5a02b84c7dc..cdfbe914fa9 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -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'; diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index e470e11a3f1..76d7639b04c 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -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'; diff --git a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts index 25840582aab..4eb5e2b7e28 100644 --- a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts @@ -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'; diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index b3813240e60..aa11fbe6036 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -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'; diff --git a/src/vs/editor/contrib/hover/test/browser/hoverCopyButton.test.ts b/src/vs/editor/contrib/hover/test/browser/hoverCopyButton.test.ts index 4b7495ca9a8..ceecd7d6adc 100644 --- a/src/vs/editor/contrib/hover/test/browser/hoverCopyButton.test.ts +++ b/src/vs/editor/contrib/hover/test/browser/hoverCopyButton.test.ts @@ -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'; diff --git a/src/vs/platform/hover/test/browser/hoverService.test.ts b/src/vs/platform/hover/test/browser/hoverService.test.ts index c92e7c565fc..8a209dfa8c1 100644 --- a/src/vs/platform/hover/test/browser/hoverService.test.ts +++ b/src/vs/platform/hover/test/browser/hoverService.test.ts @@ -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'); }); }); }); - 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 26ccd549e18..3a2de8981f3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -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, @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; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatInlineAnchorWidget.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatInlineAnchorWidget.test.ts index 5eca5ccea5b..94f5c182311 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatInlineAnchorWidget.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatInlineAnchorWidget.test.ts @@ -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'); }); }); - diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index 429365f0120..30259ad9226 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -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'; 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 38dbc83065f..7a58f61d8d9 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 @@ -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'); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatThinkingContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatThinkingContentPart.test.ts index a25a9a62625..9fd61a4f063 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatThinkingContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatThinkingContentPart.test.ts @@ -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'; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts index 0f685f734dd..517e57bc3b3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts @@ -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'; diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index cfaa1ab3f83..d5eab305fdc 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -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'; diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index 0c369dc7df1..dd19fb50a49 100644 --- a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -58,7 +58,6 @@ class TestObjectTree extends ObjectTree { } public getRendered(getProperty?: string) { - // eslint-disable-next-line no-restricted-syntax const elements = element.querySelectorAll('.monaco-tl-contents'); const sorted = [...elements].sort((a, b) => pos(a) - pos(b)); const chain: SerializedTree[] = [{ e: '', children: [] }]; diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 2df6e797173..794026f1952 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -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();