From 6d053e422779c1887920a29a1c1af541033957b2 Mon Sep 17 00:00:00 2001 From: Jarrod Whelan Date: Tue, 10 Feb 2026 03:12:41 -0800 Subject: [PATCH] feat(cli): decouple history filtering from tool output layout - Introduce 'ui.enableCompactToolOutput' (default: true) to control tool output layout. - Separated the overloaded 'output.verbosity' setting into two distinct concerns: 'output.verbosity' now strictly filters history content, while 'ui.enableCompactToolOutput' toggles between dense and boxed layouts. - Update useGeminiStream and ToolGroupMessage to respect the new architectural separation. --- packages/cli/src/config/settingsSchema.ts | 10 ++++ .../components/messages/ToolGroupMessage.tsx | 50 ++++++++++++------- packages/cli/src/ui/hooks/useGeminiStream.ts | 7 +-- schemas/settings.schema.json | 7 +++ 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 95b8f9917c..9e576b87b6 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -606,6 +606,16 @@ const SETTINGS_SCHEMA = { description: 'Show the spinner during operations.', showInDialog: true, }, + enableCompactToolOutput: { + type: 'boolean', + label: 'Enable Compact Tool Output', + category: 'UI', + requiresRestart: false, + default: true, + description: + 'Render tool outputs in a compact, single-line format when possible.', + showInDialog: true, + }, customWittyPhrases: { type: 'array', label: 'Custom Witty Phrases', diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index dac3839cb9..4ee69f92c7 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -66,7 +66,7 @@ export const ToolGroupMessage: React.FC = ({ const config = useConfig(); const { constrainHeight } = useUIState(); const { merged: settings } = useSettings(); - const isVerboseMode = settings.output?.verbosity === 'verbose'; + const compactMode = settings.ui.enableCompactToolOutput; const isEventDriven = config.isEventDrivenSchedulerEnabled(); @@ -74,18 +74,30 @@ export const ToolGroupMessage: React.FC = ({ // pre-execution states (Confirming, Pending) from the History log. // They live in the Global Queue or wait for their turn. const visibleToolCalls = useMemo(() => { - if (!isEventDriven) { - return toolCalls; - } - // Only show tools that are actually running or finished. - // We explicitly exclude Pending and Confirming to ensure they only - // appear in the Global Queue until they are approved and start executing. - return toolCalls.filter( - (t) => - t.status !== ToolCallStatus.Pending && - t.status !== ToolCallStatus.Confirming, - ); - }, [toolCalls, isEventDriven]); + // Standard filtering for Event Driven mode + const filteredTools = isEventDriven + ? toolCalls.filter( + (t) => + t.status !== ToolCallStatus.Pending && + t.status !== ToolCallStatus.Confirming, + ) + : toolCalls; + + // Additional filtering for compact mode: + // In compact mode, we hide 'Pending' tools from the history log to avoid flickering + // unless we are in a non-compact (boxed) view where we want to show the placeholder. + return filteredTools.filter((tool) => { + const isShellToolCall = isShellTool(tool.name); + const useDenseView = + compactMode && + !isShellToolCall && + tool.status !== ToolCallStatus.Confirming; + + if (!useDenseView) return true; + // In dense view, we only show tools that have started or finished. + return tool.status !== ToolCallStatus.Pending; + }); + }, [toolCalls, isEventDriven, compactMode]); const isEmbeddedShellFocused = visibleToolCalls.some((t) => isThisShellFocused( @@ -172,9 +184,9 @@ export const ToolGroupMessage: React.FC = ({ const isFirst = index === 0; const isShellToolCall = isShellTool(tool.name); - // Use dense view if not verbose, not a shell tool (for interactivity), and not confirming (needs prompt) + // Use dense view if compact mode is enabled, not a shell tool (for interactivity), and not confirming (needs prompt) const useDenseView = - !isVerboseMode && + compactMode && !isShellToolCall && tool.status !== ToolCallStatus.Confirming; @@ -268,7 +280,7 @@ export const ToolGroupMessage: React.FC = ({ // HOWEVER, if borderBottomOverride is true, it means the scheduler explicitly // wants to close a box. Since dense tools don't have boxes, this must be closing // a non-dense (e.g. shell) tool box. So we must allow it. - if (!isVerboseMode && borderBottomOverride !== true) return null; + if (compactMode && borderBottomOverride !== true) return null; if (borderBottomOverride !== undefined) { return ( @@ -292,8 +304,8 @@ export const ToolGroupMessage: React.FC = ({ const isShell = isShellTool(lastTool.name); const isConfirming = lastTool.status === ToolCallStatus.Confirming; - // Logic: If dense view (not verbose, not shell, not confirming), hide border by default - const isDense = !isVerboseMode && !isShell && !isConfirming; + // Logic: If dense view (compact mode, not shell, not confirming), hide border by default + const isDense = compactMode && !isShell && !isConfirming; if (isDense) return null; @@ -319,7 +331,7 @@ export const ToolGroupMessage: React.FC = ({ /> ); })()} - {!isVerboseMode + {compactMode ? null : (borderBottomOverride ?? true) && visibleToolCalls.length > 0 && ( diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index d75592f4e1..6f96dd4ef3 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -358,7 +358,7 @@ export const useGeminiStream = ( addItem, ]); - const isVerboseMode = settings.merged.output?.verbosity === 'verbose'; + const enableCompactToolOutput = settings.merged.ui.enableCompactToolOutput; const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => { const remainingTools = toolCalls.filter( @@ -381,7 +381,8 @@ export const useGeminiStream = ( // Once all tools are terminal and pushed, the last history item handles the closing border. // NOTE: In dense mode, we skip this if there are no shell tools (which require boxes). const requiresBoxLayout = - isVerboseMode || toolCalls.some((tc) => isShellTool(tc.request.name)); + !enableCompactToolOutput || + toolCalls.some((tc) => isShellTool(tc.request.name)); if (!requiresBoxLayout) { return items; @@ -431,7 +432,7 @@ export const useGeminiStream = ( } return items; - }, [toolCalls, pushedToolCallIds, isVerboseMode]); + }, [toolCalls, pushedToolCallIds, enableCompactToolOutput]); const activeToolPtyId = useMemo(() => { const executingShellTool = toolCalls.find( diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index a73533d737..ab0ff4ada3 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -359,6 +359,13 @@ "default": true, "type": "boolean" }, + "enableCompactToolOutput": { + "title": "Enable Compact Tool Output", + "description": "Render tool outputs in a compact, single-line format when possible.", + "markdownDescription": "Render tool outputs in a compact, single-line format when possible.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`", + "default": true, + "type": "boolean" + }, "customWittyPhrases": { "title": "Custom Witty Phrases", "description": "Custom witty phrases to display during loading. When provided, the CLI cycles through these instead of the defaults.",