mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
Merge branch 'main' into task/subprocess_xml_tagging
This commit is contained in:
@@ -178,6 +178,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
[Command.DELETE_WORD_FORWARD]: [
|
||||
{ key: 'delete', ctrl: true },
|
||||
{ key: 'delete', alt: true },
|
||||
{ key: 'd', alt: true },
|
||||
],
|
||||
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
|
||||
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
|
||||
|
||||
@@ -216,8 +216,18 @@ describe('App', () => {
|
||||
|
||||
const stateWithConfirmingTool = {
|
||||
...mockUIState,
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingGeminiHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
pendingGeminiHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
} as UIState;
|
||||
|
||||
const configWithExperiment = makeFakeConfig();
|
||||
|
||||
@@ -53,8 +53,6 @@ export const AlternateBufferQuittingDisplay = () => {
|
||||
terminalWidth={uiState.mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
activeShellPtyId={uiState.activePtyId}
|
||||
embeddedShellFocused={uiState.embeddedShellFocused}
|
||||
/>
|
||||
))}
|
||||
{showPromptedTool && (
|
||||
|
||||
@@ -44,8 +44,6 @@ interface HistoryItemDisplayProps {
|
||||
terminalWidth: number;
|
||||
isPending: boolean;
|
||||
commands?: readonly SlashCommand[];
|
||||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
availableTerminalHeightGemini?: number;
|
||||
}
|
||||
|
||||
@@ -55,8 +53,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
terminalWidth,
|
||||
isPending,
|
||||
commands,
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
availableTerminalHeightGemini,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
@@ -173,12 +169,10 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
)}
|
||||
{itemForDisplay.type === 'tool_group' && (
|
||||
<ToolGroupMessage
|
||||
item={itemForDisplay}
|
||||
toolCalls={itemForDisplay.tools}
|
||||
groupId={itemForDisplay.id}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
activeShellPtyId={activeShellPtyId}
|
||||
embeddedShellFocused={embeddedShellFocused}
|
||||
borderTop={itemForDisplay.borderTop}
|
||||
borderBottom={itemForDisplay.borderBottom}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { MainContent } from './MainContent.js';
|
||||
import { getToolGroupBorderAppearance } from '../utils/borderStyles.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Box, Text } from 'ink';
|
||||
import { act, useState, type JSX } from 'react';
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
type UIState,
|
||||
} from '../contexts/UIStateContext.js';
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import { type IndividualToolCallDisplay } from '../types.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../contexts/SettingsContext.js', async () => {
|
||||
@@ -76,6 +78,209 @@ vi.mock('./shared/ScrollableList.js', () => ({
|
||||
SCROLL_TO_ITEM_END: 0,
|
||||
}));
|
||||
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { type BackgroundShell } from '../hooks/shellReducer.js';
|
||||
|
||||
describe('getToolGroupBorderAppearance', () => {
|
||||
const mockBackgroundShells = new Map<number, BackgroundShell>();
|
||||
const activeShellPtyId = 123;
|
||||
|
||||
it('returns default empty values for non-tool_group items', () => {
|
||||
const item = { type: 'user' as const, text: 'Hello', id: 1 };
|
||||
const result = getToolGroupBorderAppearance(
|
||||
item,
|
||||
null,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({ borderColor: '', borderDimColor: false });
|
||||
});
|
||||
|
||||
it('inspects only the last pending tool_group item if current has no tools', () => {
|
||||
const item = { type: 'tool_group' as const, tools: [], id: 1 };
|
||||
const pendingItems = [
|
||||
{
|
||||
type: 'tool_group' as const,
|
||||
tools: [
|
||||
{
|
||||
callId: '1',
|
||||
name: 'some_tool',
|
||||
description: '',
|
||||
status: CoreToolCallStatus.Executing,
|
||||
ptyId: undefined,
|
||||
resultDisplay: undefined,
|
||||
confirmationDetails: undefined,
|
||||
} as IndividualToolCallDisplay,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tool_group' as const,
|
||||
tools: [
|
||||
{
|
||||
callId: '2',
|
||||
name: 'other_tool',
|
||||
description: '',
|
||||
status: CoreToolCallStatus.Success,
|
||||
ptyId: undefined,
|
||||
resultDisplay: undefined,
|
||||
confirmationDetails: undefined,
|
||||
} as IndividualToolCallDisplay,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Only the last item (Success) should be inspected, so hasPending = false.
|
||||
// The previous item was Executing (pending) but it shouldn't be counted.
|
||||
const result = getToolGroupBorderAppearance(
|
||||
item,
|
||||
null,
|
||||
false,
|
||||
pendingItems,
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.border.default,
|
||||
borderDimColor: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns default border for completed normal tools', () => {
|
||||
const item = {
|
||||
type: 'tool_group' as const,
|
||||
tools: [
|
||||
{
|
||||
callId: '1',
|
||||
name: 'some_tool',
|
||||
description: '',
|
||||
status: CoreToolCallStatus.Success,
|
||||
ptyId: undefined,
|
||||
resultDisplay: undefined,
|
||||
confirmationDetails: undefined,
|
||||
} as IndividualToolCallDisplay,
|
||||
],
|
||||
id: 1,
|
||||
};
|
||||
const result = getToolGroupBorderAppearance(
|
||||
item,
|
||||
null,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.border.default,
|
||||
borderDimColor: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns warning border for pending normal tools', () => {
|
||||
const item = {
|
||||
type: 'tool_group' as const,
|
||||
tools: [
|
||||
{
|
||||
callId: '1',
|
||||
name: 'some_tool',
|
||||
description: '',
|
||||
status: CoreToolCallStatus.Executing,
|
||||
ptyId: undefined,
|
||||
resultDisplay: undefined,
|
||||
confirmationDetails: undefined,
|
||||
} as IndividualToolCallDisplay,
|
||||
],
|
||||
id: 1,
|
||||
};
|
||||
const result = getToolGroupBorderAppearance(
|
||||
item,
|
||||
null,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.status.warning,
|
||||
borderDimColor: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns symbol border for executing shell commands', () => {
|
||||
const item = {
|
||||
type: 'tool_group' as const,
|
||||
tools: [
|
||||
{
|
||||
callId: '1',
|
||||
name: SHELL_COMMAND_NAME,
|
||||
description: '',
|
||||
status: CoreToolCallStatus.Executing,
|
||||
ptyId: activeShellPtyId,
|
||||
resultDisplay: undefined,
|
||||
confirmationDetails: undefined,
|
||||
} as IndividualToolCallDisplay,
|
||||
],
|
||||
id: 1,
|
||||
};
|
||||
// While executing shell commands, it's dim false, border symbol
|
||||
const result = getToolGroupBorderAppearance(
|
||||
item,
|
||||
activeShellPtyId,
|
||||
true,
|
||||
[],
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.ui.symbol,
|
||||
borderDimColor: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns symbol border and dims color for background executing shell command when another shell is active', () => {
|
||||
const item = {
|
||||
type: 'tool_group' as const,
|
||||
tools: [
|
||||
{
|
||||
callId: '1',
|
||||
name: SHELL_COMMAND_NAME,
|
||||
description: '',
|
||||
status: CoreToolCallStatus.Executing,
|
||||
ptyId: 456, // Different ptyId, not active
|
||||
resultDisplay: undefined,
|
||||
confirmationDetails: undefined,
|
||||
} as IndividualToolCallDisplay,
|
||||
],
|
||||
id: 1,
|
||||
};
|
||||
const result = getToolGroupBorderAppearance(
|
||||
item,
|
||||
activeShellPtyId,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.ui.symbol,
|
||||
borderDimColor: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty tools with active shell turn (isCurrentlyInShellTurn)', () => {
|
||||
const item = { type: 'tool_group' as const, tools: [], id: 1 };
|
||||
|
||||
// active shell turn
|
||||
const result = getToolGroupBorderAppearance(
|
||||
item,
|
||||
activeShellPtyId,
|
||||
true,
|
||||
[],
|
||||
mockBackgroundShells,
|
||||
);
|
||||
// Since there are no tools to inspect, it falls back to empty pending, but isCurrentlyInShellTurn=true
|
||||
// so it counts as pending shell.
|
||||
expect(result.borderColor).toEqual(theme.ui.symbol);
|
||||
// It shouldn't be dim because there are no tools to say it isEmbeddedShellFocused = false
|
||||
expect(result.borderDimColor).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MainContent', () => {
|
||||
const defaultMockUiState = {
|
||||
history: [
|
||||
@@ -258,7 +463,7 @@ describe('MainContent', () => {
|
||||
history: [],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group' as const,
|
||||
type: 'tool_group',
|
||||
id: 1,
|
||||
tools: [
|
||||
{
|
||||
|
||||
@@ -88,8 +88,6 @@ export const MainContent = () => {
|
||||
terminalWidth={mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
activeShellPtyId={uiState.activePtyId}
|
||||
embeddedShellFocused={uiState.embeddedShellFocused}
|
||||
/>
|
||||
))}
|
||||
{showConfirmationQueue && confirmingTool && (
|
||||
@@ -103,8 +101,6 @@ export const MainContent = () => {
|
||||
isAlternateBuffer,
|
||||
availableTerminalHeight,
|
||||
mainAreaWidth,
|
||||
uiState.activePtyId,
|
||||
uiState.embeddedShellFocused,
|
||||
showConfirmationQueue,
|
||||
confirmingTool,
|
||||
],
|
||||
|
||||
@@ -14,5 +14,9 @@ interface SessionSummaryDisplayProps {
|
||||
export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
|
||||
duration,
|
||||
}) => (
|
||||
<StatsDisplay title="Agent powering down. Goodbye!" duration={duration} />
|
||||
<StatsDisplay
|
||||
title="Agent powering down. Goodbye!"
|
||||
duration={duration}
|
||||
footer="Tip: Resume a previous session using gemini --resume or /resume"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -178,7 +178,7 @@ const ModelUsageTable: React.FC<{
|
||||
: `Model Usage`;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{/* Header */}
|
||||
<Box alignItems="flex-end">
|
||||
<Box width={nameWidth}>
|
||||
@@ -379,6 +379,7 @@ interface StatsDisplayProps {
|
||||
duration: string;
|
||||
title?: string;
|
||||
quotas?: RetrieveUserQuotaResponse;
|
||||
footer?: string;
|
||||
selectedAuthType?: string;
|
||||
userEmail?: string;
|
||||
tier?: string;
|
||||
@@ -390,6 +391,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
duration,
|
||||
title,
|
||||
quotas,
|
||||
footer,
|
||||
selectedAuthType,
|
||||
userEmail,
|
||||
tier,
|
||||
@@ -433,6 +435,13 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderFooter = () => {
|
||||
if (!footer) {
|
||||
return null;
|
||||
}
|
||||
return <ThemedGradient bold>{footer}</ThemedGradient>;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
@@ -536,6 +545,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
pooledLimit={pooledLimit}
|
||||
pooledResetTime={pooledResetTime}
|
||||
/>
|
||||
{renderFooter()}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Box } from 'ink';
|
||||
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { StreamingState, ToolCallStatus } from '../types.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { type Config, CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
@@ -223,6 +223,58 @@ describe('ToolConfirmationQueue', () => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('provides more height for ask_user by subtracting less overhead', async () => {
|
||||
const confirmingTool = {
|
||||
tool: {
|
||||
callId: 'call-1',
|
||||
name: 'ask_user',
|
||||
description: 'ask user',
|
||||
status: ToolCallStatus.Confirming,
|
||||
confirmationDetails: {
|
||||
type: 'ask_user' as const,
|
||||
questions: [
|
||||
{
|
||||
type: 'choice',
|
||||
header: 'Height Test',
|
||||
question: 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6',
|
||||
options: [{ label: 'Option 1', description: 'Desc' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
index: 1,
|
||||
total: 1,
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolConfirmationQueue
|
||||
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
|
||||
/>,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState: {
|
||||
terminalWidth: 80,
|
||||
terminalHeight: 40,
|
||||
availableTerminalHeight: 20,
|
||||
constrainHeight: true,
|
||||
streamingState: StreamingState.WaitingForConfirmation,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Calculation:
|
||||
// availableTerminalHeight: 20 -> maxHeight: 19 (20-1)
|
||||
// hideToolIdentity is true for ask_user -> subtracts 4 instead of 6
|
||||
// availableContentHeight = 19 - 4 = 15
|
||||
// ToolConfirmationMessage handlesOwnUI=true -> returns full 15
|
||||
// AskUserDialog uses 15 lines to render its multi-line question and options.
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Line 6');
|
||||
expect(lastFrame()).not.toContain('lines hidden');
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not render expansion hint when constrainHeight is false', () => {
|
||||
const longDiff = 'line\n'.repeat(50);
|
||||
const confirmingTool = {
|
||||
|
||||
@@ -60,6 +60,12 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
|
||||
? Math.max(uiAvailableHeight - 1, 4)
|
||||
: Math.floor(terminalHeight * 0.5);
|
||||
|
||||
const isRoutine =
|
||||
tool.confirmationDetails?.type === 'ask_user' ||
|
||||
tool.confirmationDetails?.type === 'exit_plan_mode';
|
||||
const borderColor = isRoutine ? theme.status.success : theme.status.warning;
|
||||
const hideToolIdentity = isRoutine;
|
||||
|
||||
// ToolConfirmationMessage needs to know the height available for its OWN content.
|
||||
// We subtract the lines used by the Queue wrapper:
|
||||
// - 2 lines for the rounded border
|
||||
@@ -67,15 +73,9 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
|
||||
// - 2 lines for Tool Identity (text + margin)
|
||||
const availableContentHeight =
|
||||
constrainHeight && !isAlternateBuffer
|
||||
? Math.max(maxHeight - 6, 4)
|
||||
? Math.max(maxHeight - (hideToolIdentity ? 4 : 6), 4)
|
||||
: undefined;
|
||||
|
||||
const isRoutine =
|
||||
tool.confirmationDetails?.type === 'ask_user' ||
|
||||
tool.confirmationDetails?.type === 'exit_plan_mode';
|
||||
const borderColor = isRoutine ? theme.status.success : theme.status.warning;
|
||||
const hideToolIdentity = isRoutine;
|
||||
|
||||
return (
|
||||
<OverflowProvider>
|
||||
<Box flexDirection="column" width={mainAreaWidth} flexShrink={0}>
|
||||
|
||||
@@ -17,12 +17,13 @@ exports[`<SessionSummaryDisplay /> > renders the summary display with a title 1`
|
||||
│ » API Time: 50.2s (100.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 10 500 500 2,000 │
|
||||
│ │
|
||||
│ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. │
|
||||
│ │
|
||||
│ Tip: Resume a previous session using gemini --resume or /resume │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -112,11 +112,11 @@ exports[`<StatsDisplay /> > Conditional Rendering Tests > hides Efficiency secti
|
||||
│ » API Time: 100ms (100.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 1 100 0 100 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -155,7 +155,6 @@ exports[`<StatsDisplay /> > Quota Display > renders pooled quota information for
|
||||
│ » API Time: 0s (0.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ │
|
||||
│ auto Usage │
|
||||
│ 65% usage remaining │
|
||||
│ Usage limit: 1,100 │
|
||||
@@ -166,6 +165,7 @@ exports[`<StatsDisplay /> > Quota Display > renders pooled quota information for
|
||||
│ ──────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro - │
|
||||
│ gemini-2.5-flash - │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -185,11 +185,11 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information for unused
|
||||
│ » API Time: 0s (0.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Usage remaining │
|
||||
│ ──────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-flash - 50.0% resets in 2h │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -209,11 +209,11 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information when quota
|
||||
│ » API Time: 100ms (100.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Usage remaining │
|
||||
│ ──────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 1 75.0% resets in 1h 30m │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -271,7 +271,6 @@ exports[`<StatsDisplay /> > renders a table with two models correctly 1`] = `
|
||||
│ » API Time: 19.5s (100.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────────── │
|
||||
@@ -279,6 +278,7 @@ exports[`<StatsDisplay /> > renders a table with two models correctly 1`] = `
|
||||
│ gemini-2.5-flash 5 15,000 10,000 15,000 │
|
||||
│ │
|
||||
│ Savings Highlight: 10,500 (40.4%) of input tokens were served from the cache, reducing costs. │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -299,13 +299,13 @@ exports[`<StatsDisplay /> > renders all sections when all data is present 1`] =
|
||||
│ » API Time: 100ms (44.8%) │
|
||||
│ » Tool Time: 123ms (55.2%) │
|
||||
│ │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 1 50 50 100 │
|
||||
│ │
|
||||
│ Savings Highlight: 50 (50.0%) of input tokens were served from the cache, reducing costs. │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
|
||||
@@ -40,6 +40,25 @@ exports[`ToolConfirmationQueue > does not render expansion hint when constrainHe
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationQueue > provides more height for ask_user by subtracting less overhead 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Answer Questions │
|
||||
│ │
|
||||
│ Line 1 │
|
||||
│ Line 2 │
|
||||
│ Line 3 │
|
||||
│ Line 4 │
|
||||
│ Line 5 │
|
||||
│ Line 6 │
|
||||
│ │
|
||||
│ ● 1. Option 1 │
|
||||
│ Desc │
|
||||
│ 2. Enter a custom value │
|
||||
│ │
|
||||
│ Enter to select · ↑/↓ to navigate · Esc to cancel │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationQueue > renders AskUser tool confirmation with Success color 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Answer Questions │
|
||||
|
||||
@@ -88,20 +88,16 @@ describe('<ShellToolMessage />', () => {
|
||||
CoreToolCallStatus.Executing,
|
||||
);
|
||||
updateStatus = setStatus;
|
||||
return (
|
||||
<ShellToolMessage
|
||||
{...baseProps}
|
||||
status={status}
|
||||
embeddedShellFocused={true}
|
||||
activeShellPtyId={1}
|
||||
ptyId={1}
|
||||
/>
|
||||
);
|
||||
return <ShellToolMessage {...baseProps} status={status} ptyId={1} />;
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(<Wrapper />, {
|
||||
uiActions,
|
||||
uiState: { streamingState: StreamingState.Idle },
|
||||
uiState: {
|
||||
streamingState: StreamingState.Idle,
|
||||
embeddedShellFocused: true,
|
||||
activePtyId: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify it is initially focused
|
||||
@@ -143,21 +139,29 @@ describe('<ShellToolMessage />', () => {
|
||||
'renders in Alternate Buffer mode while focused',
|
||||
{
|
||||
status: CoreToolCallStatus.Executing,
|
||||
embeddedShellFocused: true,
|
||||
activeShellPtyId: 1,
|
||||
ptyId: 1,
|
||||
},
|
||||
{ useAlternateBuffer: true },
|
||||
{
|
||||
useAlternateBuffer: true,
|
||||
uiState: {
|
||||
embeddedShellFocused: true,
|
||||
activePtyId: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'renders in Alternate Buffer mode while unfocused',
|
||||
{
|
||||
status: CoreToolCallStatus.Executing,
|
||||
embeddedShellFocused: false,
|
||||
activeShellPtyId: 1,
|
||||
ptyId: 1,
|
||||
},
|
||||
{ useAlternateBuffer: true },
|
||||
{
|
||||
useAlternateBuffer: true,
|
||||
uiState: {
|
||||
embeddedShellFocused: false,
|
||||
activePtyId: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
])('%s', async (_, props, options) => {
|
||||
const { lastFrame } = renderShell(props, options);
|
||||
@@ -199,12 +203,16 @@ describe('<ShellToolMessage />', () => {
|
||||
resultDisplay: LONG_OUTPUT,
|
||||
renderOutputAsMarkdown: false,
|
||||
availableTerminalHeight,
|
||||
activeShellPtyId: 1,
|
||||
ptyId: focused ? 1 : 2,
|
||||
ptyId: 1,
|
||||
status: CoreToolCallStatus.Executing,
|
||||
embeddedShellFocused: focused,
|
||||
},
|
||||
{ useAlternateBuffer: true },
|
||||
{
|
||||
useAlternateBuffer: true,
|
||||
uiState: {
|
||||
activePtyId: focused ? 1 : 2,
|
||||
embeddedShellFocused: focused,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -29,9 +29,9 @@ import {
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
import { type Config, CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
|
||||
export interface ShellToolMessageProps extends ToolMessageProps {
|
||||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
config?: Config;
|
||||
}
|
||||
|
||||
@@ -52,10 +52,6 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
|
||||
renderOutputAsMarkdown = true,
|
||||
|
||||
activeShellPtyId,
|
||||
|
||||
embeddedShellFocused,
|
||||
|
||||
ptyId,
|
||||
|
||||
config,
|
||||
@@ -66,6 +62,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
|
||||
borderDimColor,
|
||||
}) => {
|
||||
const { activePtyId: activeShellPtyId, embeddedShellFocused } = useUIState();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const isThisShellFocused = checkIsShellFocused(
|
||||
name,
|
||||
|
||||
@@ -235,6 +235,10 @@ export const ToolConfirmationMessage: React.FC<
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (handlesOwnUI) {
|
||||
return availableTerminalHeight;
|
||||
}
|
||||
|
||||
// Calculate the vertical space (in lines) consumed by UI elements
|
||||
// surrounding the main body content.
|
||||
const PADDING_OUTER_Y = 2; // Main container has `padding={1}` (top & bottom).
|
||||
@@ -253,7 +257,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
1; // Reserve one line for 'ShowMoreLines' hint
|
||||
|
||||
return Math.max(availableTerminalHeight - surroundingElementsHeight, 1);
|
||||
}, [availableTerminalHeight, getOptions]);
|
||||
}, [availableTerminalHeight, getOptions, handlesOwnUI]);
|
||||
|
||||
const { question, bodyContent, options } = useMemo(() => {
|
||||
let bodyContent: React.ReactNode | null = null;
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { ToolGroupMessage } from './ToolGroupMessage.js';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryItemWithoutId,
|
||||
IndividualToolCallDisplay,
|
||||
} from '../../types.js';
|
||||
import { Scrollable } from '../shared/Scrollable.js';
|
||||
import {
|
||||
makeFakeConfig,
|
||||
@@ -40,10 +44,17 @@ describe('<ToolGroupMessage />', () => {
|
||||
});
|
||||
|
||||
const baseProps = {
|
||||
groupId: 1,
|
||||
terminalWidth: 80,
|
||||
};
|
||||
|
||||
const createItem = (
|
||||
tools: IndividualToolCallDisplay[],
|
||||
): HistoryItem | HistoryItemWithoutId => ({
|
||||
id: 1,
|
||||
type: 'tool_group',
|
||||
tools,
|
||||
});
|
||||
|
||||
const baseMockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
@@ -56,12 +67,18 @@ describe('<ToolGroupMessage />', () => {
|
||||
describe('Golden Snapshots', () => {
|
||||
it('renders single successful tool call', () => {
|
||||
const toolCalls = [createToolCall()];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -81,9 +98,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
@@ -113,13 +131,19 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: CoreToolCallStatus.Error,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -153,13 +177,19 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -188,16 +218,23 @@ describe('<ToolGroupMessage />', () => {
|
||||
resultDisplay: 'More output here',
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
availableTerminalHeight={10}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -213,16 +250,23 @@ describe('<ToolGroupMessage />', () => {
|
||||
'This is a very long description that might cause wrapping issues',
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
terminalWidth={40}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -231,12 +275,19 @@ describe('<ToolGroupMessage />', () => {
|
||||
});
|
||||
|
||||
it('renders empty tool calls array', () => {
|
||||
const toolCalls: IndividualToolCallDisplay[] = [];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={[]} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: [] }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -260,14 +311,20 @@ describe('<ToolGroupMessage />', () => {
|
||||
resultDisplay: 'line1\nline2',
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<Scrollable height={10} hasFocus={true} scrollToBottom={true}>
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />
|
||||
</Scrollable>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -285,12 +342,18 @@ describe('<ToolGroupMessage />', () => {
|
||||
outputFile: '/path/to/output.txt',
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -307,6 +370,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
resultDisplay: 'line1\nline2\nline3\nline4\nline5',
|
||||
}),
|
||||
];
|
||||
const item1 = createItem(toolCalls1);
|
||||
const toolCalls2 = [
|
||||
createToolCall({
|
||||
callId: '2',
|
||||
@@ -315,18 +379,33 @@ describe('<ToolGroupMessage />', () => {
|
||||
resultDisplay: 'line1',
|
||||
}),
|
||||
];
|
||||
const item2 = createItem(toolCalls2);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<Scrollable height={6} hasFocus={true} scrollToBottom={true}>
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls1} />
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls2} />
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item1}
|
||||
toolCalls={toolCalls1}
|
||||
/>
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item2}
|
||||
toolCalls={toolCalls2}
|
||||
/>
|
||||
</Scrollable>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [
|
||||
{ type: 'tool_group', tools: toolCalls1 },
|
||||
{ type: 'tool_group', tools: toolCalls2 },
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls1,
|
||||
},
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -344,12 +423,18 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: CoreToolCallStatus.Success,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -366,12 +451,18 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: CoreToolCallStatus.Success,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -396,16 +487,23 @@ describe('<ToolGroupMessage />', () => {
|
||||
resultDisplay: '', // No result
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -453,9 +551,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
resultDisplay,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
@@ -481,9 +580,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
@@ -502,10 +602,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: CoreToolCallStatus.Executing,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
borderBottom={false}
|
||||
/>,
|
||||
@@ -540,9 +642,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
approvalMode: mode,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
|
||||
@@ -7,27 +7,27 @@
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
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, isThisShellFocused } from './ToolShared.js';
|
||||
import {
|
||||
CoreToolCallStatus,
|
||||
shouldHideToolCall,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { isShellTool } from './ToolShared.js';
|
||||
import { shouldHideToolCall } from '@google/gemini-cli-core';
|
||||
import { ShowMoreLines } from '../ShowMoreLines.js';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
|
||||
|
||||
interface ToolGroupMessageProps {
|
||||
groupId: number;
|
||||
item: HistoryItem | HistoryItemWithoutId;
|
||||
toolCalls: IndividualToolCallDisplay[];
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
onShellInputSubmit?: (input: string) => void;
|
||||
borderTop?: boolean;
|
||||
borderBottom?: boolean;
|
||||
@@ -37,11 +37,10 @@ interface ToolGroupMessageProps {
|
||||
const TOOL_MESSAGE_HORIZONTAL_MARGIN = 4;
|
||||
|
||||
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
item,
|
||||
toolCalls: allToolCalls,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
borderTop: borderTopOverride,
|
||||
borderBottom: borderBottomOverride,
|
||||
}) => {
|
||||
@@ -61,7 +60,31 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
);
|
||||
|
||||
const config = useConfig();
|
||||
const { constrainHeight } = useUIState();
|
||||
const {
|
||||
constrainHeight,
|
||||
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.
|
||||
@@ -80,31 +103,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
[toolCalls],
|
||||
);
|
||||
|
||||
const isEmbeddedShellFocused = visibleToolCalls.some((t) =>
|
||||
isThisShellFocused(
|
||||
t.name,
|
||||
t.status,
|
||||
t.ptyId,
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
),
|
||||
);
|
||||
|
||||
const hasPending = !visibleToolCalls.every(
|
||||
(t) => t.status === CoreToolCallStatus.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;
|
||||
|
||||
// If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools),
|
||||
@@ -175,12 +173,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
width={contentWidth}
|
||||
>
|
||||
{isShellToolCall ? (
|
||||
<ShellToolMessage
|
||||
{...commonProps}
|
||||
activeShellPtyId={activeShellPtyId}
|
||||
embeddedShellFocused={embeddedShellFocused}
|
||||
config={config}
|
||||
/>
|
||||
<ShellToolMessage {...commonProps} config={config} />
|
||||
) : (
|
||||
<ToolMessage {...commonProps} />
|
||||
)}
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('ToolResultDisplay Overflow', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<OverflowProvider>
|
||||
<ToolGroupMessage
|
||||
groupId={1}
|
||||
item={{ id: 1, type: 'tool_group', tools: toolCalls }}
|
||||
toolCalls={toolCalls}
|
||||
availableTerminalHeight={15} // Small height to force overflow
|
||||
terminalWidth={80}
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
data={['item1']}
|
||||
renderItem={() => (
|
||||
<ToolGroupMessage
|
||||
groupId={1}
|
||||
item={{ id: 1, type: 'tool_group', tools: toolCalls }}
|
||||
toolCalls={toolCalls}
|
||||
terminalWidth={terminalWidth - 2} // Account for ScrollableList padding
|
||||
/>
|
||||
@@ -165,7 +165,7 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
data={['item1']}
|
||||
renderItem={() => (
|
||||
<ToolGroupMessage
|
||||
groupId={1}
|
||||
item={{ id: 1, type: 'tool_group', tools: toolCalls }}
|
||||
toolCalls={toolCalls}
|
||||
terminalWidth={terminalWidth - 2}
|
||||
/>
|
||||
|
||||
@@ -141,6 +141,7 @@ const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
|
||||
'\u00B5': 'm', // "µ" toggle markup view
|
||||
'\u03A9': 'z', // "Ω" Option+z
|
||||
'\u00B8': 'Z', // "¸" Option+Shift+z
|
||||
'\u2202': 'd', // "∂" delete word forward
|
||||
};
|
||||
|
||||
function nonKeyboardEventFilter(
|
||||
|
||||
@@ -1286,7 +1286,9 @@ describe('handleAtCommand', () => {
|
||||
// Assert
|
||||
// It SHOULD be called for the tool_group
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'tool_group' }),
|
||||
expect.objectContaining({
|
||||
type: 'tool_group',
|
||||
}),
|
||||
999,
|
||||
);
|
||||
|
||||
@@ -1343,7 +1345,9 @@ describe('handleAtCommand', () => {
|
||||
});
|
||||
expect(containsResourceText).toBe(true);
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'tool_group' }),
|
||||
expect.objectContaining({
|
||||
type: 'tool_group',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -23,10 +23,15 @@ import {
|
||||
*/
|
||||
export function mapToDisplay(
|
||||
toolOrTools: ToolCall[] | ToolCall,
|
||||
options: { borderTop?: boolean; borderBottom?: boolean } = {},
|
||||
options: {
|
||||
borderTop?: boolean;
|
||||
borderBottom?: boolean;
|
||||
borderColor?: string;
|
||||
borderDimColor?: boolean;
|
||||
} = {},
|
||||
): HistoryItemToolGroup {
|
||||
const toolCalls = Array.isArray(toolOrTools) ? toolOrTools : [toolOrTools];
|
||||
const { borderTop, borderBottom } = options;
|
||||
const { borderTop, borderBottom, borderColor, borderDimColor } = options;
|
||||
|
||||
const toolDisplays = toolCalls.map((call): IndividualToolCallDisplay => {
|
||||
let description: string;
|
||||
@@ -104,5 +109,7 @@ export function mapToDisplay(
|
||||
tools: toolDisplays,
|
||||
borderTop,
|
||||
borderBottom,
|
||||
borderColor,
|
||||
borderDimColor,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import type { Part, PartListUnion } from '@google/genai';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import type { SlashCommandProcessorResult } from '../types.js';
|
||||
import { MessageType, StreamingState } from '../types.js';
|
||||
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
// --- MOCKS ---
|
||||
|
||||
@@ -78,6 +78,8 @@ import {
|
||||
type TrackedWaitingToolCall,
|
||||
type TrackedExecutingToolCall,
|
||||
} from './useToolScheduler.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { getToolGroupBorderAppearance } from '../utils/borderStyles.js';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
@@ -250,6 +252,8 @@ export const useGeminiStream = (
|
||||
mapTrackedToolCallsToDisplay(toolsToPush as TrackedToolCall[], {
|
||||
borderTop: isFirstToolInGroupRef.current,
|
||||
borderBottom: true,
|
||||
borderColor: theme.border.default,
|
||||
borderDimColor: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -290,6 +294,45 @@ export const useGeminiStream = (
|
||||
getPreferredEditor,
|
||||
);
|
||||
|
||||
const activeToolPtyId = useMemo(() => {
|
||||
const executingShellTool = toolCalls.find(
|
||||
(tc) =>
|
||||
tc.status === 'executing' && tc.request.name === 'run_shell_command',
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return (executingShellTool as TrackedExecutingToolCall | undefined)?.pid;
|
||||
}, [toolCalls]);
|
||||
|
||||
const onExec = useCallback(async (done: Promise<void>) => {
|
||||
setIsResponding(true);
|
||||
await done;
|
||||
setIsResponding(false);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
handleShellCommand,
|
||||
activeShellPtyId,
|
||||
lastShellOutputTime,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
toggleBackgroundShell,
|
||||
backgroundCurrentShell,
|
||||
registerBackgroundShell,
|
||||
dismissBackgroundShell,
|
||||
backgroundShells,
|
||||
} = useShellCommandProcessor(
|
||||
addItem,
|
||||
setPendingHistoryItem,
|
||||
onExec,
|
||||
onDebugMessage,
|
||||
config,
|
||||
geminiClient,
|
||||
setShellInputFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
activeToolPtyId,
|
||||
);
|
||||
|
||||
const streamingState = useMemo(
|
||||
() => calculateStreamingState(isResponding, toolCalls),
|
||||
[isResponding, toolCalls],
|
||||
@@ -347,6 +390,13 @@ export const useGeminiStream = (
|
||||
const historyItem = mapTrackedToolCallsToDisplay(tc, {
|
||||
borderTop: isFirst,
|
||||
borderBottom: isLastInBatch,
|
||||
...getToolGroupBorderAppearance(
|
||||
{ type: 'tool_group', tools: toolCalls },
|
||||
activeShellPtyId,
|
||||
!!isShellFocused,
|
||||
[],
|
||||
backgroundShells,
|
||||
),
|
||||
});
|
||||
addItem(historyItem);
|
||||
isFirst = false;
|
||||
@@ -362,6 +412,9 @@ export const useGeminiStream = (
|
||||
setPushedToolCallIds,
|
||||
setIsFirstToolInGroup,
|
||||
addItem,
|
||||
activeShellPtyId,
|
||||
isShellFocused,
|
||||
backgroundShells,
|
||||
]);
|
||||
|
||||
const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => {
|
||||
@@ -371,11 +424,20 @@ export const useGeminiStream = (
|
||||
|
||||
const items: HistoryItemWithoutId[] = [];
|
||||
|
||||
const appearance = getToolGroupBorderAppearance(
|
||||
{ type: 'tool_group', tools: toolCalls },
|
||||
activeShellPtyId,
|
||||
!!isShellFocused,
|
||||
[],
|
||||
backgroundShells,
|
||||
);
|
||||
|
||||
if (remainingTools.length > 0) {
|
||||
items.push(
|
||||
mapTrackedToolCallsToDisplay(remainingTools, {
|
||||
borderTop: pushedToolCallIds.size === 0,
|
||||
borderBottom: false, // Stay open to connect with the slice below
|
||||
...appearance,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -423,20 +485,18 @@ export const useGeminiStream = (
|
||||
tools: [] as IndividualToolCallDisplay[],
|
||||
borderTop: false,
|
||||
borderBottom: true,
|
||||
...appearance,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [toolCalls, pushedToolCallIds]);
|
||||
|
||||
const activeToolPtyId = useMemo(() => {
|
||||
const executingShellTool = toolCalls.find(
|
||||
(tc) =>
|
||||
tc.status === 'executing' && tc.request.name === 'run_shell_command',
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return (executingShellTool as TrackedExecutingToolCall | undefined)?.pid;
|
||||
}, [toolCalls]);
|
||||
}, [
|
||||
toolCalls,
|
||||
pushedToolCallIds,
|
||||
activeShellPtyId,
|
||||
isShellFocused,
|
||||
backgroundShells,
|
||||
]);
|
||||
|
||||
const lastQueryRef = useRef<PartListUnion | null>(null);
|
||||
const lastPromptIdRef = useRef<string | null>(null);
|
||||
@@ -448,36 +508,6 @@ export const useGeminiStream = (
|
||||
onComplete: (result: { userSelection: 'disable' | 'keep' }) => void;
|
||||
} | null>(null);
|
||||
|
||||
const onExec = useCallback(async (done: Promise<void>) => {
|
||||
setIsResponding(true);
|
||||
await done;
|
||||
setIsResponding(false);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
handleShellCommand,
|
||||
activeShellPtyId,
|
||||
lastShellOutputTime,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
toggleBackgroundShell,
|
||||
backgroundCurrentShell,
|
||||
registerBackgroundShell,
|
||||
dismissBackgroundShell,
|
||||
backgroundShells,
|
||||
} = useShellCommandProcessor(
|
||||
addItem,
|
||||
setPendingHistoryItem,
|
||||
onExec,
|
||||
onDebugMessage,
|
||||
config,
|
||||
geminiClient,
|
||||
setShellInputFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
activeToolPtyId,
|
||||
);
|
||||
|
||||
const activePtyId = activeShellPtyId || activeToolPtyId;
|
||||
|
||||
const prevActiveShellPtyIdRef = useRef<number | null>(null);
|
||||
|
||||
@@ -130,6 +130,7 @@ describe('keyMatchers', () => {
|
||||
positive: [
|
||||
createKey('delete', { ctrl: true }),
|
||||
createKey('delete', { alt: true }),
|
||||
createKey('d', { alt: true }),
|
||||
],
|
||||
negative: [createKey('delete'), createKey('backspace', { ctrl: true })],
|
||||
},
|
||||
|
||||
@@ -221,6 +221,8 @@ export type HistoryItemToolGroup = HistoryItemBase & {
|
||||
tools: IndividualToolCallDisplay[];
|
||||
borderTop?: boolean;
|
||||
borderBottom?: boolean;
|
||||
borderColor?: string;
|
||||
borderDimColor?: boolean;
|
||||
};
|
||||
|
||||
export type HistoryItemUserShell = HistoryItemBase & {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import { isShellTool } from '../components/messages/ToolShared.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryItemWithoutId,
|
||||
HistoryItemToolGroup,
|
||||
IndividualToolCallDisplay,
|
||||
} from '../types.js';
|
||||
import type { BackgroundShell } from '../hooks/shellReducer.js';
|
||||
import type { TrackedToolCall } from '../hooks/useToolScheduler.js';
|
||||
|
||||
function isTrackedToolCall(
|
||||
tool: IndividualToolCallDisplay | TrackedToolCall,
|
||||
): tool is TrackedToolCall {
|
||||
return 'request' in tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the border color and dimming state for a tool group message.
|
||||
*/
|
||||
export function getToolGroupBorderAppearance(
|
||||
item:
|
||||
| HistoryItem
|
||||
| HistoryItemWithoutId
|
||||
| { type: 'tool_group'; tools: TrackedToolCall[] },
|
||||
activeShellPtyId: number | null | undefined,
|
||||
embeddedShellFocused: boolean | undefined,
|
||||
allPendingItems: HistoryItemWithoutId[] = [],
|
||||
backgroundShells: Map<number, BackgroundShell> = new Map(),
|
||||
): { borderColor: string; borderDimColor: boolean } {
|
||||
if (item.type !== 'tool_group') {
|
||||
return { borderColor: '', borderDimColor: false };
|
||||
}
|
||||
|
||||
// If this item has no tools, it's a closing slice for the current batch.
|
||||
// We need to look at the last pending item to determine the batch's appearance.
|
||||
const toolsToInspect: Array<IndividualToolCallDisplay | TrackedToolCall> =
|
||||
item.tools.length > 0
|
||||
? item.tools
|
||||
: allPendingItems
|
||||
.filter(
|
||||
(i): i is HistoryItemToolGroup =>
|
||||
i !== null && i !== undefined && i.type === 'tool_group',
|
||||
)
|
||||
.slice(-1)
|
||||
.flatMap((i) => i.tools);
|
||||
|
||||
const hasPending = toolsToInspect.some((t) => {
|
||||
if (isTrackedToolCall(t)) {
|
||||
return (
|
||||
t.status !== 'success' &&
|
||||
t.status !== 'error' &&
|
||||
t.status !== 'cancelled'
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
t.status !== CoreToolCallStatus.Success &&
|
||||
t.status !== CoreToolCallStatus.Error &&
|
||||
t.status !== CoreToolCallStatus.Cancelled
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const isEmbeddedShellFocused = toolsToInspect.some((t) => {
|
||||
if (isTrackedToolCall(t)) {
|
||||
return (
|
||||
isShellTool(t.request.name) &&
|
||||
t.status === 'executing' &&
|
||||
t.pid === activeShellPtyId &&
|
||||
!!embeddedShellFocused
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
isShellTool(t.name) &&
|
||||
t.status === CoreToolCallStatus.Executing &&
|
||||
t.ptyId === activeShellPtyId &&
|
||||
!!embeddedShellFocused
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const isShellCommand = toolsToInspect.some((t) => {
|
||||
if (isTrackedToolCall(t)) {
|
||||
return isShellTool(t.request.name);
|
||||
} else {
|
||||
return isShellTool(t.name);
|
||||
}
|
||||
});
|
||||
|
||||
// If we have an active PTY that isn't a background shell, then the current
|
||||
// pending batch is definitely a shell batch.
|
||||
const isCurrentlyInShellTurn =
|
||||
!!activeShellPtyId && !backgroundShells.has(activeShellPtyId);
|
||||
|
||||
const isShell =
|
||||
isShellCommand || (item.tools.length === 0 && isCurrentlyInShellTurn);
|
||||
const isPending =
|
||||
hasPending || (item.tools.length === 0 && isCurrentlyInShellTurn);
|
||||
|
||||
const isEffectivelyFocused =
|
||||
isEmbeddedShellFocused ||
|
||||
(item.tools.length === 0 &&
|
||||
isCurrentlyInShellTurn &&
|
||||
!!embeddedShellFocused);
|
||||
|
||||
const borderColor =
|
||||
(isShell && isPending) || isEffectivelyFocused
|
||||
? theme.ui.symbol
|
||||
: isPending
|
||||
? theme.status.warning
|
||||
: theme.border.default;
|
||||
|
||||
const borderDimColor = isPending && (!isShell || !isEffectivelyFocused);
|
||||
|
||||
return { borderColor, borderDimColor };
|
||||
}
|
||||
Reference in New Issue
Block a user