diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 61ca83c1d16..a7cb31ff75f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -678,25 +678,26 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.ThinkingStyle]: { type: 'string', - default: 'default', - enum: ['default', 'collapsed', 'collapsedPreview', 'expanded', 'none', 'collapsedPerItem', 'fixedScrolling', 'fixedScrollingTools'], + default: 'fixedScrolling', + enum: ['collapsed', 'collapsedPreview', 'fixedScrolling'], enumDescriptions: [ - nls.localize('chat.agent.thinkingMode.default', "Let VS Code choose a thinking style for each model."), nls.localize('chat.agent.thinkingMode.collapsed', "Thinking parts will be collapsed by default."), nls.localize('chat.agent.thinkingMode.collapsedPreview', "Thinking parts will be expanded first, then collapse once we reach a part that is not thinking."), - nls.localize('chat.agent.thinkingMode.expanded', "Thinking parts will be expanded by default."), - nls.localize('chat.agent.thinkingMode.none', "Do not show the thinking"), - nls.localize('chat.agent.thinkingMode.collapsedPerItem', "Each thinking subsection is individually collapsible and collapsed by default inside the thinking block."), nls.localize('chat.agent.thinkingMode.fixedScrolling', "Show thinking in a fixed-height streaming panel that auto-scrolls; click header to expand to full height."), - nls.localize('chat.agent.thinkingMode.fixedScrollingTools', "Show thinking and certain tool calls in a fixed-height streaming panel that auto-scrolls; click header to expand to full height."), ], description: nls.localize('chat.agent.thinkingStyle', "Controls how thinking is rendered."), tags: ['experimental'], }, 'chat.agent.thinking.collapsedTools': { - type: 'boolean', - default: false, - description: nls.localize('chat.agent.thinking.collapsedTools', "When enabled, tool calls will be added by default inside the thinking block."), + type: 'string', + default: 'readOnly', + enum: ['none', 'all', 'readOnly'], + enumDescriptions: [ + nls.localize('chat.agent.thinking.collapsedTools.none', "No tool calls are added into the collapsible thinking section."), + nls.localize('chat.agent.thinking.collapsedTools.all', "All tool calls are added into the collapsible thinking section."), + nls.localize('chat.agent.thinking.collapsedTools.readOnly', "Only read-only tool calls are added into the collapsible thinking section."), + ], + markdownDescription: nls.localize('chat.agent.thinking.collapsedTools', "When enabled, tool calls are added into the collapsible thinking section according to the selected mode. This setting only applies when `#chat.agent.thinkingStyle#` is set to `fixedScrolling`."), tags: ['experimental'], }, 'chat.disableAIFeatures': { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatThinkingContentPart.ts index 41ee572bba0..e3473a8f19f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatThinkingContentPart.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { $, clearNode } from '../../../../../base/browser/dom.js'; -import * as dom from '../../../../../base/browser/dom.js'; import { IChatThinkingPart } from '../../common/chatService.js'; import { IChatContentPartRenderContext, IChatContentPart } from './chatContentParts.js'; -import { IChatRendererContent, isResponseVM } from '../../common/chatViewModel.js'; +import { IChatRendererContent } from '../../common/chatViewModel.js'; import { ThinkingDisplayMode } from '../../common/constants.js'; import { ChatTreeItem } from '../chat.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -35,23 +34,6 @@ function extractTitleFromThinkingContent(content: string): string | undefined { return headerMatch ? headerMatch[1].trim() : undefined; } -/** - * everything => fixedScrolling - * gpt-5-codex => collapsed with special grouping - */ -function resolvePerModelDefaultThinkingStyle(modelId: string | undefined): ThinkingDisplayMode { - if (!modelId) { - return ThinkingDisplayMode.FixedScrolling; - } - const id = modelId.toLowerCase(); - - // TODO @justschen: could have additional styles specific to gemini/codex besides collapased. - if (id.includes('gpt-5-codex')) { - return ThinkingDisplayMode.FixedScrollingTools; - } - - return ThinkingDisplayMode.FixedScrolling; -} export class ChatThinkingContentPart extends ChatCollapsibleContentPart implements IChatContentPart { public readonly codeblocks: undefined; @@ -70,7 +52,6 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen private fixedScrollViewport: HTMLElement | undefined; private fixedContainer: HTMLElement | undefined; private headerButton: ButtonWithIcon | undefined; - private statusIcon: HTMLElement | undefined; private lastExtractedTitle: string | undefined; private hasMultipleItems: boolean = false; @@ -89,18 +70,9 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.id = content.id; - const configuredMode = this.configurationService.getValue('chat.agent.thinkingStyle') ?? ThinkingDisplayMode.None; - let effectiveMode = configuredMode; - if (configuredMode === ThinkingDisplayMode.Default) { - let modelId: string | undefined; - if (isResponseVM(context.element)) { - modelId = context.element.model.request?.modelId; - } - effectiveMode = resolvePerModelDefaultThinkingStyle(modelId); - } + const configuredMode = this.configurationService.getValue('chat.agent.thinkingStyle') ?? ThinkingDisplayMode.Collapsed; - this.perItemCollapsedMode = effectiveMode === ThinkingDisplayMode.CollapsedPerItem; - this.fixedScrollingMode = effectiveMode === ThinkingDisplayMode.FixedScrolling || effectiveMode === ThinkingDisplayMode.FixedScrollingTools; + this.fixedScrollingMode = configuredMode === ThinkingDisplayMode.FixedScrolling; this.currentTitle = extractedTitle; if (extractedTitle !== this.defaultTitle) { @@ -108,7 +80,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } this.currentThinkingValue = this.parseContent(initialText); - if (effectiveMode === ThinkingDisplayMode.Collapsed) { + if (configuredMode === ThinkingDisplayMode.Collapsed) { this.setExpanded(false); } else { this.setExpanded(true); @@ -159,11 +131,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen const button = this.headerButton = this._register(new ButtonWithIcon(header, {})); button.label = this.defaultTitle; - button.icon = Codicon.chevronRight; - this.statusIcon = $('.chat-thinking-fixed-title-icon'); - const spinnerEl = dom.h(ThemeIcon.asCSSSelector(ThemeIcon.modify(Codicon.loading, 'spin'))); - this.statusIcon.appendChild(spinnerEl.root); - button.element.appendChild(this.statusIcon); + button.icon = ThemeIcon.modify(Codicon.loading, 'spin'); this.fixedScrollViewport = this.wrapper; this.textContainer = $('.chat-thinking-item.markdown-content'); @@ -199,7 +167,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } this.fixedCollapsed = collapsed; this.fixedContainer.classList.toggle('collapsed', collapsed); - if (this.headerButton) { + if (this.fixedContainer.classList.contains('finished') && this.headerButton) { this.headerButton.icon = collapsed ? Codicon.chevronRight : Codicon.chevronDown; } if (this.fixedCollapsed && userInitiated) { @@ -309,11 +277,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } if (this.headerButton) { this.headerButton.label = finalLabel; + this.headerButton.icon = this.fixedCollapsed ? Codicon.chevronRight : Codicon.chevronDown; } - if (this.statusIcon && this.fixedContainer) { + if (this.fixedContainer) { this.fixedContainer.classList.add('finished'); - this.setFixedCollapsedState(true); - this.statusIcon.replaceChildren(dom.h(ThemeIcon.asCSSSelector(Codicon.check)).root); } this.currentTitle = finalLabel; @@ -330,10 +297,6 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.setTitle(suffix); this.currentTitle = suffix; } - - if (!this.fixedScrollingMode && this.statusIcon) { - this.statusIcon.replaceChildren(dom.h(ThemeIcon.asCSSSelector(Codicon.check)).root); - } } public appendItem(content: HTMLElement): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 85198cd27f4..4073b9c33a1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -771,17 +771,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind !== 'markdownContent' || part.content.value.trim().length > 0); const thinkingStyle = this.configService.getValue('chat.agent.thinkingStyle'); - if ((thinkingStyle === ThinkingDisplayMode.FixedScrolling || thinkingStyle === ThinkingDisplayMode.Default) && lastPart?.kind === 'thinking') { - return false; - } - if (this.shouldCombineThinking(element)) { + if (thinkingStyle === ThinkingDisplayMode.FixedScrolling && this.configService.getValue('chat.agent.thinking.collapsedTools') !== 'none' && this._currentThinkingPart) { return lastPart?.kind !== 'thinking' && lastPart?.kind !== 'toolInvocation' && lastPart?.kind !== 'prepareToolInvocation'; } if ( !lastPart || - lastPart.kind === 'references' || lastPart.kind === 'thinking' || + lastPart.kind === 'references' || (lastPart.kind === 'thinking' && thinkingStyle !== ThinkingDisplayMode.FixedScrolling) || ((lastPart.kind === 'toolInvocation' || lastPart.kind === 'toolInvocationSerialized') && (IChatToolInvocation.isComplete(lastPart) || lastPart.presentation === 'hidden')) || ((lastPart.kind === 'textEditGroup' || lastPart.kind === 'notebookEditGroup') && lastPart.done && !partsToRender.some(part => part.kind === 'toolInvocation' && !IChatToolInvocation.isComplete(part))) || (lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) || @@ -793,11 +790,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.agent.thinkingStyle'); - const defaultCodex = thinkingStyle === ThinkingDisplayMode.Default && element.model.request?.modelId?.toLowerCase().includes('codex'); - return thinkingStyle === ThinkingDisplayMode.FixedScrollingTools || !!defaultCodex; - } private getChatFileChangesSummaryPart(element: IChatResponseViewModel): IChatChangesSummaryPart | undefined { if (!this.shouldShowFileChangesSummary(element)) { @@ -1030,7 +1022,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.checkpoints.showFileChanges'); } - private shouldShowThinkingPart(): boolean { - return this.configService.getValue('chat.agent.thinkingStyle') !== ThinkingDisplayMode.None; - } - private getDataForProgressiveRender(element: IChatResponseViewModel) { if (!element.isComplete && element.response.value.length > 0 && (element.contentUpdateTimings ? element.contentUpdateTimings.lastWordCount : 0) === 0) { /** @@ -1207,12 +1195,13 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.agent.thinking.collapsedTools'); - const thinkingStyle = this.configService.getValue('chat.agent.thinkingStyle'); - const collapsedTools = this.configService.getValue('chat.agent.thinking.collapsedTools'); - const collapseToolsForFixedScrolling = thinkingStyle === ThinkingDisplayMode.FixedScrollingTools && collapsedTools; + if (collapsedTools === 'none') { + return false; + } - if (collapseToolsForFixedScrolling) { + if (collapsedTools === 'all') { if (part.kind === 'toolInvocation') { return !part.confirmationMessages; } @@ -1278,30 +1267,27 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer content.kind === other.kind); - } + const collapsedTools = this.configService.getValue('chat.agent.thinking.collapsedTools'); + // if we get an empty thinking part, mark thinking as finished + if (content.kind === 'thinking' && (Array.isArray(content.value) ? content.value.length === 0 : !content.value)) { + this._currentThinkingPart?.resetId(); + this._streamingThinking = false; + return this.renderNoContent(other => content.kind === other.kind); + } - const lastRenderedPart = context.preceedingContentParts.length ? context.preceedingContentParts[context.preceedingContentParts.length - 1] : undefined; - const previousContent = context.contentIndex > 0 ? context.content[context.contentIndex - 1] : undefined; + const lastRenderedPart = context.preceedingContentParts.length ? context.preceedingContentParts[context.preceedingContentParts.length - 1] : undefined; + const previousContent = context.contentIndex > 0 ? context.content[context.contentIndex - 1] : undefined; - // Special handling for "create" tool invocations- do not end thinking if previous part is a create tool invocation and config is set. - const collapsedTools = this.configService.getValue('chat.agent.thinking.collapsedTools'); - const shouldKeepThinkingForCreateTool = collapsedTools && lastRenderedPart instanceof ChatToolInvocationPart && this.isCreateToolInvocationContent(previousContent); + // Special handling for "create" tool invocations- do not end thinking if previous part is a create tool invocation and config is set. + const shouldKeepThinkingForCreateTool = collapsedTools !== 'none' && lastRenderedPart instanceof ChatToolInvocationPart && this.isCreateToolInvocationContent(previousContent); - if (!shouldKeepThinkingForCreateTool) { - const isResponseElement = isResponseVM(context.element); - const allowToolStreaming = isResponseElement && this.shouldCombineThinking(context.element); - const isThinkingContent = content.kind === 'working' || content.kind === 'thinking'; - const toolInvocationContext = isResponseElement ? context.element : undefined; - const isToolStreamingContent = allowToolStreaming && this.shouldPinPart(content, toolInvocationContext); - if (this._currentThinkingPart && !this._streamingThinking && !isThinkingContent && !isToolStreamingContent) { - this.finalizeCurrentThinkingPart(); - } + if (!shouldKeepThinkingForCreateTool) { + const isResponseElement = isResponseVM(context.element); + const isThinkingContent = content.kind === 'working' || content.kind === 'thinking'; + const toolInvocationContext = isResponseElement ? context.element : undefined; + const isToolStreamingContent = isResponseElement && this.shouldPinPart(content, toolInvocationContext); + if (this._currentThinkingPart && !this._streamingThinking && !isThinkingContent && !isToolStreamingContent) { + this.finalizeCurrentThinkingPart(); } } @@ -1345,7 +1331,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.agent.thinking.collapsedTools') !== 'none') { if (this.shouldPinPart(toolInvocation, context.element)) { if (this._currentThinkingPart && part?.domNode) { this._currentThinkingPart.appendItem(part?.domNode); } + } else { + this.finalizeCurrentThinkingPart(); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 1f60d1e3ccf..f30ec89f893 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -763,7 +763,8 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-editing-session { - margin-bottom: -4px; /* Counter the flex gap */ + margin-bottom: -4px; + /* Counter the flex gap */ /* reset the 4px gap of the container for editing sessions */ width: 100%; position: relative; @@ -1086,7 +1087,8 @@ have to be updated for changes to the rules above, or to support more deeply nes /* Chat Todo List Widget Container - mirrors chat-editing-session styling */ .interactive-session .interactive-input-part > .chat-todo-list-widget-container { - margin-bottom: -4px; /* reset the 4px gap of the container for editing sessions */ + margin-bottom: -4px; + /* reset the 4px gap of the container for editing sessions */ width: 100%; position: relative; } @@ -2942,54 +2944,38 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-thinking-fixed-header { display: flex; align-items: center; - gap: 4px; - border: 1px solid var(--vscode-chat-requestBorder); - color: var(--vscode-interactive-session-foreground); - opacity: 0.85; + gap: 7px; + font-size: var(--vscode-chat-font-size-body-s); + font-family: var(--vscode-chat-font-family, inherit); + color: var(--vscode-descriptionForeground); outline: none; - margin: 3px; - border: none; + padding-top: 2px; &:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; } - .chat-thinking-fixed-title-icon { - font-size: 16px; - margin-left: auto; - line-height: 0; - } - - .chat-thinking-fixed-title-icon .codicon-check { - color: var(--vscode-debugIcon-startForeground) !important; - } - .monaco-button { - display: flex; + display: inline-flex; align-items: center; - width: 100%; + width: fit-content; border: none; + border-radius: 4px; outline: none; - padding: 3px 8px; + padding: 2px 6px 2px 2px; + text-align: initial; + justify-content: initial; + gap: 4px; &:hover { - background: var(--vscode-toolbar-hoverBackground); + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-foreground); } } - .monaco-button:focus { - outline: none; - } - - .monaco-button:focus-visible { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; - } - - .chat-thinking-item-caret { - margin-left: auto; - font-size: 12px; + .monaco-button .codicon { + font-size: var(--vscode-chat-font-size-body-s); } .monaco-button-label, @@ -3004,8 +2990,6 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-thinking-fixed-height-controller { display: flex; flex-direction: column; - border: 1px solid var(--vscode-chat-requestBorder); - border-radius: 4px; margin-bottom: 8px; &.collapsed { @@ -3029,9 +3013,8 @@ have to be updated for changes to the rules above, or to support more deeply nes /* item and dot rendering */ .chat-used-context-list.chat-thinking-collapsible { - border: none; - border-top: 1px solid var(--vscode-chat-requestBorder); - border-radius: 0; + border: 1px solid var(--vscode-chat-requestBorder); + border-radius: 4px; margin-bottom: 0; position: relative; overflow: hidden; diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 074eaa63c1b..19ce2bcd8d1 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -51,14 +51,9 @@ export function isChatMode(mode: unknown): mode is ChatModeKind { // Thinking display modes for pinned content export enum ThinkingDisplayMode { - Default = 'default', Collapsed = 'collapsed', CollapsedPreview = 'collapsedPreview', - Expanded = 'expanded', - None = 'none', - CollapsedPerItem = 'collapsedPerItem', FixedScrolling = 'fixedScrolling', - FixedScrollingTools = 'fixedScrollingTools' } export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook' | 'editing-session';