From e552bc960929f5d717df24d76f82f0357dea8f1b Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 17 Sep 2025 17:01:06 +0800 Subject: [PATCH] fix: terminal flicker when subagent is executing --- packages/cli/src/ui/App.tsx | 15 ++-- .../src/ui/components/HistoryItemDisplay.tsx | 6 +- .../messages/ToolConfirmationMessage.tsx | 36 +++++++++ .../runtime/AgentExecutionDisplay.tsx | 79 +++++++++++++++---- packages/cli/src/ui/hooks/useGeminiStream.ts | 11 ++- 5 files changed, 120 insertions(+), 27 deletions(-) diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 5039a170..3494f3ce 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -723,8 +723,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { ); const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit); - const pendingHistoryItems = [...pendingSlashCommandHistoryItems]; - pendingHistoryItems.push(...pendingGeminiHistoryItems); + const pendingHistoryItems = useMemo(() => { + const items = [...pendingSlashCommandHistoryItems]; + items.push(...pendingGeminiHistoryItems); + return items.map((item, i) => ({ ...item, id: i })); + }, [pendingSlashCommandHistoryItems, pendingGeminiHistoryItems]); const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(streamingState); @@ -1070,16 +1073,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { - {pendingHistoryItems.map((item, i) => ( + {pendingHistoryItems.map((item) => ( = ({ +const HistoryItemDisplayComponent: React.FC = ({ item, availableTerminalHeight, terminalWidth, @@ -101,3 +101,7 @@ export const HistoryItemDisplay: React.FC = ({ {item.type === 'summary' && } ); + +HistoryItemDisplayComponent.displayName = 'HistoryItemDisplay'; + +export const HistoryItemDisplay = React.memo(HistoryItemDisplayComponent); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index f1113b62..434617bc 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -29,6 +29,7 @@ export interface ToolConfirmationMessageProps { isFocused?: boolean; availableTerminalHeight?: number; terminalWidth: number; + compactMode?: boolean; } export const ToolConfirmationMessage: React.FC< @@ -39,6 +40,7 @@ export const ToolConfirmationMessage: React.FC< isFocused = true, availableTerminalHeight, terminalWidth, + compactMode = false, }) => { const { onConfirm } = confirmationDetails; const childWidth = terminalWidth - 2; // 2 for padding @@ -70,6 +72,40 @@ export const ToolConfirmationMessage: React.FC< const handleSelect = (item: ToolConfirmationOutcome) => handleConfirm(item); + // Compact mode: return simple 3-option display + if (compactMode) { + const compactOptions: Array> = [ + { + label: 'Yes, allow once', + value: ToolConfirmationOutcome.ProceedOnce, + }, + { + label: 'Allow always', + value: ToolConfirmationOutcome.ProceedAlways, + }, + { + label: 'No', + value: ToolConfirmationOutcome.Cancel, + }, + ]; + + return ( + + + Do you want to proceed? + + + + + + ); + } + + // Original logic continues unchanged below let bodyContent: React.ReactNode | null = null; // Removed contextDisplay here let question: string; diff --git a/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx b/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx index 355e7eae..a1beb922 100644 --- a/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx +++ b/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx @@ -17,12 +17,12 @@ import { COLOR_OPTIONS } from '../constants.js'; import { fmtDuration } from '../utils.js'; import { ToolConfirmationMessage } from '../../messages/ToolConfirmationMessage.js'; -export type DisplayMode = 'default' | 'verbose'; +export type DisplayMode = 'compact' | 'default' | 'verbose'; export interface AgentExecutionDisplayProps { data: TaskResultDisplay; availableHeight?: number; - childWidth?: number; + childWidth: number; } const getStatusColor = ( @@ -76,8 +76,8 @@ export const AgentExecutionDisplay: React.FC = ({ data, availableHeight, childWidth, -}) => { - const [displayMode, setDisplayMode] = React.useState('default'); +}: AgentExecutionDisplayProps) => { + const [displayMode, setDisplayMode] = React.useState('compact'); const agentColor = useMemo(() => { const colorOption = COLOR_OPTIONS.find( @@ -90,8 +90,6 @@ export const AgentExecutionDisplay: React.FC = ({ // This component only listens to keyboard shortcut events when the subagent is running if (data.status !== 'running') return ''; - if (displayMode === 'verbose') return 'Press ctrl+r to show less.'; - if (displayMode === 'default') { const hasMoreLines = data.taskPrompt.split('\n').length > MAX_TASK_PROMPT_LINES; @@ -99,17 +97,28 @@ export const AgentExecutionDisplay: React.FC = ({ data.toolCalls && data.toolCalls.length > MAX_TOOL_CALLS; if (hasMoreToolCalls || hasMoreLines) { - return 'Press ctrl+r to show more.'; + return 'Press ctrl+r to show less, ctrl+e to show more.'; } - return ''; + return 'Press ctrl+r to show less.'; } - return ''; - }, [displayMode, data.toolCalls, data.taskPrompt, data.status]); - // Handle ctrl+r keypresses to control display mode + if (displayMode === 'verbose') { + return 'Press ctrl+e to show less.'; + } + + return ''; + }, [displayMode, data]); + + // Handle keyboard shortcuts to control display mode useKeypress( (key) => { if (key.ctrl && key.name === 'r') { + // ctrl+r toggles between compact and default + setDisplayMode((current) => + current === 'compact' ? 'default' : 'compact', + ); + } else if (key.ctrl && key.name === 'e') { + // ctrl+e toggles between default and verbose setDisplayMode((current) => current === 'default' ? 'verbose' : 'default', ); @@ -118,6 +127,44 @@ export const AgentExecutionDisplay: React.FC = ({ { isActive: true }, ); + if (displayMode === 'compact') { + return ( + + {data.toolCalls && data.toolCalls.length > 0 && ( + + + {/* Show count of additional tool calls if there are more than 1 */} + {data.toolCalls.length > 1 && !data.pendingConfirmation && ( + + + +{data.toolCalls.length - 1} more tool calls{' '} + {data.status === 'running' ? '(ctrl+r to expand)' : ''} + + + )} + + )} + + {/* Inline approval prompt when awaiting confirmation */} + {data.pendingConfirmation && ( + + + + )} + + ); + } + + // Default and verbose modes use normal layout return ( {/* Header with subagent name and status */} @@ -154,7 +201,8 @@ export const AgentExecutionDisplay: React.FC = ({ confirmationDetails={data.pendingConfirmation} isFocused={true} availableTerminalHeight={availableHeight} - terminalWidth={childWidth ?? 80} + terminalWidth={childWidth} + compactMode={true} /> )} @@ -276,7 +324,8 @@ const ToolCallItem: React.FC<{ resultDisplay?: string; description?: string; }; -}> = ({ toolCall }) => { + compact?: boolean; +}> = ({ toolCall, compact = false }) => { const STATUS_INDICATOR_WIDTH = 3; // Map subagent status to ToolCallStatus-like display @@ -331,8 +380,8 @@ const ToolCallItem: React.FC<{ - {/* Second line: truncated returnDisplay output */} - {truncatedOutput && ( + {/* Second line: truncated returnDisplay output - hidden in compact mode */} + {!compact && truncatedOutput && ( {truncatedOutput} diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 282e8a1c..fd633ac0 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -901,10 +901,13 @@ export const useGeminiStream = ( ], ); - const pendingHistoryItems = [ - pendingHistoryItemRef.current, - pendingToolCallGroupDisplay, - ].filter((i) => i !== undefined && i !== null); + const pendingHistoryItems = useMemo( + () => + [pendingHistoryItemRef.current, pendingToolCallGroupDisplay].filter( + (i) => i !== undefined && i !== null, + ), + [pendingHistoryItemRef, pendingToolCallGroupDisplay], + ); useEffect(() => { const saveRestorableToolCalls = async () => {