mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-17 09:30:58 -07:00
247 lines
7.6 KiB
TypeScript
247 lines
7.6 KiB
TypeScript
/**
|
|
* @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 {
|
|
HistoryItem,
|
|
HistoryItemWithoutId,
|
|
IndividualToolCallDisplay,
|
|
} from '../../types.js';
|
|
import { ToolCallStatus, mapCoreStatusToDisplayStatus } from '../../types.js';
|
|
import { ToolMessage } from './ToolMessage.js';
|
|
import { ShellToolMessage } from './ShellToolMessage.js';
|
|
import { theme } from '../../semantic-colors.js';
|
|
import { useConfig } from '../../contexts/ConfigContext.js';
|
|
import { isShellTool } from './ToolShared.js';
|
|
import {
|
|
shouldHideToolCall,
|
|
CoreToolCallStatus,
|
|
} from '@google/gemini-cli-core';
|
|
import { useUIState } from '../../contexts/UIStateContext.js';
|
|
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
|
|
import { useSettings } from '../../contexts/SettingsContext.js';
|
|
|
|
interface ToolGroupMessageProps {
|
|
item: HistoryItem | HistoryItemWithoutId;
|
|
toolCalls: IndividualToolCallDisplay[];
|
|
availableTerminalHeight?: number;
|
|
terminalWidth: number;
|
|
onShellInputSubmit?: (input: string) => void;
|
|
borderTop?: boolean;
|
|
borderBottom?: boolean;
|
|
isExpandable?: boolean;
|
|
}
|
|
|
|
// Main component renders the border and maps the tools using ToolMessage
|
|
const TOOL_MESSAGE_HORIZONTAL_MARGIN = 4;
|
|
|
|
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|
item,
|
|
toolCalls: allToolCalls,
|
|
availableTerminalHeight,
|
|
terminalWidth,
|
|
borderTop: borderTopOverride,
|
|
borderBottom: borderBottomOverride,
|
|
isExpandable,
|
|
}) => {
|
|
const settings = useSettings();
|
|
const isLowErrorVerbosity = settings.merged.ui?.errorVerbosity !== 'full';
|
|
|
|
// Filter out tool calls that should be hidden (e.g. in-progress Ask User, or Plan Mode operations).
|
|
const toolCalls = useMemo(
|
|
() =>
|
|
allToolCalls.filter((t) => {
|
|
if (
|
|
isLowErrorVerbosity &&
|
|
t.status === CoreToolCallStatus.Error &&
|
|
!t.isClientInitiated
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return !shouldHideToolCall({
|
|
displayName: t.name,
|
|
status: t.status,
|
|
approvalMode: t.approvalMode,
|
|
hasResultDisplay: !!t.resultDisplay,
|
|
parentCallId: t.parentCallId,
|
|
});
|
|
}),
|
|
[allToolCalls, isLowErrorVerbosity],
|
|
);
|
|
|
|
const config = useConfig();
|
|
const {
|
|
activePtyId,
|
|
embeddedShellFocused,
|
|
backgroundShells,
|
|
pendingHistoryItems,
|
|
} = useUIState();
|
|
|
|
const { borderColor, borderDimColor } = useMemo(
|
|
() =>
|
|
getToolGroupBorderAppearance(
|
|
item,
|
|
activePtyId,
|
|
embeddedShellFocused,
|
|
pendingHistoryItems,
|
|
backgroundShells,
|
|
),
|
|
[
|
|
item,
|
|
activePtyId,
|
|
embeddedShellFocused,
|
|
pendingHistoryItems,
|
|
backgroundShells,
|
|
],
|
|
);
|
|
|
|
// 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.
|
|
// 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.
|
|
const visibleToolCalls = useMemo(
|
|
() =>
|
|
toolCalls.filter((t) => {
|
|
const displayStatus = mapCoreStatusToDisplayStatus(t.status);
|
|
// We hide Confirming tools from the history log because they are
|
|
// currently being rendered in the interactive ToolConfirmationQueue.
|
|
// We show everything else, including Pending (waiting to run) and
|
|
// Canceled (rejected by user), to ensure the history is complete
|
|
// and to avoid tools "vanishing" after approval.
|
|
return displayStatus !== ToolCallStatus.Confirming;
|
|
}),
|
|
|
|
[toolCalls],
|
|
);
|
|
|
|
const staticHeight = /* border */ 2;
|
|
|
|
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;
|
|
|
|
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
|
|
|
|
// 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.
|
|
const isExplicitClosingSlice = allToolCalls.length === 0;
|
|
if (
|
|
visibleToolCalls.length === 0 &&
|
|
(!isExplicitClosingSlice || borderBottomOverride !== true)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const content = (
|
|
<Box
|
|
flexDirection="column"
|
|
/*
|
|
This width constraint is highly important and protects us from an Ink rendering bug.
|
|
Since the ToolGroup can typically change rendering states frequently, it can cause
|
|
Ink to render the border of the box incorrectly and span multiple lines and even
|
|
cause tearing.
|
|
*/
|
|
width={terminalWidth}
|
|
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
|
|
>
|
|
{visibleToolCalls.map((tool, index) => {
|
|
const isFirst = index === 0;
|
|
const isShellToolCall = isShellTool(tool.name);
|
|
|
|
const commonProps = {
|
|
...tool,
|
|
availableTerminalHeight: availableTerminalHeightPerToolMessage,
|
|
terminalWidth: contentWidth,
|
|
emphasis: 'medium' as const,
|
|
isFirst:
|
|
borderTopOverride !== undefined
|
|
? borderTopOverride && isFirst
|
|
: isFirst,
|
|
borderColor,
|
|
borderDimColor,
|
|
isExpandable,
|
|
};
|
|
|
|
return (
|
|
<Box
|
|
key={tool.callId}
|
|
flexDirection="column"
|
|
minHeight={1}
|
|
width={contentWidth}
|
|
>
|
|
{isShellToolCall ? (
|
|
<ShellToolMessage {...commonProps} config={config} />
|
|
) : (
|
|
<ToolMessage {...commonProps} />
|
|
)}
|
|
{tool.outputFile && (
|
|
<Box
|
|
borderLeft={true}
|
|
borderRight={true}
|
|
borderTop={false}
|
|
borderBottom={false}
|
|
borderColor={borderColor}
|
|
borderDimColor={borderDimColor}
|
|
flexDirection="column"
|
|
borderStyle="round"
|
|
paddingLeft={1}
|
|
paddingRight={1}
|
|
>
|
|
<Box>
|
|
<Text color={theme.text.primary}>
|
|
Output too long and was saved to: {tool.outputFile}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
})}
|
|
{
|
|
/*
|
|
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) && (
|
|
<Box
|
|
height={0}
|
|
width={contentWidth}
|
|
borderLeft={true}
|
|
borderRight={true}
|
|
borderTop={false}
|
|
borderBottom={borderBottomOverride ?? true}
|
|
borderColor={borderColor}
|
|
borderDimColor={borderDimColor}
|
|
borderStyle="round"
|
|
/>
|
|
)
|
|
}
|
|
</Box>
|
|
);
|
|
|
|
return content;
|
|
};
|