DeepSeek V3.2 Thinking Mode Integration (#1134)

This commit is contained in:
tanzhenxin
2025-12-05 15:08:35 +08:00
committed by GitHub
parent a58d3f7aaf
commit 3e2a2255ee
24 changed files with 752 additions and 107 deletions

View File

@@ -19,12 +19,16 @@ const UNDERLINE_TAG_END_LENGTH = 4; // For "</u>"
interface RenderInlineProps {
text: string;
textColor?: string;
}
const RenderInlineInternal: React.FC<RenderInlineProps> = ({ text }) => {
const RenderInlineInternal: React.FC<RenderInlineProps> = ({
text,
textColor = theme.text.primary,
}) => {
// Early return for plain text without markdown or URLs
if (!/[*_~`<[https?:]/.test(text)) {
return <Text color={theme.text.primary}>{text}</Text>;
return <Text color={textColor}>{text}</Text>;
}
const nodes: React.ReactNode[] = [];

View File

@@ -17,6 +17,7 @@ interface MarkdownDisplayProps {
isPending: boolean;
availableTerminalHeight?: number;
terminalWidth: number;
textColor?: string;
}
// Constants for Markdown parsing and rendering
@@ -31,6 +32,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
isPending,
availableTerminalHeight,
terminalWidth,
textColor = theme.text.primary,
}) => {
if (!text) return <></>;
@@ -116,7 +118,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
addContentBlock(
<Box key={key}>
<Text wrap="wrap">
<RenderInline text={line} />
<RenderInline text={line} textColor={textColor} />
</Text>
</Box>,
);
@@ -155,7 +157,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
addContentBlock(
<Box key={key}>
<Text wrap="wrap">
<RenderInline text={line} />
<RenderInline text={line} textColor={textColor} />
</Text>
</Box>,
);
@@ -173,36 +175,36 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
switch (level) {
case 1:
headerNode = (
<Text bold color={theme.text.link}>
<RenderInline text={headerText} />
<Text bold color={textColor}>
<RenderInline text={headerText} textColor={textColor} />
</Text>
);
break;
case 2:
headerNode = (
<Text bold color={theme.text.link}>
<RenderInline text={headerText} />
<Text bold color={textColor}>
<RenderInline text={headerText} textColor={textColor} />
</Text>
);
break;
case 3:
headerNode = (
<Text bold color={theme.text.primary}>
<RenderInline text={headerText} />
<Text bold color={textColor}>
<RenderInline text={headerText} textColor={textColor} />
</Text>
);
break;
case 4:
headerNode = (
<Text italic color={theme.text.secondary}>
<RenderInline text={headerText} />
<Text italic color={textColor}>
<RenderInline text={headerText} textColor={textColor} />
</Text>
);
break;
default:
headerNode = (
<Text color={theme.text.primary}>
<RenderInline text={headerText} />
<Text color={textColor}>
<RenderInline text={headerText} textColor={textColor} />
</Text>
);
break;
@@ -219,6 +221,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
type="ul"
marker={marker}
leadingWhitespace={leadingWhitespace}
textColor={textColor}
/>,
);
} else if (olMatch) {
@@ -232,6 +235,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
type="ol"
marker={marker}
leadingWhitespace={leadingWhitespace}
textColor={textColor}
/>,
);
} else {
@@ -245,8 +249,8 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
} else {
addContentBlock(
<Box key={key}>
<Text wrap="wrap" color={theme.text.primary}>
<RenderInline text={line} />
<Text wrap="wrap" color={textColor}>
<RenderInline text={line} textColor={textColor} />
</Text>
</Box>,
);
@@ -367,6 +371,7 @@ interface RenderListItemProps {
type: 'ul' | 'ol';
marker: string;
leadingWhitespace?: string;
textColor?: string;
}
const RenderListItemInternal: React.FC<RenderListItemProps> = ({
@@ -374,6 +379,7 @@ const RenderListItemInternal: React.FC<RenderListItemProps> = ({
type,
marker,
leadingWhitespace = '',
textColor = theme.text.primary,
}) => {
const prefix = type === 'ol' ? `${marker}. ` : `${marker} `;
const prefixWidth = prefix.length;
@@ -385,11 +391,11 @@ const RenderListItemInternal: React.FC<RenderListItemProps> = ({
flexDirection="row"
>
<Box width={prefixWidth}>
<Text color={theme.text.primary}>{prefix}</Text>
<Text color={textColor}>{prefix}</Text>
</Box>
<Box flexGrow={LIST_ITEM_TEXT_FLEX_GROW}>
<Text wrap="wrap" color={theme.text.primary}>
<RenderInline text={itemText} />
<Text wrap="wrap" color={textColor}>
<RenderInline text={itemText} textColor={textColor} />
</Text>
</Box>
</Box>

View File

@@ -102,7 +102,7 @@ describe('resumeHistoryUtils', () => {
]);
});
it('marks tool results as error, skips thought text, and falls back when tool is missing', () => {
it('marks tool results as error, captures thought text, and falls back when tool is missing', () => {
const conversation = {
messages: [
{
@@ -142,6 +142,11 @@ describe('resumeHistoryUtils', () => {
const items = buildResumedHistoryItems(session, makeConfig({}));
expect(items).toEqual([
{
id: expect.any(Number),
type: 'gemini_thought',
text: 'should be skipped',
},
{ id: expect.any(Number), type: 'gemini', text: 'visible text' },
{
id: expect.any(Number),

View File

@@ -17,7 +17,7 @@ import type { HistoryItem, HistoryItemWithoutId } from '../types.js';
import { ToolCallStatus } from '../types.js';
/**
* Extracts text content from a Content object's parts.
* Extracts text content from a Content object's parts (excluding thought parts).
*/
function extractTextFromParts(parts: Part[] | undefined): string {
if (!parts) return '';
@@ -34,6 +34,22 @@ function extractTextFromParts(parts: Part[] | undefined): string {
return textParts.join('\n');
}
/**
* Extracts thought text content from a Content object's parts.
* Thought parts are identified by having `thought: true`.
*/
function extractThoughtTextFromParts(parts: Part[] | undefined): string {
if (!parts) return '';
const thoughtParts: string[] = [];
for (const part of parts) {
if ('text' in part && part.text && 'thought' in part && part.thought) {
thoughtParts.push(part.text);
}
}
return thoughtParts.join('\n');
}
/**
* Extracts function calls from a Content object's parts.
*/
@@ -187,12 +203,28 @@ function convertToHistoryItems(
case 'assistant': {
const parts = record.message?.parts as Part[] | undefined;
// Extract thought content
const thoughtText = extractThoughtTextFromParts(parts);
// Extract text content (non-function-call, non-thought)
const text = extractTextFromParts(parts);
// Extract function calls
const functionCalls = extractFunctionCalls(parts);
// If there's thought content, add it as a gemini_thought message
if (thoughtText) {
// Flush any pending tool group before thought
if (currentToolGroup.length > 0) {
items.push({
type: 'tool_group',
tools: [...currentToolGroup],
});
currentToolGroup = [];
}
items.push({ type: 'gemini_thought', text: thoughtText });
}
// If there's text content, add it as a gemini message
if (text) {
// Flush any pending tool group before text