44 *--------------------------------------------------------------------------------------------*/
55
66import { $ , clearNode } from '../../../../../base/browser/dom.js' ;
7- import { Disposable } from '../../../../../base/common/lifecycle.js' ;
87import { IChatThinkingPart } from '../../common/chatService.js' ;
98import { IChatContentPartRenderContext , IChatContentPart } from './chatContentParts.js' ;
9+ import { IChatRendererContent } from '../../common/chatViewModel.js' ;
10+ import { ChatTreeItem } from '../chat.js' ;
1011import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js' ;
12+ import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js' ;
1113import { MarkdownString } from '../../../../../base/common/htmlContent.js' ;
1214import { 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 ( / < \| i m _ s e p \| > \* { 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 ( / < \| i m _ s e p \| > \* { 4 , } / g, '' )
43- . trim ( ) ;
72+ const noSep = content . replace ( / < \| i m _ s e p \| > \* { 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