diff options
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/ui/components/messages/ToolGroupMessage.tsx | 53 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/messages/ToolMessage.tsx | 158 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useToolScheduler.ts | 81 |
3 files changed, 186 insertions, 106 deletions
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index 4b2c7dfe..8bcde3bb 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -29,6 +29,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({ const staticHeight = /* border */ 2 + /* marginBottom */ 1; + // only prompt for tool approval on the first 'confirming' tool in the list + // note, after the CTA, this automatically moves over to the next 'confirming' tool const toolAwaitingApproval = useMemo( () => toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming), [toolCalls], @@ -50,27 +52,38 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({ borderColor={borderColor} marginBottom={1} > - {toolCalls.map((tool) => ( - <Box key={tool.callId} flexDirection="column"> - <ToolMessage - key={tool.callId} - callId={tool.callId} - name={tool.name} - description={tool.description} - resultDisplay={tool.resultDisplay} - status={tool.status} - confirmationDetails={tool.confirmationDetails} - availableTerminalHeight={availableTerminalHeight - staticHeight} - /> - {tool.status === ToolCallStatus.Confirming && - tool.callId === toolAwaitingApproval?.callId && - tool.confirmationDetails && ( - <ToolConfirmationMessage + {toolCalls.map((tool) => { + const isConfirming = toolAwaitingApproval?.callId === tool.callId; + return ( + <Box key={tool.callId} flexDirection="column"> + <Box flexDirection="row" alignItems="center"> + <ToolMessage + callId={tool.callId} + name={tool.name} + description={tool.description} + resultDisplay={tool.resultDisplay} + status={tool.status} confirmationDetails={tool.confirmationDetails} - ></ToolConfirmationMessage> - )} - </Box> - ))} + availableTerminalHeight={availableTerminalHeight - staticHeight} + emphasis={ + isConfirming + ? 'high' + : toolAwaitingApproval + ? 'low' + : 'medium' + } + /> + </Box> + {tool.status === ToolCallStatus.Confirming && + isConfirming && + tool.confirmationDetails && ( + <ToolConfirmationMessage + confirmationDetails={tool.confirmationDetails} + /> + )} + </Box> + ); + })} </Box> ); }; diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx index 32b23b9e..32b3b7e8 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx @@ -12,8 +12,15 @@ import { DiffRenderer } from './DiffRenderer.js'; import { Colors } from '../../colors.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; +const STATIC_HEIGHT = 1; +const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc. +const STATUS_INDICATOR_WIDTH = 3; + +export type TextEmphasis = 'high' | 'medium' | 'low'; + export interface ToolMessageProps extends IndividualToolCallDisplay { availableTerminalHeight: number; + emphasis?: TextEmphasis; } export const ToolMessage: React.FC<ToolMessageProps> = ({ @@ -22,63 +29,45 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({ resultDisplay, status, availableTerminalHeight, + emphasis = 'medium', }) => { - const statusIndicatorWidth = 3; - const hasResult = resultDisplay && resultDisplay.toString().trim().length > 0; - const staticHeight = /* Header */ 1; - - let displayableResult = resultDisplay; - let hiddenLines = 0; + const contentHeightEstimate = + availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT; + const resultIsString = + typeof resultDisplay === 'string' && resultDisplay.trim().length > 0; + const lines = React.useMemo( + () => (resultIsString ? resultDisplay.split('\n') : []), + [resultIsString, resultDisplay], + ); // Truncate the overall string content if it's too long. // MarkdownRenderer will handle specific truncation for code blocks within this content. - if (typeof resultDisplay === 'string' && resultDisplay.length > 0) { - const lines = resultDisplay.split('\n'); - // Estimate available height for this specific tool message content area - // This is a rough estimate; ideally, we'd have a more precise measurement. - const contentHeightEstimate = availableTerminalHeight - staticHeight - 5; // Subtracting lines for tool name, status, padding etc. - if (lines.length > contentHeightEstimate && contentHeightEstimate > 0) { - displayableResult = lines.slice(0, contentHeightEstimate).join('\n'); - hiddenLines = lines.length - contentHeightEstimate; - } - } + // Estimate available height for this specific tool message content area + // This is a rough estimate; ideally, we'd have a more precise measurement. + const displayableResult = React.useMemo( + () => + resultIsString + ? lines.slice(0, contentHeightEstimate).join('\n') + : resultDisplay, + [lines, resultIsString, contentHeightEstimate, resultDisplay], + ); + const hiddenLines = lines.length - contentHeightEstimate; return ( <Box paddingX={1} paddingY={0} flexDirection="column"> <Box minHeight={1}> {/* Status Indicator */} - <Box minWidth={statusIndicatorWidth}> - {(status === ToolCallStatus.Pending || - status === ToolCallStatus.Executing) && <Spinner type="dots" />} - {status === ToolCallStatus.Success && ( - <Text color={Colors.AccentGreen}>✔</Text> - )} - {status === ToolCallStatus.Confirming && ( - <Text color={Colors.AccentYellow}>?</Text> - )} - {status === ToolCallStatus.Canceled && ( - <Text color={Colors.AccentYellow} bold> - - - </Text> - )} - {status === ToolCallStatus.Error && ( - <Text color={Colors.AccentRed} bold> - x - </Text> - )} - </Box> - <Box> - <Text - wrap="truncate-end" - strikethrough={status === ToolCallStatus.Canceled} - > - <Text bold>{name}</Text>{' '} - <Text color={Colors.SubtleComment}>{description}</Text> - </Text> - </Box> + <ToolStatusIndicator status={status} /> + <ToolInfo + name={name} + status={status} + description={description} + emphasis={emphasis} + /> + {emphasis === 'high' && <TrailingIndicator />} </Box> - {hasResult && ( - <Box paddingLeft={statusIndicatorWidth} width="100%"> + {displayableResult && ( + <Box paddingLeft={STATUS_INDICATOR_WIDTH} width="100%"> <Box flexDirection="column"> {typeof displayableResult === 'string' && ( <Box flexDirection="column"> @@ -89,7 +78,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({ /> </Box> )} - {typeof displayableResult === 'object' && ( + {typeof displayableResult !== 'string' && ( <DiffRenderer diffContent={displayableResult.fileDiff} filename={displayableResult.fileName} @@ -109,3 +98,76 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({ </Box> ); }; + +type ToolStatusIndicator = { + status: ToolCallStatus; +}; +const ToolStatusIndicator: React.FC<ToolStatusIndicator> = ({ status }) => ( + <Box minWidth={STATUS_INDICATOR_WIDTH}> + {status === ToolCallStatus.Pending && ( + <Text color={Colors.AccentGreen}>o</Text> + )} + {status === ToolCallStatus.Executing && <Spinner type="dots" />} + {status === ToolCallStatus.Success && ( + <Text color={Colors.AccentGreen}>✔</Text> + )} + {status === ToolCallStatus.Confirming && ( + <Text color={Colors.AccentYellow}>?</Text> + )} + {status === ToolCallStatus.Canceled && ( + <Text color={Colors.AccentYellow} bold> + - + </Text> + )} + {status === ToolCallStatus.Error && ( + <Text color={Colors.AccentRed} bold> + x + </Text> + )} + </Box> +); + +type ToolInfo = { + name: string; + description: string; + status: ToolCallStatus; + emphasis: TextEmphasis; +}; +const ToolInfo: React.FC<ToolInfo> = ({ + name, + description, + status, + emphasis, +}) => { + const nameColor = React.useMemo<string>(() => { + switch (emphasis) { + case 'high': + return Colors.Foreground; + case 'medium': + return Colors.Foreground; + case 'low': + return Colors.SubtleComment; + default: { + const exhaustiveCheck: never = emphasis; + return exhaustiveCheck; + } + } + }, [emphasis]); + return ( + <Box> + <Text + wrap="truncate-end" + strikethrough={status === ToolCallStatus.Canceled} + > + <Text color={nameColor} bold> + {name} + </Text>{' '} + <Text color={Colors.SubtleComment}>{description}</Text> + </Text> + </Box> + ); +}; + +const TrailingIndicator: React.FC = () => ( + <Text color={Colors.Foreground}> ←</Text> +); diff --git a/packages/cli/src/ui/hooks/useToolScheduler.ts b/packages/cli/src/ui/hooks/useToolScheduler.ts index 8bcc0ae9..a5770d36 100644 --- a/packages/cli/src/ui/hooks/useToolScheduler.ts +++ b/packages/cli/src/ui/hooks/useToolScheduler.ts @@ -184,48 +184,53 @@ export function useToolScheduler( useEffect(() => { // effect for executing scheduled tool calls - if (toolCalls.every((t) => t.status === 'scheduled')) { + const allToolsConfirmed = toolCalls.every( + (t) => t.status === 'scheduled' || t.status === 'cancelled', + ); + if (allToolsConfirmed) { const signal = abortController.signal; - toolCalls.forEach((c) => { - const callId = c.request.callId; - setToolCalls(setStatus(c.request.callId, 'executing')); - c.tool - .execute(c.request.args, signal) - .then((result) => { - if (signal.aborted) { - setToolCalls( - setStatus(callId, 'cancelled', 'Cancelled during execution'), - ); - return; - } - const functionResponse: Part = { - functionResponse: { - name: c.request.name, - id: callId, - response: { output: result.llmContent }, - }, - }; - const response: ToolCallResponseInfo = { - callId, - responsePart: functionResponse, - resultDisplay: result.returnDisplay, - error: undefined, - }; - setToolCalls(setStatus(callId, 'success', response)); - }) - .catch((e) => - setToolCalls( - setStatus( + toolCalls + .filter((t) => t.status === 'scheduled') + .forEach((t) => { + const callId = t.request.callId; + setToolCalls(setStatus(t.request.callId, 'executing')); + t.tool + .execute(t.request.args, signal) + .then((result) => { + if (signal.aborted) { + setToolCalls( + setStatus(callId, 'cancelled', 'Cancelled during execution'), + ); + return; + } + const functionResponse: Part = { + functionResponse: { + name: t.request.name, + id: callId, + response: { output: result.llmContent }, + }, + }; + const response: ToolCallResponseInfo = { callId, - 'error', - toolErrorResponse( - c.request, - e instanceof Error ? e : new Error(String(e)), + responsePart: functionResponse, + resultDisplay: result.returnDisplay, + error: undefined, + }; + setToolCalls(setStatus(callId, 'success', response)); + }) + .catch((e) => + setToolCalls( + setStatus( + callId, + 'error', + toolErrorResponse( + t.request, + e instanceof Error ? e : new Error(String(e)), + ), ), ), - ), - ); - }); + ); + }); } }, [toolCalls, toolRegistry, abortController.signal]); |
