diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 9dd0f96758..c4aec2e9cd 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -524,6 +524,8 @@ const baseMockUiState = { nightly: false, updateInfo: null, pendingHistoryItems: [], + mainControlsRef: () => {}, + rootUiRef: { current: null }, }; export const mockAppState: AppState = { diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 950363f6a8..b836202eb7 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -70,9 +70,7 @@ describe('App', () => { cleanUiDetailsVisible: true, quittingMessages: null, dialogsVisible: false, - mainControlsRef: { - current: null, - } as unknown as React.MutableRefObject, + mainControlsRef: vi.fn(), rootUiRef: { current: null, } as unknown as React.MutableRefObject, diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index ce5fc7c872..d58ed45d89 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -14,7 +14,7 @@ import { } from 'react'; import { type DOMElement, - measureElement, + ResizeObserver, useApp, useStdout, useStdin, @@ -397,7 +397,6 @@ export const AppContainer = (props: AppContainerProps) => { const branchName = useGitBranchName(config.getTargetDir()); // Layout measurements - const mainControlsRef = useRef(null); // For performance profiling only const rootUiRef = useRef(null); const lastTitleRef = useRef(null); @@ -1396,6 +1395,7 @@ Logging in with Google... Restarting Gemini CLI to continue. !proQuotaRequest && !copyModeEnabled; + const observerRef = useRef(null); const [controlsHeight, setControlsHeight] = useState(0); const [lastNonCopyControlsHeight, setLastNonCopyControlsHeight] = useState(0); @@ -1410,15 +1410,26 @@ Logging in with Google... Restarting Gemini CLI to continue. ? lastNonCopyControlsHeight : controlsHeight; - useLayoutEffect(() => { - if (mainControlsRef.current) { - const fullFooterMeasurement = measureElement(mainControlsRef.current); - const roundedHeight = Math.round(fullFooterMeasurement.height); - if (roundedHeight > 0 && roundedHeight !== controlsHeight) { - setControlsHeight(roundedHeight); - } + const mainControlsRef = useCallback((node: DOMElement | null) => { + if (observerRef.current) { + observerRef.current.disconnect(); + observerRef.current = null; } - }, [buffer, terminalWidth, terminalHeight, controlsHeight, isInputActive]); + + if (node) { + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + const roundedHeight = Math.round(entry.contentRect.height); + setControlsHeight((prev) => + roundedHeight !== prev ? roundedHeight : prev, + ); + } + }); + observer.observe(node); + observerRef.current = observer; + } + }, []); // Compute available terminal height based on stable controls measurement const availableTerminalHeight = Math.max( diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx index 53c820f69e..4f1cca7d8c 100644 --- a/packages/cli/src/ui/components/AskUserDialog.test.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx @@ -1491,4 +1491,47 @@ describe('AskUserDialog', () => { expect(frame).toContain('3. Option 3'); }); }); + + it('allows the question to exceed 15 lines in a tall terminal', async () => { + const longQuestion = Array.from( + { length: 25 }, + (_, i) => `Line ${i + 1}`, + ).join('\n'); + const questions: Question[] = [ + { + question: longQuestion, + header: 'Tall Test', + type: QuestionType.CHOICE, + options: [ + { label: 'Option 1', description: 'D1' }, + { label: 'Option 2', description: 'D2' }, + { label: 'Option 3', description: 'D3' }, + ], + multiSelect: false, + unconstrainedHeight: false, + }, + ]; + + const { lastFrame, waitUntilReady } = await renderWithProviders( + , + { width: 80 }, + ); + + await waitFor(async () => { + await waitUntilReady(); + const frame = lastFrame(); + // Should show more than 15 lines of the question + // (The limit was previously 15, so showing Line 20 proves it's working) + expect(frame).toContain('Line 20'); + expect(frame).toContain('Line 25'); + // Should still show the options + expect(frame).toContain('1. Option 1'); + }); + }); }); diff --git a/packages/cli/src/ui/components/AskUserDialog.tsx b/packages/cli/src/ui/components/AskUserDialog.tsx index cbb505320c..483fcb5055 100644 --- a/packages/cli/src/ui/components/AskUserDialog.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.tsx @@ -855,13 +855,7 @@ const ChoiceQuestionView: React.FC = ({ listHeight && !isAlternateBuffer ? question.unconstrainedHeight ? Math.max(1, listHeight - selectionItems.length * 2) - : Math.min( - 15, - Math.max( - 1, - listHeight - Math.max(DIALOG_PADDING, reservedListHeight), - ), - ) + : Math.max(1, listHeight - Math.max(DIALOG_PADDING, reservedListHeight)) : undefined; const maxItemsToShow = diff --git a/packages/cli/src/ui/components/MainContent.test.tsx b/packages/cli/src/ui/components/MainContent.test.tsx index e5d74b5cf5..b6bc0795eb 100644 --- a/packages/cli/src/ui/components/MainContent.test.tsx +++ b/packages/cli/src/ui/components/MainContent.test.tsx @@ -21,6 +21,10 @@ import { type UIState, } from '../contexts/UIStateContext.js'; import { type IndividualToolCallDisplay } from '../types.js'; +import { + type ConfirmingToolState, + useConfirmingTool, +} from '../hooks/useConfirmingTool.js'; // Mock dependencies const mockUseSettings = vi.fn().mockReturnValue({ @@ -53,6 +57,10 @@ vi.mock('../hooks/useAlternateBuffer.js', () => ({ useAlternateBuffer: vi.fn(), })); +vi.mock('../hooks/useConfirmingTool.js', () => ({ + useConfirmingTool: vi.fn(), +})); + vi.mock('./AppHeader.js', () => ({ AppHeader: ({ showDetails = true }: { showDetails?: boolean }) => ( {showDetails ? 'AppHeader(full)' : 'AppHeader(minimal)'} @@ -503,6 +511,54 @@ describe('MainContent', () => { unmount(); }); + it('renders a subagent with a complete box including bottom border', async () => { + const subagentCall = { + callId: 'subagent-1', + name: 'codebase_investigator', + description: 'Investigating codebase', + status: CoreToolCallStatus.Executing, + kind: 'agent', + resultDisplay: { + isSubagentProgress: true, + agentName: 'codebase_investigator', + recentActivity: [ + { + id: '1', + type: 'tool_call', + content: 'run_shell_command', + args: '{"command": "echo hello"}', + status: 'running', + }, + ], + state: 'running', + }, + } as Partial as IndividualToolCallDisplay; + + const uiState = { + ...defaultMockUiState, + history: [{ id: 1, type: 'user', text: 'Investigate' }], + pendingHistoryItems: [ + { + type: 'tool_group' as const, + tools: [subagentCall], + borderBottom: true, + }, + ], + }; + + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: uiState as Partial, + config: makeFakeConfig({ useAlternateBuffer: false }), + }); + + await waitFor(() => { + expect(lastFrame()).toContain('codebase_investigator'); + }); + + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }); + it('renders a split tool group without a gap between static and pending areas', async () => { const toolCalls = [ { @@ -547,13 +603,124 @@ describe('MainContent', () => { const { lastFrame, unmount } = await renderWithProviders(, { uiState: uiState as Partial, }); - const output = lastFrame(); - // Verify Part 1 and Part 2 are rendered. - expect(output).toContain('Part 1'); - expect(output).toContain('Part 2'); + + await waitFor(() => { + const output = lastFrame(); + // Verify Part 1 and Part 2 are rendered. + expect(output).toContain('Part 1'); + expect(output).toContain('Part 2'); + }); // The snapshot will be the best way to verify there is no gap (empty line) between them. - expect(output).toMatchSnapshot(); + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }); + + it('renders a ToolConfirmationQueue without an extra line when preceded by hidden tools', async () => { + const { ApprovalMode, WRITE_FILE_DISPLAY_NAME } = await import( + '@google/gemini-cli-core' + ); + const hiddenToolCalls = [ + { + callId: 'tool-hidden', + name: WRITE_FILE_DISPLAY_NAME, + approvalMode: ApprovalMode.PLAN, + status: CoreToolCallStatus.Success, + resultDisplay: 'Hidden content', + } as Partial as IndividualToolCallDisplay, + ]; + + const confirmingTool = { + tool: { + callId: 'call-1', + name: 'exit_plan_mode', + status: CoreToolCallStatus.AwaitingApproval, + confirmationDetails: { + type: 'exit_plan_mode' as const, + planPath: '/path/to/plan', + }, + }, + index: 1, + total: 1, + }; + + const uiState = { + ...defaultMockUiState, + history: [{ id: 1, type: 'user', text: 'Apply plan' }], + pendingHistoryItems: [ + { + type: 'tool_group' as const, + tools: hiddenToolCalls, + borderBottom: true, + }, + ], + }; + + // We need to mock useConfirmingTool to return our confirmingTool + vi.mocked(useConfirmingTool).mockReturnValue( + confirmingTool as unknown as ConfirmingToolState, + ); + + mockUseSettings.mockReturnValue( + createMockSettings({ + security: { enablePermanentToolApproval: true }, + ui: { errorVerbosity: 'full' }, + }), + ); + + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: uiState as Partial, + config: makeFakeConfig({ useAlternateBuffer: false }), + }); + + await waitFor(() => { + const output = lastFrame(); + // The output should NOT contain 'Hidden content' + expect(output).not.toContain('Hidden content'); + // The output should contain the confirmation header + expect(output).toContain('Ready to start implementation?'); + }); + + // Snapshot will reveal if there are extra blank lines + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }); + + it('renders a spurious line when a tool group has only hidden tools and borderBottom true', async () => { + const { ApprovalMode, WRITE_FILE_DISPLAY_NAME } = await import( + '@google/gemini-cli-core' + ); + const uiState = { + ...defaultMockUiState, + history: [{ id: 1, type: 'user', text: 'Apply plan' }], + pendingHistoryItems: [ + { + type: 'tool_group' as const, + tools: [ + { + callId: 'tool-1', + name: WRITE_FILE_DISPLAY_NAME, + approvalMode: ApprovalMode.PLAN, + status: CoreToolCallStatus.Success, + resultDisplay: 'hidden', + } as Partial as IndividualToolCallDisplay, + ], + borderBottom: true, + }, + ], + }; + + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: uiState as Partial, + config: makeFakeConfig({ useAlternateBuffer: false }), + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Apply plan'); + }); + + // This snapshot will show no spurious line because the group is now correctly suppressed. + expect(lastFrame()).toMatchSnapshot(); unmount(); }); diff --git a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap index d5173e8c9c..0e8e29e54d 100644 --- a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap @@ -91,6 +91,19 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unc " `; +exports[`MainContent > renders a ToolConfirmationQueue without an extra line when preceded by hidden tools 1`] = ` +"AppHeader(full) +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + > Apply plan +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +╭──────────────────────────────────────────────────────────────────────────────╮ +│ Ready to start implementation? │ +│ │ +│ Error reading plan: Storage must be initialized before use │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" +`; + exports[`MainContent > renders a split tool group without a gap between static and pending areas 1`] = ` "AppHeader(full) ╭──────────────────────────────────────────────────────────────────────────╮ @@ -105,6 +118,30 @@ exports[`MainContent > renders a split tool group without a gap between static a " `; +exports[`MainContent > renders a spurious line when a tool group has only hidden tools and borderBottom true 1`] = ` +"AppHeader(full) +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + > Apply plan +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +" +`; + +exports[`MainContent > renders a subagent with a complete box including bottom border 1`] = ` +"AppHeader(full) +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + > Investigate +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +╭──────────────────────────────────────────────────────────────────────────╮ +│ ≡ Running Agent... (ctrl+o to collapse) │ +│ │ +│ Running subagent codebase_investigator... │ +│ │ +│ ⠋ run_shell_command echo hello │ +│ │ +╰──────────────────────────────────────────────────────────────────────────╯ +" +`; + exports[`MainContent > renders mixed history items (user + gemini) with single line padding between them 1`] = ` "ScrollableList AppHeader(full) diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index 69da3a1029..637e8afa40 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -172,12 +172,10 @@ export const ToolGroupMessage: React.FC = ({ // If all tools are filtered out (e.g., in-progress AskUser tools, low-verbosity // internal errors, plan-mode hidden write/edit), we should not emit standalone // border fragments. The only case where an empty group should render is the - // explicit "closing slice" (tools: []) used to bridge static/pending sections. + // explicit "closing slice" (tools: []) used to bridge static/pending sections, + // and only if it's actually continuing an open box from above. const isExplicitClosingSlice = allToolCalls.length === 0; - if ( - visibleToolCalls.length === 0 && - (!isExplicitClosingSlice || borderBottomOverride !== true) - ) { + if (visibleToolCalls.length === 0 && !isExplicitClosingSlice) { return null; } @@ -269,19 +267,20 @@ export const ToolGroupMessage: React.FC = ({ We have to keep the bottom border separate so it doesn't get drawn over by the sticky header directly inside it. */ - (visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && ( - - ) + (visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && + borderBottomOverride !== false && ( + + ) } ); diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessageRegression.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessageRegression.test.tsx new file mode 100644 index 0000000000..96239fb720 --- /dev/null +++ b/packages/cli/src/ui/components/messages/ToolGroupMessageRegression.test.tsx @@ -0,0 +1,160 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { renderWithProviders } from '../../../test-utils/render.js'; +import { describe, it, expect } from 'vitest'; +import { ToolGroupMessage } from './ToolGroupMessage.js'; +import { + makeFakeConfig, + CoreToolCallStatus, + ApprovalMode, + WRITE_FILE_DISPLAY_NAME, + Kind, +} from '@google/gemini-cli-core'; +import os from 'node:os'; +import { createMockSettings } from '../../../test-utils/settings.js'; +import type { IndividualToolCallDisplay } from '../../types.js'; + +describe('ToolGroupMessage Regression Tests', () => { + const baseMockConfig = makeFakeConfig({ + model: 'gemini-pro', + targetDir: os.tmpdir(), + }); + const fullVerbositySettings = createMockSettings({ + ui: { errorVerbosity: 'full' }, + }); + + const createToolCall = ( + overrides: Partial = {}, + ): IndividualToolCallDisplay => + ({ + callId: 'tool-123', + name: 'test-tool', + status: CoreToolCallStatus.Success, + ...overrides, + }) as IndividualToolCallDisplay; + + const createItem = (tools: IndividualToolCallDisplay[]) => ({ + id: 1, + type: 'tool_group' as const, + tools, + }); + + it('Plan Mode: suppresses phantom tool group (hidden tools)', async () => { + const toolCalls = [ + createToolCall({ + name: WRITE_FILE_DISPLAY_NAME, + approvalMode: ApprovalMode.PLAN, + status: CoreToolCallStatus.Success, + }), + ]; + const item = createItem(toolCalls); + + const { lastFrame, unmount } = await renderWithProviders( + , + { config: baseMockConfig, settings: fullVerbositySettings }, + ); + + expect(lastFrame({ allowEmpty: true })).toBe(''); + unmount(); + }); + + it('Agent Case: suppresses the bottom border box for ongoing agents (no vertical ticks)', async () => { + const toolCalls = [ + createToolCall({ + name: 'agent', + kind: Kind.Agent, + status: CoreToolCallStatus.Executing, + resultDisplay: { + isSubagentProgress: true, + agentName: 'TestAgent', + state: 'running', + recentActivity: [], + }, + }), + ]; + const item = createItem(toolCalls); + + const { lastFrame, unmount } = await renderWithProviders( + , + { config: baseMockConfig, settings: fullVerbositySettings }, + ); + + const output = lastFrame(); + expect(output).toContain('Running Agent...'); + // It should render side borders from the content + expect(output).toContain('│'); + // It should NOT render the bottom border box (no corners ╰ ╯) + expect(output).not.toContain('╰'); + expect(output).not.toContain('╯'); + unmount(); + }); + + it('Agent Case: renders a bottom border horizontal line for completed agents', async () => { + const toolCalls = [ + createToolCall({ + name: 'agent', + kind: Kind.Agent, + status: CoreToolCallStatus.Success, + resultDisplay: { + isSubagentProgress: true, + agentName: 'TestAgent', + state: 'completed', + recentActivity: [], + }, + }), + ]; + const item = createItem(toolCalls); + + const { lastFrame, unmount } = await renderWithProviders( + , + { config: baseMockConfig, settings: fullVerbositySettings }, + ); + + const output = lastFrame(); + // Verify it rendered subagent content + expect(output).toContain('Agent'); + // It should render the bottom horizontal line + expect(output).toContain( + '╰──────────────────────────────────────────────────────────────────────────╯', + ); + unmount(); + }); + + it('Bridges: still renders a bridge if it has a top border', async () => { + const toolCalls: IndividualToolCallDisplay[] = []; + const item = createItem(toolCalls); + + const { lastFrame, unmount } = await renderWithProviders( + , + { config: baseMockConfig, settings: fullVerbositySettings }, + ); + + expect(lastFrame({ allowEmpty: true })).not.toBe(''); + unmount(); + }); +}); diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index e4d95a79af..8447247e53 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -191,7 +191,7 @@ export interface UIState { sessionStats: SessionStatsState; terminalWidth: number; terminalHeight: number; - mainControlsRef: React.MutableRefObject; + mainControlsRef: React.RefCallback; // NOTE: This is for performance profiling only. rootUiRef: React.MutableRefObject; currentIDE: IdeInfo | null; diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 54006d2ab2..757c24f2c3 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -26,7 +26,6 @@ import { debugLogger, runInDevTraceSpan, EDIT_TOOL_NAMES, - ASK_USER_TOOL_NAME, processRestorableToolCalls, recordToolCallInteractions, ToolErrorType, @@ -40,6 +39,7 @@ import { isBackgroundExecutionData, Kind, ACTIVATE_SKILL_TOOL_NAME, + shouldHideToolCall, } from '@google/gemini-cli-core'; import type { Config, @@ -66,7 +66,12 @@ import type { SlashCommandProcessorResult, HistoryItemModel, } from '../types.js'; -import { StreamingState, MessageType } from '../types.js'; +import { + StreamingState, + MessageType, + mapCoreStatusToDisplayStatus, + ToolCallStatus, +} from '../types.js'; import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js'; import { useShellCommandProcessor } from './shellCommandProcessor.js'; import { handleAtCommand } from './atCommandProcessor.js'; @@ -541,14 +546,39 @@ export const useGeminiStream = ( const anyVisibleInHistory = pushedToolCallIds.size > 0; const anyVisibleInPending = remainingTools.some((tc) => { - // AskUser tools are rendered by AskUserDialog, not ToolGroupMessage - const isInProgress = - tc.status !== 'success' && - tc.status !== 'error' && - tc.status !== 'cancelled'; - if (tc.request.name === ASK_USER_TOOL_NAME && isInProgress) { + const displayName = tc.tool?.displayName ?? tc.request.name; + + let hasResultDisplay = false; + if ( + tc.status === CoreToolCallStatus.Success || + tc.status === CoreToolCallStatus.Error || + tc.status === CoreToolCallStatus.Cancelled + ) { + hasResultDisplay = !!tc.response?.resultDisplay; + } else if (tc.status === CoreToolCallStatus.Executing) { + hasResultDisplay = !!tc.liveOutput; + } + + // AskUser tools and Plan Mode write/edit are handled by this logic + if ( + shouldHideToolCall({ + displayName, + status: tc.status, + approvalMode: tc.approvalMode, + hasResultDisplay, + parentCallId: tc.request.parentCallId, + }) + ) { return false; } + + // ToolGroupMessage explicitly hides Confirming tools because they are + // rendered in the interactive ToolConfirmationQueue instead. + const displayStatus = mapCoreStatusToDisplayStatus(tc.status); + if (displayStatus === ToolCallStatus.Confirming) { + return false; + } + // ToolGroupMessage now shows all non-canceled tools, so they are visible // in pending and we need to draw the closing border for them. return true; diff --git a/packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx b/packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx index 43b970da8e..7bf51b7d84 100644 --- a/packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx +++ b/packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx @@ -25,7 +25,7 @@ const mockUIState = { dialogsVisible: false, streamingState: StreamingState.Idle, isBackgroundShellListOpen: false, - mainControlsRef: { current: null }, + mainControlsRef: vi.fn(), customDialog: null, historyManager: { addItem: vi.fn() }, history: [],