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

154 lines
5.0 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 } from '../../types.js';
2025-04-18 19:09:41 -04:00
import { ToolMessage } from './ToolMessage.js';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import { theme } from '../../semantic-colors.js';
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
import { useConfig } from '../../contexts/ConfigContext.js';
2025-11-11 07:50:11 -08:00
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.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;
2025-06-12 02:21:54 +01:00
isFocused?: boolean;
activeShellPtyId?: number | null;
embeddedShellFocused?: boolean;
onShellInputSubmit?: (input: string) => void;
2025-04-15 21:41:08 -07:00
}
// Main component renders the border and maps the tools using ToolMessage
2025-04-18 19:09:41 -04:00
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
2025-04-17 18:06:21 -04:00
toolCalls,
availableTerminalHeight,
terminalWidth,
2025-06-12 02:21:54 +01:00
isFocused = true,
activeShellPtyId,
embeddedShellFocused,
2025-04-17 18:06:21 -04:00
}) => {
const isEmbeddedShellFocused =
embeddedShellFocused &&
toolCalls.some(
(t) =>
t.ptyId === activeShellPtyId && t.status === ToolCallStatus.Executing,
);
2025-04-22 07:48:12 -04:00
const hasPending = !toolCalls.every(
(t) => t.status === ToolCallStatus.Success,
);
const config = useConfig();
2025-11-11 07:50:11 -08:00
const isAlternateBuffer = useAlternateBuffer();
const isShellCommand = toolCalls.some(
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_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 staticHeight = /* border */ 2 + /* marginBottom */ 1;
// This is a bit of a magic number, but it accounts for the border and
2025-11-11 07:50:11 -08:00
// marginLeft in regular mode and just the border in alternate buffer mode.
const innerWidth = isAlternateBuffer ? terminalWidth - 3 : terminalWidth - 4;
// only prompt for tool approval on the first 'confirming' tool in the list
// note, after the CTA, this automatically moves over to the next 'confirming' tool
const toolAwaitingApproval = useMemo(
() => toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
[toolCalls],
);
let countToolCallsWithResults = 0;
for (const tool of toolCalls) {
if (tool.resultDisplay !== undefined && tool.resultDisplay !== '') {
countToolCallsWithResults++;
}
}
const countOneLineToolCalls = toolCalls.length - countToolCallsWithResults;
const availableTerminalHeightPerToolMessage = availableTerminalHeight
? Math.max(
Math.floor(
(availableTerminalHeight - staticHeight - countOneLineToolCalls) /
Math.max(1, countToolCallsWithResults),
),
1,
)
: undefined;
2025-04-17 18:06:21 -04:00
return (
2025-04-22 07:48:12 -04:00
<Box
flexDirection="column"
borderStyle="round"
/*
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}
borderDimColor={
hasPending && (!isShellCommand || !isEmbeddedShellFocused)
}
2025-04-22 07:48:12 -04:00
borderColor={borderColor}
gap={1}
2025-04-22 07:48:12 -04:00
>
{toolCalls.map((tool) => {
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
return (
2025-11-11 07:50:11 -08:00
<Box
key={tool.callId}
flexDirection="column"
minHeight={1}
width={innerWidth}
>
<ToolMessage
{...tool}
availableTerminalHeight={availableTerminalHeightPerToolMessage}
terminalWidth={innerWidth}
emphasis={
isConfirming ? 'high' : toolAwaitingApproval ? 'low' : 'medium'
}
activeShellPtyId={activeShellPtyId}
embeddedShellFocused={embeddedShellFocused}
config={config}
/>
{tool.status === ToolCallStatus.Confirming &&
isConfirming &&
tool.confirmationDetails && (
<ToolConfirmationMessage
confirmationDetails={tool.confirmationDetails}
config={config}
2025-06-12 02:21:54 +01:00
isFocused={isFocused}
availableTerminalHeight={
availableTerminalHeightPerToolMessage
}
terminalWidth={innerWidth}
/>
)}
{tool.outputFile && (
<Box marginX={1}>
2025-09-10 10:57:07 -07:00
<Text color={theme.text.primary}>
Output too long and was saved to: {tool.outputFile}
</Text>
</Box>
)}
</Box>
);
})}
2025-04-17 18:06:21 -04:00
</Box>
);
2025-04-15 21:41:08 -07:00
};