mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
DeepSeek V3.2 Thinking Mode Integration (#1134)
This commit is contained in:
@@ -245,6 +245,7 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Test input' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-1',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('Hello');
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith(' World');
|
||||
@@ -293,11 +294,21 @@ describe('runNonInteractive', () => {
|
||||
expect.any(AbortSignal),
|
||||
undefined,
|
||||
);
|
||||
// Verify first call has isContinuation: false
|
||||
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[{ text: 'Use a tool' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-2',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
// Verify second call (after tool execution) has isContinuation: true
|
||||
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[{ text: 'Tool response' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-2',
|
||||
{ isContinuation: true },
|
||||
);
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('Final answer');
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('\n');
|
||||
@@ -372,6 +383,7 @@ describe('runNonInteractive', () => {
|
||||
],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-3',
|
||||
{ isContinuation: true },
|
||||
);
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('Sorry, let me try again.');
|
||||
});
|
||||
@@ -497,6 +509,7 @@ describe('runNonInteractive', () => {
|
||||
processedParts,
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-7',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
|
||||
// 6. Assert the final output is correct
|
||||
@@ -528,6 +541,7 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Test input' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-1',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
|
||||
// JSON adapter emits array of messages, last one is result with stats
|
||||
@@ -680,6 +694,7 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Empty response test' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-empty',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
|
||||
// JSON adapter emits array of messages, last one is result with stats
|
||||
@@ -831,6 +846,7 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Prompt from command' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-slash',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('Response from command');
|
||||
@@ -887,6 +903,7 @@ describe('runNonInteractive', () => {
|
||||
[{ text: '/unknowncommand' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-unknown',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('Response to unknown');
|
||||
@@ -1217,6 +1234,7 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Message from stream-json input' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-envelope',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1692,6 +1710,7 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Simple string content' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-string-content',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
|
||||
// UserMessage with array of text blocks
|
||||
@@ -1724,6 +1743,7 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'First part' }, { text: 'Second part' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-blocks-content',
|
||||
{ isContinuation: false },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -172,6 +172,7 @@ export async function runNonInteractive(
|
||||
adapter.emitMessage(systemMessage);
|
||||
}
|
||||
|
||||
let isFirstTurn = true;
|
||||
while (true) {
|
||||
turnCount++;
|
||||
if (
|
||||
@@ -187,7 +188,9 @@ export async function runNonInteractive(
|
||||
currentMessages[0]?.parts || [],
|
||||
abortController.signal,
|
||||
prompt_id,
|
||||
{ isContinuation: !isFirstTurn },
|
||||
);
|
||||
isFirstTurn = false;
|
||||
|
||||
// Start assistant message for this turn
|
||||
if (adapter) {
|
||||
@@ -207,7 +210,9 @@ export async function runNonInteractive(
|
||||
}
|
||||
} else {
|
||||
// Text output mode - direct stdout
|
||||
if (event.type === GeminiEventType.Content) {
|
||||
if (event.type === GeminiEventType.Thought) {
|
||||
process.stdout.write(event.value.description);
|
||||
} else if (event.type === GeminiEventType.Content) {
|
||||
process.stdout.write(event.value);
|
||||
} else if (event.type === GeminiEventType.ToolCallRequest) {
|
||||
toolCallRequests.push(event.value);
|
||||
|
||||
@@ -15,6 +15,8 @@ import { InfoMessage } from './messages/InfoMessage.js';
|
||||
import { ErrorMessage } from './messages/ErrorMessage.js';
|
||||
import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
|
||||
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
|
||||
import { GeminiThoughtMessage } from './messages/GeminiThoughtMessage.js';
|
||||
import { GeminiThoughtMessageContent } from './messages/GeminiThoughtMessageContent.js';
|
||||
import { CompressionMessage } from './messages/CompressionMessage.js';
|
||||
import { SummaryMessage } from './messages/SummaryMessage.js';
|
||||
import { WarningMessage } from './messages/WarningMessage.js';
|
||||
@@ -85,6 +87,26 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_thought' && (
|
||||
<GeminiThoughtMessage
|
||||
text={itemForDisplay.text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={
|
||||
availableTerminalHeightGemini ?? availableTerminalHeight
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_thought_content' && (
|
||||
<GeminiThoughtMessageContent
|
||||
text={itemForDisplay.text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={
|
||||
availableTerminalHeightGemini ?? availableTerminalHeight
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'info' && (
|
||||
<InfoMessage text={itemForDisplay.text} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
interface GeminiThoughtMessageProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays model thinking/reasoning text with a softer, dimmed style
|
||||
* to visually distinguish it from regular content output.
|
||||
*/
|
||||
export const GeminiThoughtMessage: React.FC<GeminiThoughtMessageProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
}) => {
|
||||
const prefix = '✦ ';
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.text.secondary}>{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="column">
|
||||
<MarkdownDisplay
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
textColor={theme.text.secondary}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box } from 'ink';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
interface GeminiThoughtMessageContentProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continuation component for thought messages, similar to GeminiMessageContent.
|
||||
* Used when a thought response gets too long and needs to be split for performance.
|
||||
*/
|
||||
export const GeminiThoughtMessageContent: React.FC<
|
||||
GeminiThoughtMessageContentProps
|
||||
> = ({ text, isPending, availableTerminalHeight, terminalWidth }) => {
|
||||
const originalPrefix = '✦ ';
|
||||
const prefixWidth = originalPrefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={prefixWidth} marginBottom={1}>
|
||||
<MarkdownDisplay
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
textColor={theme.text.secondary}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -2261,6 +2261,57 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should accumulate streamed thought descriptions', async () => {
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Thought,
|
||||
value: { subject: '', description: 'thinking ' },
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.Thought,
|
||||
value: { subject: '', description: 'more' },
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.Finished,
|
||||
value: { reason: 'STOP', usageMetadata: undefined },
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useGeminiStream(
|
||||
new MockedGeminiClientClass(mockConfig),
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockLoadedSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => 'vscode' as EditorType,
|
||||
() => {},
|
||||
() => Promise.resolve(),
|
||||
false,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
false, // visionModelPreviewEnabled
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Streamed thought');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.thought?.description).toBe('thinking more');
|
||||
});
|
||||
});
|
||||
|
||||
it('should memoize pendingHistoryItems', () => {
|
||||
mockUseReactToolScheduler.mockReturnValue([
|
||||
[],
|
||||
|
||||
@@ -497,6 +497,61 @@ export const useGeminiStream = (
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
||||
);
|
||||
|
||||
const mergeThought = useCallback(
|
||||
(incoming: ThoughtSummary) => {
|
||||
setThought((prev) => {
|
||||
if (!prev) {
|
||||
return incoming;
|
||||
}
|
||||
const subject = incoming.subject || prev.subject;
|
||||
const description = `${prev.description ?? ''}${incoming.description ?? ''}`;
|
||||
return { subject, description };
|
||||
});
|
||||
},
|
||||
[setThought],
|
||||
);
|
||||
|
||||
const handleThoughtEvent = useCallback(
|
||||
(
|
||||
eventValue: ThoughtSummary,
|
||||
currentThoughtBuffer: string,
|
||||
userMessageTimestamp: number,
|
||||
): string => {
|
||||
if (turnCancelledRef.current) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Extract the description text from the thought summary
|
||||
const thoughtText = eventValue.description ?? '';
|
||||
if (!thoughtText) {
|
||||
return currentThoughtBuffer;
|
||||
}
|
||||
|
||||
const newThoughtBuffer = currentThoughtBuffer + thoughtText;
|
||||
|
||||
// If we're not already showing a thought, start a new one
|
||||
if (pendingHistoryItemRef.current?.type !== 'gemini_thought') {
|
||||
// If there's a pending non-thought item, finalize it first
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
}
|
||||
setPendingHistoryItem({ type: 'gemini_thought', text: '' });
|
||||
}
|
||||
|
||||
// Update the existing thought message with accumulated content
|
||||
setPendingHistoryItem({
|
||||
type: 'gemini_thought',
|
||||
text: newThoughtBuffer,
|
||||
});
|
||||
|
||||
// Also update the thought state for the loading indicator
|
||||
mergeThought(eventValue);
|
||||
|
||||
return newThoughtBuffer;
|
||||
},
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem, mergeThought],
|
||||
);
|
||||
|
||||
const handleUserCancelledEvent = useCallback(
|
||||
(userMessageTimestamp: number) => {
|
||||
if (turnCancelledRef.current) {
|
||||
@@ -710,11 +765,16 @@ export const useGeminiStream = (
|
||||
signal: AbortSignal,
|
||||
): Promise<StreamProcessingStatus> => {
|
||||
let geminiMessageBuffer = '';
|
||||
let thoughtBuffer = '';
|
||||
const toolCallRequests: ToolCallRequestInfo[] = [];
|
||||
for await (const event of stream) {
|
||||
switch (event.type) {
|
||||
case ServerGeminiEventType.Thought:
|
||||
setThought(event.value);
|
||||
thoughtBuffer = handleThoughtEvent(
|
||||
event.value,
|
||||
thoughtBuffer,
|
||||
userMessageTimestamp,
|
||||
);
|
||||
break;
|
||||
case ServerGeminiEventType.Content:
|
||||
geminiMessageBuffer = handleContentEvent(
|
||||
@@ -776,6 +836,7 @@ export const useGeminiStream = (
|
||||
},
|
||||
[
|
||||
handleContentEvent,
|
||||
handleThoughtEvent,
|
||||
handleUserCancelledEvent,
|
||||
handleErrorEvent,
|
||||
scheduleToolCalls,
|
||||
|
||||
@@ -103,6 +103,16 @@ export type HistoryItemGeminiContent = HistoryItemBase & {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type HistoryItemGeminiThought = HistoryItemBase & {
|
||||
type: 'gemini_thought';
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type HistoryItemGeminiThoughtContent = HistoryItemBase & {
|
||||
type: 'gemini_thought_content';
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type HistoryItemInfo = HistoryItemBase & {
|
||||
type: 'info';
|
||||
text: string;
|
||||
@@ -241,6 +251,8 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemUserShell
|
||||
| HistoryItemGemini
|
||||
| HistoryItemGeminiContent
|
||||
| HistoryItemGeminiThought
|
||||
| HistoryItemGeminiThoughtContent
|
||||
| HistoryItemInfo
|
||||
| HistoryItemError
|
||||
| HistoryItemWarning
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user