Files
gemini-cli/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
T

230 lines
7.4 KiB
TypeScript
Raw Normal View History

/**
* @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, mapCoreStatusToDisplayStatus } from '../../types.js';
2025-04-18 19:09:41 -04:00
import { ToolMessage } from './ToolMessage.js';
import { ShellToolMessage } from './ShellToolMessage.js';
import { theme } from '../../semantic-colors.js';
import { useConfig } from '../../contexts/ConfigContext.js';
2026-01-22 16:02:14 -08:00
import { isShellTool, isThisShellFocused } from './ToolShared.js';
import {
shouldHideAskUserTool,
CoreToolCallStatus,
} from '@google/gemini-cli-core';
import { ShowMoreLines } from '../ShowMoreLines.js';
import { useUIState } from '../../contexts/UIStateContext.js';
2025-04-15 21:41:08 -07:00
interface ToolGroupMessageProps {
groupId: number;
2025-04-17 18:06:21 -04:00
toolCalls: IndividualToolCallDisplay[];
availableTerminalHeight?: number;
terminalWidth: number;
activeShellPtyId?: number | null;
embeddedShellFocused?: boolean;
onShellInputSubmit?: (input: string) => void;
borderTop?: boolean;
borderBottom?: boolean;
2025-04-15 21:41:08 -07:00
}
// Main component renders the border and maps the tools using ToolMessage
const TOOL_MESSAGE_HORIZONTAL_MARGIN = 4;
2025-04-18 19:09:41 -04:00
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
toolCalls: allToolCalls,
availableTerminalHeight,
terminalWidth,
activeShellPtyId,
embeddedShellFocused,
borderTop: borderTopOverride,
borderBottom: borderBottomOverride,
2025-04-17 18:06:21 -04:00
}) => {
// Filter out Ask User tools that should be hidden (e.g. in-progress or errors without result)
const toolCalls = useMemo(
() =>
allToolCalls.filter((t) => {
const displayStatus = mapCoreStatusToDisplayStatus(t.status);
return !shouldHideAskUserTool(t.name, displayStatus, !!t.resultDisplay);
}),
[allToolCalls],
);
const config = useConfig();
const { constrainHeight } = useUIState();
// 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);
return (
displayStatus !== ToolCallStatus.Pending &&
displayStatus !== ToolCallStatus.Confirming
);
}),
[toolCalls],
);
const isEmbeddedShellFocused = visibleToolCalls.some((t) =>
2026-01-22 16:02:14 -08:00
isThisShellFocused(
t.name,
t.status,
t.ptyId,
activeShellPtyId,
embeddedShellFocused,
),
);
const hasPending = !visibleToolCalls.every(
(t) => t.status === CoreToolCallStatus.Success,
2025-04-22 07:48:12 -04:00
);
2026-01-22 16:02:14 -08:00
const isShellCommand = toolCalls.some((t) => isShellTool(t.name));
2025-07-18 17:30:28 -07:00
const borderColor =
(isShellCommand && hasPending) || isEmbeddedShellFocused
? theme.ui.symbol
: hasPending
? theme.status.warning
: theme.border.default;
2025-04-15 21:41:08 -07:00
const borderDimColor =
hasPending && (!isShellCommand || !isEmbeddedShellFocused);
const staticHeight = /* border */ 2 + /* marginBottom */ 1;
// If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools),
// only render if we need to close a border from previous
// tool groups. borderBottomOverride=true means we must render the closing border;
// undefined or false means there's nothing to display.
if (visibleToolCalls.length === 0 && borderBottomOverride !== true) {
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;
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
2025-04-17 18:06:21 -04:00
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.
2025-04-22 07:48:12 -04:00
<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}
2025-04-22 07:48:12 -04:00
>
{visibleToolCalls.map((tool, index) => {
const isFirst = index === 0;
2026-01-22 16:02:14 -08:00
const isShellToolCall = isShellTool(tool.name);
const commonProps = {
...tool,
availableTerminalHeight: availableTerminalHeightPerToolMessage,
terminalWidth: contentWidth,
emphasis: 'medium' as const,
isFirst:
borderTopOverride !== undefined
? borderTopOverride && isFirst
: isFirst,
borderColor,
borderDimColor,
};
return (
2025-11-11 07:50:11 -08:00
<Box
key={tool.callId}
flexDirection="column"
minHeight={1}
width={contentWidth}
2025-11-11 07:50:11 -08:00
>
2026-01-22 16:02:14 -08:00
{isShellToolCall ? (
<ShellToolMessage
{...commonProps}
activeShellPtyId={activeShellPtyId}
embeddedShellFocused={embeddedShellFocused}
config={config}
/>
) : (
<ToolMessage {...commonProps} />
)}
<Box
borderLeft={true}
borderRight={true}
borderTop={false}
2025-11-13 14:33:48 -08:00
borderBottom={false}
borderColor={borderColor}
borderDimColor={borderDimColor}
flexDirection="column"
borderStyle="round"
paddingLeft={1}
paddingRight={1}
>
{tool.outputFile && (
<Box>
<Text color={theme.text.primary}>
Output too long and was saved to: {tool.outputFile}
</Text>
</Box>
)}
</Box>
</Box>
);
})}
2025-11-13 14:33:48 -08:00
{
/*
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) && (
2025-11-13 14:33:48 -08:00
<Box
height={0}
width={contentWidth}
2025-11-13 14:33:48 -08:00
borderLeft={true}
borderRight={true}
borderTop={false}
borderBottom={borderBottomOverride ?? true}
2025-11-13 14:33:48 -08:00
borderColor={borderColor}
borderDimColor={borderDimColor}
borderStyle="round"
/>
)
}
{(borderBottomOverride ?? true) && visibleToolCalls.length > 0 && (
<ShowMoreLines constrainHeight={constrainHeight} />
)}
2025-04-17 18:06:21 -04:00
</Box>
);
2025-04-15 21:41:08 -07:00
};