Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 36f67c8

Browse files
authored
feat: support chat reasoning (#4470)
* feat: support chat reasoning * feat: update reasoning display * fix: auto collapse when complete * chore: add icon
1 parent 52111ac commit 36f67c8

File tree

8 files changed

+119
-22
lines changed

8 files changed

+119
-22
lines changed

packages/ai-native/src/browser/chat/chat-model.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
IChatComponent,
77
IChatMarkdownContent,
88
IChatProgress,
9+
IChatReasoning,
910
IChatToolContent,
1011
IChatTreeData,
1112
uuid,
@@ -33,7 +34,8 @@ export type IChatProgressResponseContent =
3334
| IChatAsyncContent
3435
| IChatTreeData
3536
| IChatComponent
36-
| IChatToolContent;
37+
| IChatToolContent
38+
| IChatReasoning;
3739

3840
export class ChatResponseModel extends Disposable {
3941
#responseParts: IChatProgressResponseContent[] = [];
@@ -131,6 +133,18 @@ export class ChatResponseModel extends Disposable {
131133
};
132134
}
133135

136+
this.#updateResponseText();
137+
} else if (progress.kind === 'reasoning') {
138+
const lastResponsePart = this.#responseParts[responsePartLength];
139+
if (!lastResponsePart || lastResponsePart.kind !== 'reasoning') {
140+
// 去掉开头的 <think> 标签
141+
this.#responseParts.push({ content: progress.content.replace(/^<think>/, ''), kind: 'reasoning' });
142+
} else {
143+
this.#responseParts[responsePartLength] = {
144+
content: lastResponsePart.content + progress.content,
145+
kind: 'reasoning',
146+
};
147+
}
134148
this.#updateResponseText();
135149
} else if (progress.kind === 'asyncContent') {
136150
// Add a new resolving part
@@ -181,6 +195,9 @@ export class ChatResponseModel extends Disposable {
181195
if (part.kind === 'toolCall') {
182196
return part.content.function.name;
183197
}
198+
if (part.kind === 'reasoning') {
199+
return '';
200+
}
184201
return part.content.value;
185202
})
186203
.join('\n\n');
@@ -387,7 +404,7 @@ export class ChatModel extends Disposable implements IChatModel {
387404

388405
const { kind } = progress;
389406

390-
const basicKind = ['content', 'markdownContent', 'asyncContent', 'treeData', 'component', 'toolCall'];
407+
const basicKind = ['content', 'markdownContent', 'asyncContent', 'treeData', 'component', 'toolCall', 'reasoning'];
391408

392409
if (basicKind.includes(kind)) {
393410
request.response.updateContent(progress, quiet);

packages/ai-native/src/browser/components/ChatReply.tsx

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import cls from 'classnames';
12
import React, {
23
Fragment,
34
ReactNode,
@@ -40,6 +41,7 @@ import {
4041
IChatResponseProgressFileTreeData,
4142
IChatToolContent,
4243
URI,
44+
localize,
4345
} from '@opensumi/ide-core-common';
4446
import { IIconService } from '@opensumi/ide-theme';
4547
import { IMarkdownString, MarkdownString } from '@opensumi/monaco-editor-core/esm/vs/base/common/htmlContent';
@@ -224,6 +226,28 @@ export const ChatReply = (props: IChatReplyProps) => {
224226
const chatApiService = useInjectable<ChatService>(ChatServiceToken);
225227
const chatAgentService = useInjectable<IChatAgentService>(IChatAgentService);
226228
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);
229+
const [collapseThinkingIndexSet, setCollapseThinkingIndexSet] = useState<Set<number>>(
230+
!request.response.isComplete
231+
? new Set()
232+
: new Set(
233+
request.response.responseContents
234+
.map((item, index) => (item.kind === 'reasoning' ? index : -1))
235+
.filter((item) => item !== -1),
236+
),
237+
);
238+
239+
useEffect(() => {
240+
if (request.response.isComplete) {
241+
setCollapseThinkingIndexSet(
242+
new Set(
243+
request.response.responseContents
244+
.map((item, index) => (item.kind === 'reasoning' ? index : -1))
245+
.filter((item) => item !== -1),
246+
),
247+
);
248+
}
249+
}, [request.response.isComplete]);
250+
227251
useEffect(() => {
228252
const disposableCollection = new DisposableCollection();
229253

@@ -263,23 +287,6 @@ export const ChatReply = (props: IChatReplyProps) => {
263287
onRegenerate?.();
264288
}, [onRegenerate]);
265289

266-
const onStop = () => {
267-
if (onDone) {
268-
onDone();
269-
}
270-
aiReporter.end(relationId, {
271-
assistantMessage: request.response.responseText,
272-
replytime: Date.now() - startTime,
273-
success: false,
274-
isStop: true,
275-
command,
276-
agentId,
277-
messageId: msgId,
278-
sessionId: aiChatService.sessionModel.sessionId,
279-
});
280-
aiChatService.cancelRequest();
281-
};
282-
283290
const renderMarkdown = useCallback(
284291
(markdown: IMarkdownString) => {
285292
if (chatRenderRegistry.chatAIRoleRender) {
@@ -313,12 +320,48 @@ export const ChatReply = (props: IChatReplyProps) => {
313320
node = <ComponentRender component={item.component} value={item.value} messageId={msgId} />;
314321
} else if (item.kind === 'toolCall') {
315322
node = <ToolCallRender toolCall={item.content} messageId={msgId} />;
323+
} else if (item.kind === 'reasoning') {
324+
// 思考中必然为最后一条
325+
const isThinking = index === request.response.responseContents.length - 1 && !request.response.isComplete;
326+
node = (
327+
<div className={cls(styles.reasoning, { [styles.thinking]: isThinking })}>
328+
<Button
329+
size='small'
330+
type='secondary'
331+
className={styles.thinking}
332+
onClick={() => {
333+
if (isThinking) {
334+
return;
335+
}
336+
if (collapseThinkingIndexSet.has(index)) {
337+
collapseThinkingIndexSet.delete(index);
338+
} else {
339+
collapseThinkingIndexSet.add(index);
340+
}
341+
setCollapseThinkingIndexSet(new Set(collapseThinkingIndexSet));
342+
}}
343+
>
344+
<Icon iconClass='codicon codicon-sparkle' />
345+
{localize('aiNative.chat.thinking')}
346+
{isThinking ? (
347+
<Loading />
348+
) : collapseThinkingIndexSet.has(index) ? (
349+
<Icon iconClass='codicon codicon-chevron-right' />
350+
) : (
351+
<Icon iconClass='codicon codicon-chevron-down' />
352+
)}
353+
</Button>
354+
{!collapseThinkingIndexSet.has(index) ? (
355+
<div className={styles.reasoning_content}>{renderMarkdown(new MarkdownString(item.content))}</div>
356+
) : null}
357+
</div>
358+
);
316359
} else {
317360
node = renderMarkdown(item.content);
318361
}
319362
return <Fragment key={`${item.kind}-${index}`}>{node}</Fragment>;
320363
}),
321-
[request.response.responseContents],
364+
[request.response.responseContents, collapseThinkingIndexSet],
322365
);
323366

324367
const followupNode = React.useMemo(() => {

packages/ai-native/src/browser/components/ChatThinking.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const ChatThinking = (props: ITinkingProps) => {
3333
);
3434

3535
const renderContent = useCallback(() => {
36-
if (!children || !message?.trim()) {
36+
if (!children) {
3737
if (CustomThinkingRender) {
3838
return <CustomThinkingRender thinkingText={thinkingText} />;
3939
}

packages/ai-native/src/browser/components/components.module.less

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,3 +541,27 @@
541541
}
542542
}
543543
}
544+
545+
.reasoning {
546+
.thinking {
547+
display: flex;
548+
align-items: center;
549+
gap: 4px;
550+
margin-bottom: 4px;
551+
transition: color 0.2s ease-in-out;
552+
:global {
553+
.codicon {
554+
color: inherit;
555+
}
556+
.codicon-sparkle {
557+
margin-right: -1px;
558+
font-size: 14px;
559+
}
560+
}
561+
}
562+
.reasoning_content {
563+
padding-left: 12px;
564+
border-left: 2px solid var(--descriptionForeground);
565+
color: var(--descriptionForeground);
566+
}
567+
}

packages/ai-native/src/node/base-language-model.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ export abstract class BaseLanguageModel {
185185
});
186186
} else if (chunk.type === 'error') {
187187
chatReadableStream.emitError(new Error(chunk.error as string));
188+
} else if (chunk.type === 'reasoning') {
189+
chatReadableStream.emitData({
190+
kind: 'reasoning',
191+
content: chunk.textDelta,
192+
});
188193
}
189194
}
190195

packages/core-common/src/types/ai-native/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,13 +401,19 @@ export interface IChatComponent {
401401
kind: 'component';
402402
}
403403

404+
export interface IChatReasoning {
405+
content: string;
406+
kind: 'reasoning';
407+
}
408+
404409
export type IChatProgress =
405410
| IChatContent
406411
| IChatMarkdownContent
407412
| IChatAsyncContent
408413
| IChatTreeData
409414
| IChatComponent
410-
| IChatToolContent;
415+
| IChatToolContent
416+
| IChatReasoning;
411417

412418
export interface IChatMessage {
413419
role: ChatMessageRole;

packages/i18n/src/common/en-US.lang.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,7 @@ export const localizationBundle = {
14591459
'aiNative.chat.expand.unfullscreen': 'unfullscreen',
14601460
'aiNative.chat.expand.fullescreen': 'fullescreen',
14611461
'aiNative.chat.enter.send': 'Send (Enter)',
1462+
'aiNative.chat.thinking': 'Deep Think',
14621463

14631464
'aiNative.inline.chat.operate.chat.title': 'Chat({0})',
14641465
'aiNative.inline.chat.operate.check.title': 'Check',

packages/i18n/src/common/zh-CN.lang.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,7 @@ export const localizationBundle = {
12271227
'aiNative.chat.expand.unfullscreen': '收起',
12281228
'aiNative.chat.expand.fullescreen': '展开全屏',
12291229
'aiNative.chat.enter.send': 'Enter 发送',
1230+
'aiNative.chat.thinking': '深度思考',
12301231

12311232
'aiNative.inline.chat.operate.chat.title': 'Chat({0})',
12321233
'aiNative.inline.chat.operate.check.title': '采纳',

0 commit comments

Comments
 (0)