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.
This commit is contained in:
Jarrod Whelan
2026-02-10 03:12:41 -08:00
parent 5f99cbe560
commit 6d053e4227
4 changed files with 52 additions and 22 deletions
+10
View File
@@ -606,6 +606,16 @@ const SETTINGS_SCHEMA = {
description: 'Show the spinner during operations.', description: 'Show the spinner during operations.',
showInDialog: true, 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: { customWittyPhrases: {
type: 'array', type: 'array',
label: 'Custom Witty Phrases', label: 'Custom Witty Phrases',
@@ -66,7 +66,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
const config = useConfig(); const config = useConfig();
const { constrainHeight } = useUIState(); const { constrainHeight } = useUIState();
const { merged: settings } = useSettings(); const { merged: settings } = useSettings();
const isVerboseMode = settings.output?.verbosity === 'verbose'; const compactMode = settings.ui.enableCompactToolOutput;
const isEventDriven = config.isEventDrivenSchedulerEnabled(); const isEventDriven = config.isEventDrivenSchedulerEnabled();
@@ -74,18 +74,30 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
// pre-execution states (Confirming, Pending) from the History log. // pre-execution states (Confirming, Pending) from the History log.
// They live in the Global Queue or wait for their turn. // They live in the Global Queue or wait for their turn.
const visibleToolCalls = useMemo(() => { const visibleToolCalls = useMemo(() => {
if (!isEventDriven) { // Standard filtering for Event Driven mode
return toolCalls; const filteredTools = isEventDriven
} ? toolCalls.filter(
// Only show tools that are actually running or finished. (t) =>
// We explicitly exclude Pending and Confirming to ensure they only t.status !== ToolCallStatus.Pending &&
// appear in the Global Queue until they are approved and start executing. t.status !== ToolCallStatus.Confirming,
return toolCalls.filter( )
(t) => : toolCalls;
t.status !== ToolCallStatus.Pending &&
t.status !== ToolCallStatus.Confirming, // Additional filtering for compact mode:
); // In compact mode, we hide 'Pending' tools from the history log to avoid flickering
}, [toolCalls, isEventDriven]); // 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) => const isEmbeddedShellFocused = visibleToolCalls.some((t) =>
isThisShellFocused( isThisShellFocused(
@@ -172,9 +184,9 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
const isFirst = index === 0; const isFirst = index === 0;
const isShellToolCall = isShellTool(tool.name); 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 = const useDenseView =
!isVerboseMode && compactMode &&
!isShellToolCall && !isShellToolCall &&
tool.status !== ToolCallStatus.Confirming; tool.status !== ToolCallStatus.Confirming;
@@ -268,7 +280,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
// HOWEVER, if borderBottomOverride is true, it means the scheduler explicitly // 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 // 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. // 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) { if (borderBottomOverride !== undefined) {
return ( return (
@@ -292,8 +304,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
const isShell = isShellTool(lastTool.name); const isShell = isShellTool(lastTool.name);
const isConfirming = lastTool.status === ToolCallStatus.Confirming; const isConfirming = lastTool.status === ToolCallStatus.Confirming;
// Logic: If dense view (not verbose, not shell, not confirming), hide border by default // Logic: If dense view (compact mode, not shell, not confirming), hide border by default
const isDense = !isVerboseMode && !isShell && !isConfirming; const isDense = compactMode && !isShell && !isConfirming;
if (isDense) return null; if (isDense) return null;
@@ -319,7 +331,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
/> />
); );
})()} })()}
{!isVerboseMode {compactMode
? null ? null
: (borderBottomOverride ?? true) && : (borderBottomOverride ?? true) &&
visibleToolCalls.length > 0 && ( visibleToolCalls.length > 0 && (
+4 -3
View File
@@ -358,7 +358,7 @@ export const useGeminiStream = (
addItem, addItem,
]); ]);
const isVerboseMode = settings.merged.output?.verbosity === 'verbose'; const enableCompactToolOutput = settings.merged.ui.enableCompactToolOutput;
const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => { const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => {
const remainingTools = toolCalls.filter( 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. // 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). // NOTE: In dense mode, we skip this if there are no shell tools (which require boxes).
const requiresBoxLayout = const requiresBoxLayout =
isVerboseMode || toolCalls.some((tc) => isShellTool(tc.request.name)); !enableCompactToolOutput ||
toolCalls.some((tc) => isShellTool(tc.request.name));
if (!requiresBoxLayout) { if (!requiresBoxLayout) {
return items; return items;
@@ -431,7 +432,7 @@ export const useGeminiStream = (
} }
return items; return items;
}, [toolCalls, pushedToolCallIds, isVerboseMode]); }, [toolCalls, pushedToolCallIds, enableCompactToolOutput]);
const activeToolPtyId = useMemo(() => { const activeToolPtyId = useMemo(() => {
const executingShellTool = toolCalls.find( const executingShellTool = toolCalls.find(
+7
View File
@@ -359,6 +359,13 @@
"default": true, "default": true,
"type": "boolean" "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": { "customWittyPhrases": {
"title": "Custom Witty Phrases", "title": "Custom Witty Phrases",
"description": "Custom witty phrases to display during loading. When provided, the CLI cycles through these instead of the defaults.", "description": "Custom witty phrases to display during loading. When provided, the CLI cycles through these instead of the defaults.",