Skip to content

Commit 576af27

Browse files
authored
handle streaming thinking tokens (microsoft#264207)
* handle streaming thinking tokens * some cleanup * cleanup * more cleanup * better comments
1 parent 9702bf9 commit 576af27

File tree

6 files changed

+238
-304
lines changed

6 files changed

+238
-304
lines changed

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -636,9 +636,9 @@ configurationRegistry.registerConfiguration({
636636
default: 'collapsedPreview',
637637
enum: ['collapsed', 'collapsedPreview', 'expanded', 'none'],
638638
enumDescriptions: [
639-
nls.localize('chat.agent.thinkingMode.collapsed', "Collapsed normal"),
640-
nls.localize('chat.agent.thinkingMode.collapsedPreview', "Collapsed and show thinking related tool calls as they come in."),
641-
nls.localize('chat.agent.thinkingMode.expanded', "Uncollapsed (expanded)"),
639+
nls.localize('chat.agent.thinkingMode.collapsed', "Thinking parts will be collapsed by default."),
640+
nls.localize('chat.agent.thinkingMode.expanded', "Thinking parts will be expanded by default."),
641+
nls.localize('chat.agent.thinkingMode.collapsedPreview', "Thinking parts will be expanded first, then collapse once we reach a part that is not thinking."),
642642
nls.localize('chat.agent.thinkingMode.none', "Do not show the thinking"),
643643
],
644644
description: nls.localize('chat.agent.thinkingCollapsedByDefault', "Controls how thinking is rendered."),

src/vs/workbench/contrib/chat/browser/chatContentParts/chatPinnedContentPart.ts

Lines changed: 0 additions & 151 deletions
This file was deleted.

src/vs/workbench/contrib/chat/browser/chatContentParts/chatThinkingContentPart.ts

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,85 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { $, clearNode } from '../../../../../base/browser/dom.js';
7-
import { Disposable } from '../../../../../base/common/lifecycle.js';
87
import { IChatThinkingPart } from '../../common/chatService.js';
98
import { IChatContentPartRenderContext, IChatContentPart } from './chatContentParts.js';
9+
import { IChatRendererContent } from '../../common/chatViewModel.js';
10+
import { ChatTreeItem } from '../chat.js';
1011
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
12+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
1113
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
1214
import { MarkdownRenderer, IMarkdownRenderResult } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
15+
import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js';
16+
import { localize } from '../../../../../nls.js';
1317

14-
export class ChatThinkingContentPart extends Disposable implements IChatContentPart {
15-
readonly domNode: HTMLElement;
18+
function extractTextFromPart(content: IChatThinkingPart): string {
19+
const raw = Array.isArray(content.value) ? content.value.join('') : (content.value || '');
20+
return raw.replace(/<\|im_sep\|>\*{4,}/g, '').trim();
21+
}
22+
23+
function extractTitleFromThinkingContent(content: string): string | undefined {
24+
const headerMatch = content.match(/^\*\*([^*]+)\*\*\s*\n\n/);
25+
return headerMatch ? headerMatch[1].trim() : undefined;
26+
}
27+
28+
export class ChatThinkingContentPart extends ChatCollapsibleContentPart implements IChatContentPart {
1629
public readonly codeblocks: undefined;
1730
public readonly codeblocksPartId: undefined;
1831

32+
private id: string | undefined;
1933
private currentThinkingValue: string;
34+
private currentTitle: string;
35+
private defaultTitle = localize('chat.thinking.header', 'Thinking...');
2036
private readonly renderer: MarkdownRenderer;
2137
private textContainer!: HTMLElement;
2238
private markdownResult: IMarkdownRenderResult | undefined;
39+
private wrapper!: HTMLElement;
2340

2441
constructor(
2542
content: IChatThinkingPart,
26-
_context: IChatContentPartRenderContext,
43+
context: IChatContentPartRenderContext,
2744
@IInstantiationService instantiationService: IInstantiationService,
45+
@IConfigurationService private readonly configurationService: IConfigurationService,
2846
) {
29-
super();
47+
const initialText = extractTextFromPart(content);
48+
const extractedTitle = extractTitleFromThinkingContent(initialText)
49+
?? localize('chat.thinking.header', 'Thinking...');
50+
51+
super(extractedTitle, context);
3052

3153
this.renderer = instantiationService.createInstance(MarkdownRenderer, {});
32-
this.currentThinkingValue = this.parseContent(Array.isArray(content.value) ? content.value.join('') : content.value || '');
54+
this.id = content.id;
55+
this.currentThinkingValue = initialText;
56+
this.currentTitle = extractedTitle;
57+
58+
const mode = this.configurationService.getValue<string>('chat.agent.thinkingStyle') ?? 'none';
59+
if (mode === 'expanded' || mode === 'collapsedPreview') {
60+
this.setExpanded(true);
61+
} else if (mode === 'collapsed') {
62+
this.setExpanded(false);
63+
}
64+
65+
const node = this.domNode;
66+
node.classList.add('chat-thinking-box');
67+
node.tabIndex = 0;
3368

34-
this.domNode = $('.chat-thinking-box');
35-
this.domNode.tabIndex = 0;
36-
this.renderContent();
3769
}
3870

3971
private parseContent(content: string): string {
40-
// Remove <|im_sep|>****
41-
return content
42-
.replace(/<\|im_sep\|>\*{4,}/g, '')
43-
.trim();
72+
const noSep = content.replace(/<\|im_sep\|>\*{4,}/g, '').trim();
73+
return noSep;
4474
}
4575

46-
private renderContent(): void {
47-
this.textContainer = $('.chat-thinking-text.markdown-content');
48-
this.domNode.appendChild(this.textContainer);
76+
protected override initContent(): HTMLElement {
77+
this.wrapper = $('.chat-used-context-list.chat-thinking-collapsible');
78+
this.textContainer = $('.chat-thinking-item.markdown-content');
79+
this.wrapper.appendChild(this.textContainer);
4980

5081
if (this.currentThinkingValue) {
5182
this.renderMarkdown(this.currentThinkingValue);
5283
}
84+
85+
return this.wrapper;
5386
}
5487

5588
private renderMarkdown(content: string): void {
@@ -64,12 +97,67 @@ export class ChatThinkingContentPart extends Disposable implements IChatContentP
6497
}
6598

6699
clearNode(this.textContainer);
67-
this.markdownResult = this.renderer.render(new MarkdownString(cleanedContent));
100+
this.markdownResult = this._register(this.renderer.render(new MarkdownString(cleanedContent)));
68101
this.textContainer.appendChild(this.markdownResult.element);
69102
}
70103

71-
hasSameContent(other: any): boolean {
72-
return other.kind === 'thinking';
104+
public resetId(): void {
105+
this.id = undefined;
106+
}
107+
108+
public collapseContent(): void {
109+
this.setExpanded(false);
110+
}
111+
112+
public updateThinking(content: IChatThinkingPart): void {
113+
const raw = extractTextFromPart(content);
114+
const next = this.parseContent(raw);
115+
if (next === this.currentThinkingValue) {
116+
return;
117+
}
118+
this.currentThinkingValue = next;
119+
this.renderMarkdown(next);
120+
121+
// if title is present now (e.g., arrived mid-stream), update the header label
122+
const maybeTitle = extractTitleFromThinkingContent(raw);
123+
if (maybeTitle && maybeTitle !== this.currentTitle) {
124+
this.setTitle(maybeTitle);
125+
this.currentTitle = maybeTitle;
126+
}
127+
}
128+
129+
public finalizeTitleIfDefault(): void {
130+
if (this.currentTitle === this.defaultTitle) {
131+
const done = localize('chat.pinned.thinking.header.done', 'Thought for a few seconds...');
132+
this.setTitle(done);
133+
this.currentTitle = done;
134+
}
135+
}
136+
137+
public appendItem(content: HTMLElement): void {
138+
this.wrapper.appendChild(content);
139+
}
140+
141+
// makes a new text container. when we update, we now update this container.
142+
public setupThinkingContainer(content: IChatThinkingPart, context: IChatContentPartRenderContext) {
143+
this.textContainer = $('.chat-thinking-item.markdown-content');
144+
this.wrapper.appendChild(this.textContainer);
145+
this.id = content?.id;
146+
this.updateThinking(content);
147+
}
148+
149+
hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], _element: ChatTreeItem): boolean {
150+
151+
// only need this check if we are adding tools into thinking dropdown.
152+
// if (other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') {
153+
// return true;
154+
// }
155+
156+
if (other.kind !== 'thinking') {
157+
return false;
158+
}
159+
160+
return other?.id !== this.id;
73161
}
74162

75163
override dispose(): void {

0 commit comments

Comments
 (0)