thinking settings cleanup and style fix (#274656)

* thinking settings cleanup and style fix

* update setting description

* Update src/vs/workbench/contrib/chat/browser/chatListRenderer.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Justin Chen
2025-11-02 17:43:58 -08:00
committed by GitHub
parent 4c2ed1a2e7
commit 34199d40fc
5 changed files with 72 additions and 141 deletions
@@ -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': {
@@ -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<ThinkingDisplayMode>('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<ThinkingDisplayMode>('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 {
@@ -771,17 +771,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
const lastPart = findLast(partsToRender, part => part.kind !== 'markdownContent' || part.content.value.trim().length > 0);
const thinkingStyle = this.configService.getValue<ThinkingDisplayMode>('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<string>('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<Ch
return false;
}
private shouldCombineThinking(element: IChatResponseViewModel): boolean {
const thinkingStyle = this.configService.getValue<ThinkingDisplayMode>('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<Ch
};
// combine tool invocations into thinking part if needed. render the tool, but do not replace the working spinner with the new part's dom node since it is already inside the thinking part.
if (this._currentThinkingPart && (partToRender.kind === 'toolInvocation' || partToRender.kind === 'toolInvocationSerialized') && this.shouldCombineThinking(element) && this.shouldPinPart(partToRender, element)) {
if (this._currentThinkingPart && (partToRender.kind === 'toolInvocation' || partToRender.kind === 'toolInvocationSerialized') && this.shouldPinPart(partToRender, element)) {
const newPart = this.renderChatContentPart(partToRender, templateData, context);
if (newPart) {
renderedParts[contentIndex] = newPart;
@@ -1157,10 +1149,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return element.isComplete && this.configService.getValue<boolean>('chat.checkpoints.showFileChanges');
}
private shouldShowThinkingPart(): boolean {
return this.configService.getValue<ThinkingDisplayMode>('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<Ch
// put thinking parts inside a pinned part. commented out for now.
private shouldPinPart(part: IChatRendererContent, element?: IChatResponseViewModel): boolean {
const collapsedTools = this.configService.getValue<string>('chat.agent.thinking.collapsedTools');
const thinkingStyle = this.configService.getValue<ThinkingDisplayMode>('chat.agent.thinkingStyle');
const collapsedTools = this.configService.getValue<boolean>('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<Ch
private renderChatContentPart(content: IChatRendererContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined {
try {
if (this.shouldShowThinkingPart()) {
// 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 collapsedTools = this.configService.getValue<string>('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<boolean>('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<Ch
return this.renderChangesSummary(content, context, templateData);
} else if (content.kind === 'mcpServersStarting') {
return this.renderMcpServersInteractionRequired(content, context, templateData);
} else if (content.kind === 'thinking' && this.shouldShowThinkingPart()) {
} else if (content.kind === 'thinking') {
return this.renderThinkingPart(content, context, templateData);
}
@@ -1509,11 +1495,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}));
this.handleRenderedCodeblocks(context.element, part, codeBlockStartIndex);
if (isResponseVM(context.element) && this.shouldCombineThinking(context.element)) {
// handling for when we want to put tool invocations inside a thinking part
if (isResponseVM(context.element) && this.configService.getValue<string>('chat.agent.thinking.collapsedTools') !== 'none') {
if (this.shouldPinPart(toolInvocation, context.element)) {
if (this._currentThinkingPart && part?.domNode) {
this._currentThinkingPart.appendItem(part?.domNode);
}
} else {
this.finalizeCurrentThinkingPart();
}
}
@@ -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;
@@ -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';