/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useMemo } from 'react'; import { Box, Text } from 'ink'; import type { IndividualToolCallDisplay } from '../../types.js'; import { ToolCallStatus } from '../../types.js'; import { ToolMessage } from './ToolMessage.js'; import { ShellToolMessage } from './ShellToolMessage.js'; import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; import { theme } from '../../semantic-colors.js'; import { useConfig } from '../../contexts/ConfigContext.js'; import { isShellTool, isThisShellFocused } from './ToolShared.js'; import { ASK_USER_DISPLAY_NAME } from '@google/gemini-cli-core'; import { ShowMoreLines } from '../ShowMoreLines.js'; import { useUIState } from '../../contexts/UIStateContext.js'; interface ToolGroupMessageProps { groupId: number; toolCalls: IndividualToolCallDisplay[]; availableTerminalHeight?: number; terminalWidth: number; isFocused?: boolean; activeShellPtyId?: number | null; embeddedShellFocused?: boolean; onShellInputSubmit?: (input: string) => void; borderTop?: boolean; borderBottom?: boolean; } // Helper to identify Ask User tools that are in progress (have their own dialog UI) const isAskUserInProgress = (t: IndividualToolCallDisplay): boolean => t.name === ASK_USER_DISPLAY_NAME && [ ToolCallStatus.Pending, ToolCallStatus.Executing, ToolCallStatus.Confirming, ].includes(t.status); // Main component renders the border and maps the tools using ToolMessage export const ToolGroupMessage: React.FC = ({ toolCalls: allToolCalls, availableTerminalHeight, terminalWidth, isFocused = true, activeShellPtyId, embeddedShellFocused, borderTop: borderTopOverride, borderBottom: borderBottomOverride, }) => { // Filter out in-progress Ask User tools (they have their own AskUserDialog UI) const toolCalls = useMemo( () => allToolCalls.filter((t) => !isAskUserInProgress(t)), [allToolCalls], ); const config = useConfig(); const { constrainHeight } = useUIState(); const isEventDriven = config.isEventDrivenSchedulerEnabled(); // If Event-Driven Scheduler is enabled, we HIDE tools that are still in // 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]); const isEmbeddedShellFocused = visibleToolCalls.some((t) => isThisShellFocused( t.name, t.status, t.ptyId, activeShellPtyId, embeddedShellFocused, ), ); const hasPending = !visibleToolCalls.every( (t) => t.status === ToolCallStatus.Success, ); const isShellCommand = toolCalls.some((t) => isShellTool(t.name)); const borderColor = (isShellCommand && hasPending) || isEmbeddedShellFocused ? theme.ui.symbol : hasPending ? theme.status.warning : theme.border.default; const borderDimColor = hasPending && (!isShellCommand || !isEmbeddedShellFocused); const staticHeight = /* border */ 2 + /* marginBottom */ 1; // Inline confirmations are ONLY used when the Global Queue is disabled. const toolAwaitingApproval = useMemo( () => isEventDriven ? undefined : toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming), [toolCalls, isEventDriven], ); // If all tools are hidden (e.g. group only contains confirming or pending tools), // render nothing in the history log unless we have a border override. if ( visibleToolCalls.length === 0 && borderTopOverride === undefined && borderBottomOverride === undefined ) { return null; } let countToolCallsWithResults = 0; for (const tool of visibleToolCalls) { if (tool.resultDisplay !== undefined && tool.resultDisplay !== '') { countToolCallsWithResults++; } } const countOneLineToolCalls = visibleToolCalls.length - countToolCallsWithResults; const availableTerminalHeightPerToolMessage = availableTerminalHeight ? Math.max( Math.floor( (availableTerminalHeight - staticHeight - countOneLineToolCalls) / Math.max(1, countToolCallsWithResults), ), 1, ) : undefined; return ( // This box doesn't have a border even though it conceptually does because // we need to allow the sticky headers to render the borders themselves so // that the top border can be sticky. {visibleToolCalls.map((tool, index) => { const isConfirming = toolAwaitingApproval?.callId === tool.callId; const isFirst = index === 0; const isShellToolCall = isShellTool(tool.name); const commonProps = { ...tool, availableTerminalHeight: availableTerminalHeightPerToolMessage, terminalWidth, emphasis: isConfirming ? ('high' as const) : toolAwaitingApproval ? ('low' as const) : ('medium' as const), isFirst: borderTopOverride !== undefined ? borderTopOverride && isFirst : isFirst, borderColor, borderDimColor, }; return ( {isShellToolCall ? ( ) : ( )} {tool.status === ToolCallStatus.Confirming && isConfirming && tool.confirmationDetails && ( )} {tool.outputFile && ( Output too long and was saved to: {tool.outputFile} )} ); })} { /* 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) && ( ) } {(borderBottomOverride ?? true) && visibleToolCalls.length > 0 && ( )} ); };