Fix bottom border color

This commit is contained in:
jacob314
2026-02-17 08:34:59 -08:00
parent 366f1df120
commit 25baa158aa
12 changed files with 565 additions and 119 deletions
+12 -2
View File
@@ -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: [
{
+145 -29
View File
@@ -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),
);
});
+4 -1
View File
@@ -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 ---
+98 -40
View File
@@ -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);