mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 14:04:41 -07:00
Fix bottom border color
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -47,6 +47,8 @@ interface HistoryItemDisplayProps {
|
||||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
availableTerminalHeightGemini?: number;
|
||||
borderColor?: string;
|
||||
borderDimColor?: boolean;
|
||||
}
|
||||
|
||||
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
@@ -58,6 +60,8 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
availableTerminalHeightGemini,
|
||||
borderColor,
|
||||
borderDimColor,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const inlineThinkingMode = getInlineThinkingMode(settings);
|
||||
@@ -181,6 +185,8 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
embeddedShellFocused={embeddedShellFocused}
|
||||
borderTop={itemForDisplay.borderTop}
|
||||
borderBottom={itemForDisplay.borderBottom}
|
||||
borderColor={borderColor ?? ''}
|
||||
borderDimColor={borderDimColor ?? false}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'compression' && (
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { MainContent } from './MainContent.js';
|
||||
import { MainContent, getToolGroupBorderAppearance } from './MainContent.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Box, Text } from 'ink';
|
||||
import { act, useState, type JSX } from 'react';
|
||||
@@ -18,6 +18,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 +77,208 @@ 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
|
||||
});
|
||||
});
|
||||
|
||||
describe('MainContent', () => {
|
||||
const defaultMockUiState = {
|
||||
history: [
|
||||
@@ -258,7 +461,7 @@ describe('MainContent', () => {
|
||||
history: [],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group' as const,
|
||||
type: 'tool_group',
|
||||
id: 1,
|
||||
tools: [
|
||||
{
|
||||
|
||||
@@ -19,10 +19,85 @@ import { useMemo, memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
|
||||
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
|
||||
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import { isShellTool } from './messages/ToolShared.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryItemWithoutId,
|
||||
HistoryItemToolGroup,
|
||||
} from '../types.js';
|
||||
import type { UIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
|
||||
const MemoizedAppHeader = memo(AppHeader);
|
||||
|
||||
/**
|
||||
* Calculates the border color and dimming state for a tool group message.
|
||||
*/
|
||||
export function getToolGroupBorderAppearance(
|
||||
item: HistoryItem | HistoryItemWithoutId,
|
||||
activeShellPtyId: number | null | undefined,
|
||||
embeddedShellFocused: boolean | undefined,
|
||||
allPendingItems: HistoryItemWithoutId[],
|
||||
backgroundShells: UIState['backgroundShells'],
|
||||
): { 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 =
|
||||
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) =>
|
||||
t.status !== CoreToolCallStatus.Success &&
|
||||
t.status !== CoreToolCallStatus.Error &&
|
||||
t.status !== CoreToolCallStatus.Cancelled,
|
||||
);
|
||||
|
||||
const isEmbeddedShellFocused = toolsToInspect.some(
|
||||
(t) =>
|
||||
isShellTool(t.name) &&
|
||||
t.status === CoreToolCallStatus.Executing &&
|
||||
t.ptyId === activeShellPtyId &&
|
||||
!!embeddedShellFocused,
|
||||
);
|
||||
|
||||
const isShellCommand = toolsToInspect.some((t) => 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 borderColor =
|
||||
(isShell && isPending) || isEmbeddedShellFocused
|
||||
? theme.ui.symbol
|
||||
: isPending
|
||||
? theme.status.warning
|
||||
: theme.border.default;
|
||||
|
||||
const borderDimColor = isPending && (!isShell || !isEmbeddedShellFocused);
|
||||
|
||||
return { borderColor, borderDimColor };
|
||||
}
|
||||
|
||||
// Limit Gemini messages to a very high number of lines to mitigate performance
|
||||
// issues in the worst case if we somehow get an enormous response from Gemini.
|
||||
// This threshold is arbitrary but should be high enough to never impact normal
|
||||
@@ -49,49 +124,77 @@ export const MainContent = () => {
|
||||
staticAreaMaxItemHeight,
|
||||
availableTerminalHeight,
|
||||
cleanUiDetailsVisible,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
backgroundShells,
|
||||
} = uiState;
|
||||
const showHeaderDetails = cleanUiDetailsVisible;
|
||||
|
||||
const historyItems = useMemo(
|
||||
() =>
|
||||
uiState.history.map((h) => (
|
||||
<MemoizedHistoryItemDisplay
|
||||
terminalWidth={mainAreaWidth}
|
||||
availableTerminalHeight={staticAreaMaxItemHeight}
|
||||
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
||||
key={h.id}
|
||||
item={h}
|
||||
isPending={false}
|
||||
commands={uiState.slashCommands}
|
||||
/>
|
||||
)),
|
||||
uiState.history.map((h) => {
|
||||
const { borderColor, borderDimColor } = getToolGroupBorderAppearance(
|
||||
h,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
[],
|
||||
backgroundShells,
|
||||
);
|
||||
return (
|
||||
<MemoizedHistoryItemDisplay
|
||||
terminalWidth={mainAreaWidth}
|
||||
availableTerminalHeight={staticAreaMaxItemHeight}
|
||||
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
||||
key={h.id}
|
||||
item={h}
|
||||
isPending={false}
|
||||
commands={uiState.slashCommands}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
[
|
||||
uiState.history,
|
||||
mainAreaWidth,
|
||||
staticAreaMaxItemHeight,
|
||||
uiState.slashCommands,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
backgroundShells,
|
||||
],
|
||||
);
|
||||
|
||||
const pendingItems = useMemo(
|
||||
() => (
|
||||
<Box flexDirection="column">
|
||||
{pendingHistoryItems.map((item, i) => (
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
(uiState.constrainHeight && !isAlternateBuffer) ||
|
||||
isAlternateBuffer
|
||||
? availableTerminalHeight
|
||||
: undefined
|
||||
}
|
||||
terminalWidth={mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
activeShellPtyId={uiState.activePtyId}
|
||||
embeddedShellFocused={uiState.embeddedShellFocused}
|
||||
/>
|
||||
))}
|
||||
{pendingHistoryItems.map((item, i) => {
|
||||
const { borderColor, borderDimColor } = getToolGroupBorderAppearance(
|
||||
item,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
pendingHistoryItems,
|
||||
backgroundShells,
|
||||
);
|
||||
return (
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
(uiState.constrainHeight && !isAlternateBuffer) ||
|
||||
isAlternateBuffer
|
||||
? availableTerminalHeight
|
||||
: undefined
|
||||
}
|
||||
terminalWidth={mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
activeShellPtyId={activePtyId}
|
||||
embeddedShellFocused={embeddedShellFocused}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{showConfirmationQueue && confirmingTool && (
|
||||
<ToolConfirmationQueue confirmingTool={confirmingTool} />
|
||||
)}
|
||||
@@ -103,8 +206,9 @@ export const MainContent = () => {
|
||||
isAlternateBuffer,
|
||||
availableTerminalHeight,
|
||||
mainAreaWidth,
|
||||
uiState.activePtyId,
|
||||
uiState.embeddedShellFocused,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
backgroundShells,
|
||||
showConfirmationQueue,
|
||||
confirmingTool,
|
||||
],
|
||||
@@ -130,6 +234,13 @@ export const MainContent = () => {
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'history') {
|
||||
const { borderColor, borderDimColor } = getToolGroupBorderAppearance(
|
||||
item.item,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
[],
|
||||
backgroundShells,
|
||||
);
|
||||
return (
|
||||
<MemoizedHistoryItemDisplay
|
||||
terminalWidth={mainAreaWidth}
|
||||
@@ -139,6 +250,8 @@ export const MainContent = () => {
|
||||
item={item.item}
|
||||
isPending={false}
|
||||
commands={uiState.slashCommands}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -151,6 +264,9 @@ export const MainContent = () => {
|
||||
mainAreaWidth,
|
||||
uiState.slashCommands,
|
||||
pendingItems,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
backgroundShells,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ describe('<ToolGroupMessage />', () => {
|
||||
const baseProps = {
|
||||
groupId: 1,
|
||||
terminalWidth: 80,
|
||||
borderColor: 'grey',
|
||||
borderDimColor: false,
|
||||
};
|
||||
|
||||
const baseMockConfig = makeFakeConfig({
|
||||
@@ -61,7 +63,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -119,7 +126,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -159,7 +171,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -197,7 +214,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -222,7 +244,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -236,7 +263,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: [] }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -267,7 +299,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -290,7 +327,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -325,8 +367,14 @@ describe('<ToolGroupMessage />', () => {
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [
|
||||
{ type: 'tool_group', tools: toolCalls1 },
|
||||
{ type: 'tool_group', tools: toolCalls2 },
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls1,
|
||||
},
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -349,7 +397,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -371,7 +424,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -405,7 +463,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,11 +13,8 @@ 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';
|
||||
|
||||
@@ -31,6 +28,8 @@ interface ToolGroupMessageProps {
|
||||
onShellInputSubmit?: (input: string) => void;
|
||||
borderTop?: boolean;
|
||||
borderBottom?: boolean;
|
||||
borderColor: string;
|
||||
borderDimColor: boolean;
|
||||
}
|
||||
|
||||
// Main component renders the border and maps the tools using ToolMessage
|
||||
@@ -44,6 +43,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
embeddedShellFocused,
|
||||
borderTop: borderTopOverride,
|
||||
borderBottom: borderBottomOverride,
|
||||
borderColor,
|
||||
borderDimColor,
|
||||
}) => {
|
||||
// Filter out tool calls that should be hidden (e.g. in-progress Ask User, or Plan Mode operations).
|
||||
const toolCalls = useMemo(
|
||||
@@ -80,31 +81,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),
|
||||
|
||||
@@ -35,6 +35,8 @@ describe('ToolResultDisplay Overflow', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<OverflowProvider>
|
||||
<ToolGroupMessage
|
||||
borderColor="grey"
|
||||
borderDimColor={false}
|
||||
groupId={1}
|
||||
toolCalls={toolCalls}
|
||||
availableTerminalHeight={15} // Small height to force overflow
|
||||
|
||||
@@ -79,6 +79,8 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
data={['item1']}
|
||||
renderItem={() => (
|
||||
<ToolGroupMessage
|
||||
borderColor="grey"
|
||||
borderDimColor={false}
|
||||
groupId={1}
|
||||
toolCalls={toolCalls}
|
||||
terminalWidth={terminalWidth - 2} // Account for ScrollableList padding
|
||||
@@ -165,6 +167,8 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
data={['item1']}
|
||||
renderItem={() => (
|
||||
<ToolGroupMessage
|
||||
borderColor="grey"
|
||||
borderDimColor={false}
|
||||
groupId={1}
|
||||
toolCalls={toolCalls}
|
||||
terminalWidth={terminalWidth - 2}
|
||||
|
||||
@@ -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,7 +23,10 @@ import {
|
||||
*/
|
||||
export function mapToDisplay(
|
||||
toolOrTools: ToolCall[] | ToolCall,
|
||||
options: { borderTop?: boolean; borderBottom?: boolean } = {},
|
||||
options: {
|
||||
borderTop?: boolean;
|
||||
borderBottom?: boolean;
|
||||
} = {},
|
||||
): HistoryItemToolGroup {
|
||||
const toolCalls = Array.isArray(toolOrTools) ? toolOrTools : [toolOrTools];
|
||||
const { borderTop, borderBottom } = options;
|
||||
|
||||
@@ -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,11 @@ import {
|
||||
type TrackedWaitingToolCall,
|
||||
type TrackedExecutingToolCall,
|
||||
} from './useToolScheduler.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import {
|
||||
isShellTool,
|
||||
isThisShellFocused,
|
||||
} from '../components/messages/ToolShared.js';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
@@ -120,6 +125,42 @@ function showCitations(settings: LoadedSettings): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getToolGroupBorderAppearance(
|
||||
toolCalls: TrackedToolCall[],
|
||||
activeShellPtyId: number | null,
|
||||
embeddedShellFocused: boolean,
|
||||
): { borderColor: string; borderDimColor: boolean } {
|
||||
const hasPending = toolCalls.some(
|
||||
(t) =>
|
||||
t.status !== 'success' &&
|
||||
t.status !== 'error' &&
|
||||
t.status !== 'cancelled',
|
||||
);
|
||||
|
||||
const isEmbeddedShellFocused = toolCalls.some((t) =>
|
||||
isThisShellFocused(
|
||||
t.request.name,
|
||||
t.status,
|
||||
t.status === 'executing' ? t.pid : undefined,
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
),
|
||||
);
|
||||
|
||||
const isShellCommand = toolCalls.some((t) => isShellTool(t.request.name));
|
||||
const borderColor =
|
||||
(isShellCommand && hasPending) || isEmbeddedShellFocused
|
||||
? theme.ui.symbol
|
||||
: hasPending
|
||||
? theme.status.warning
|
||||
: theme.border.default;
|
||||
|
||||
const borderDimColor =
|
||||
hasPending && (!isShellCommand || !isEmbeddedShellFocused);
|
||||
|
||||
return { borderColor, borderDimColor };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the current streaming state based on tool call status and responding flag.
|
||||
*/
|
||||
@@ -250,6 +291,8 @@ export const useGeminiStream = (
|
||||
mapTrackedToolCallsToDisplay(toolsToPush as TrackedToolCall[], {
|
||||
borderTop: isFirstToolInGroupRef.current,
|
||||
borderBottom: true,
|
||||
borderColor: theme.border.default,
|
||||
borderDimColor: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -290,6 +333,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 +429,11 @@ export const useGeminiStream = (
|
||||
const historyItem = mapTrackedToolCallsToDisplay(tc, {
|
||||
borderTop: isFirst,
|
||||
borderBottom: isLastInBatch,
|
||||
...getToolGroupBorderAppearance(
|
||||
toolCalls,
|
||||
activeShellPtyId,
|
||||
!!isShellFocused,
|
||||
),
|
||||
});
|
||||
addItem(historyItem);
|
||||
isFirst = false;
|
||||
@@ -362,6 +449,8 @@ export const useGeminiStream = (
|
||||
setPushedToolCallIds,
|
||||
setIsFirstToolInGroup,
|
||||
addItem,
|
||||
activeShellPtyId,
|
||||
isShellFocused,
|
||||
]);
|
||||
|
||||
const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => {
|
||||
@@ -371,11 +460,18 @@ export const useGeminiStream = (
|
||||
|
||||
const items: HistoryItemWithoutId[] = [];
|
||||
|
||||
const appearance = getToolGroupBorderAppearance(
|
||||
toolCalls,
|
||||
activeShellPtyId,
|
||||
!!isShellFocused,
|
||||
);
|
||||
|
||||
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 +519,12 @@ 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]);
|
||||
|
||||
const lastQueryRef = useRef<PartListUnion | null>(null);
|
||||
const lastPromptIdRef = useRef<string | null>(null);
|
||||
@@ -448,36 +536,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);
|
||||
|
||||
Reference in New Issue
Block a user