🚀 Add Todo Write Tool for Task Management and Progress Tracking (#478)

This commit is contained in:
tanzhenxin
2025-08-28 20:32:21 +08:00
committed by GitHub
parent c1498668b6
commit 1610c1586e
13 changed files with 1901 additions and 103 deletions

View File

@@ -12,6 +12,8 @@ import { Colors } from '../../colors.js';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { TodoDisplay } from '../TodoDisplay.js';
import { TodoResultDisplay } from '@qwen-code/qwen-code-core';
const STATIC_HEIGHT = 1;
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
@@ -23,6 +25,116 @@ const MIN_LINES_SHOWN = 2; // show at least this many lines
const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 1000000;
export type TextEmphasis = 'high' | 'medium' | 'low';
type DisplayRendererResult =
| { type: 'none' }
| { type: 'todo'; data: TodoResultDisplay }
| { type: 'string'; data: string }
| { type: 'diff'; data: { fileDiff: string; fileName: string } };
/**
* Custom hook to determine the type of result display and return appropriate rendering info
*/
const useResultDisplayRenderer = (
resultDisplay: unknown,
): DisplayRendererResult =>
React.useMemo(() => {
if (!resultDisplay) {
return { type: 'none' };
}
// Check for TodoResultDisplay
if (
typeof resultDisplay === 'object' &&
resultDisplay !== null &&
'type' in resultDisplay &&
resultDisplay.type === 'todo_list'
) {
return {
type: 'todo',
data: resultDisplay as TodoResultDisplay,
};
}
// Check for FileDiff
if (
typeof resultDisplay === 'object' &&
resultDisplay !== null &&
'fileDiff' in resultDisplay
) {
return {
type: 'diff',
data: resultDisplay as { fileDiff: string; fileName: string },
};
}
// Default to string
return {
type: 'string',
data: resultDisplay as string,
};
}, [resultDisplay]);
/**
* Component to render todo list results
*/
const TodoResultRenderer: React.FC<{ data: TodoResultDisplay }> = ({
data,
}) => <TodoDisplay todos={data.todos} />;
/**
* Component to render string results (markdown or plain text)
*/
const StringResultRenderer: React.FC<{
data: string;
renderAsMarkdown: boolean;
availableHeight?: number;
childWidth: number;
}> = ({ data, renderAsMarkdown, availableHeight, childWidth }) => {
let displayData = data;
// Truncate if too long
if (displayData.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
displayData = '...' + displayData.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
}
if (renderAsMarkdown) {
return (
<Box flexDirection="column">
<MarkdownDisplay
text={displayData}
isPending={false}
availableTerminalHeight={availableHeight}
terminalWidth={childWidth}
/>
</Box>
);
}
return (
<MaxSizedBox maxHeight={availableHeight} maxWidth={childWidth}>
<Box>
<Text wrap="wrap">{displayData}</Text>
</Box>
</MaxSizedBox>
);
};
/**
* Component to render diff results
*/
const DiffResultRenderer: React.FC<{
data: { fileDiff: string; fileName: string };
availableHeight?: number;
childWidth: number;
}> = ({ data, availableHeight, childWidth }) => (
<DiffRenderer
diffContent={data.fileDiff}
filename={data.fileName}
availableTerminalHeight={availableHeight}
terminalWidth={childWidth}
/>
);
export interface ToolMessageProps extends IndividualToolCallDisplay {
availableTerminalHeight?: number;
terminalWidth: number;
@@ -55,13 +167,10 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
}
const childWidth = terminalWidth - 3; // account for padding.
if (typeof resultDisplay === 'string') {
if (resultDisplay.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
// Truncate the result display to fit within the available width.
resultDisplay =
'...' + resultDisplay.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
}
}
// Use the custom hook to determine the display type
const displayRenderer = useResultDisplayRenderer(resultDisplay);
return (
<Box paddingX={1} paddingY={0} flexDirection="column">
<Box minHeight={1}>
@@ -74,32 +183,25 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
/>
{emphasis === 'high' && <TrailingIndicator />}
</Box>
{resultDisplay && (
{displayRenderer.type !== 'none' && (
<Box paddingLeft={STATUS_INDICATOR_WIDTH} width="100%" marginTop={1}>
<Box flexDirection="column">
{typeof resultDisplay === 'string' && renderOutputAsMarkdown && (
<Box flexDirection="column">
<MarkdownDisplay
text={resultDisplay}
isPending={false}
availableTerminalHeight={availableHeight}
terminalWidth={childWidth}
/>
</Box>
{displayRenderer.type === 'todo' && (
<TodoResultRenderer data={displayRenderer.data} />
)}
{typeof resultDisplay === 'string' && !renderOutputAsMarkdown && (
<MaxSizedBox maxHeight={availableHeight} maxWidth={childWidth}>
<Box>
<Text wrap="wrap">{resultDisplay}</Text>
</Box>
</MaxSizedBox>
{displayRenderer.type === 'string' && (
<StringResultRenderer
data={displayRenderer.data}
renderAsMarkdown={renderOutputAsMarkdown}
availableHeight={availableHeight}
childWidth={childWidth}
/>
)}
{typeof resultDisplay !== 'string' && (
<DiffRenderer
diffContent={resultDisplay.fileDiff}
filename={resultDisplay.fileName}
availableTerminalHeight={availableHeight}
terminalWidth={childWidth}
{displayRenderer.type === 'diff' && (
<DiffResultRenderer
data={displayRenderer.data}
availableHeight={availableHeight}
childWidth={childWidth}
/>
)}
</Box>