mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 09:01:17 -07:00
feat(cli): truncate shell output in UI history and improve active shell display (#17438)
This commit is contained in:
@@ -68,8 +68,9 @@ describe('<AnsiOutputText />', () => {
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
const lines = output!.split('\n');
|
||||
expect(lines[0]).toBe('First line');
|
||||
expect(lines[1]).toBe('Third line');
|
||||
expect(lines[0].trim()).toBe('First line');
|
||||
expect(lines[1].trim()).toBe('');
|
||||
expect(lines[2].trim()).toBe('Third line');
|
||||
});
|
||||
|
||||
it('respects the availableTerminalHeight prop and slices the lines correctly', () => {
|
||||
@@ -89,6 +90,45 @@ describe('<AnsiOutputText />', () => {
|
||||
expect(output).toContain('Line 4');
|
||||
});
|
||||
|
||||
it('respects the maxLines prop and slices the lines correctly', () => {
|
||||
const data: AnsiOutput = [
|
||||
[createAnsiToken({ text: 'Line 1' })],
|
||||
[createAnsiToken({ text: 'Line 2' })],
|
||||
[createAnsiToken({ text: 'Line 3' })],
|
||||
[createAnsiToken({ text: 'Line 4' })],
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
<AnsiOutputText data={data} maxLines={2} width={80} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Line 1');
|
||||
expect(output).not.toContain('Line 2');
|
||||
expect(output).toContain('Line 3');
|
||||
expect(output).toContain('Line 4');
|
||||
});
|
||||
|
||||
it('prioritizes maxLines over availableTerminalHeight if maxLines is smaller', () => {
|
||||
const data: AnsiOutput = [
|
||||
[createAnsiToken({ text: 'Line 1' })],
|
||||
[createAnsiToken({ text: 'Line 2' })],
|
||||
[createAnsiToken({ text: 'Line 3' })],
|
||||
[createAnsiToken({ text: 'Line 4' })],
|
||||
];
|
||||
// availableTerminalHeight=3, maxLines=2 => show 2 lines
|
||||
const { lastFrame } = render(
|
||||
<AnsiOutputText
|
||||
data={data}
|
||||
availableTerminalHeight={3}
|
||||
maxLines={2}
|
||||
width={80}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Line 2');
|
||||
expect(output).toContain('Line 3');
|
||||
expect(output).toContain('Line 4');
|
||||
});
|
||||
|
||||
it('renders a large AnsiOutput object without crashing', () => {
|
||||
const largeData: AnsiOutput = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
|
||||
@@ -14,40 +14,56 @@ interface AnsiOutputProps {
|
||||
data: AnsiOutput;
|
||||
availableTerminalHeight?: number;
|
||||
width: number;
|
||||
maxLines?: number;
|
||||
disableTruncation?: boolean;
|
||||
}
|
||||
|
||||
export const AnsiOutputText: React.FC<AnsiOutputProps> = ({
|
||||
data,
|
||||
availableTerminalHeight,
|
||||
width,
|
||||
maxLines,
|
||||
disableTruncation,
|
||||
}) => {
|
||||
const lastLines = data.slice(
|
||||
-(availableTerminalHeight && availableTerminalHeight > 0
|
||||
const availableHeightLimit =
|
||||
availableTerminalHeight && availableTerminalHeight > 0
|
||||
? availableTerminalHeight
|
||||
: DEFAULT_HEIGHT),
|
||||
);
|
||||
: undefined;
|
||||
|
||||
const numLinesRetained =
|
||||
availableHeightLimit !== undefined && maxLines !== undefined
|
||||
? Math.min(availableHeightLimit, maxLines)
|
||||
: (availableHeightLimit ?? maxLines ?? DEFAULT_HEIGHT);
|
||||
|
||||
const lastLines = disableTruncation ? data : data.slice(-numLinesRetained);
|
||||
return (
|
||||
<Box flexDirection="column" width={width} flexShrink={0}>
|
||||
<Box flexDirection="column" width={width} flexShrink={0} overflow="hidden">
|
||||
{lastLines.map((line: AnsiLine, lineIndex: number) => (
|
||||
<Text key={lineIndex} wrap="truncate">
|
||||
{line.length > 0
|
||||
? line.map((token: AnsiToken, tokenIndex: number) => (
|
||||
<Text
|
||||
key={tokenIndex}
|
||||
color={token.fg}
|
||||
backgroundColor={token.bg}
|
||||
inverse={token.inverse}
|
||||
dimColor={token.dim}
|
||||
bold={token.bold}
|
||||
italic={token.italic}
|
||||
underline={token.underline}
|
||||
>
|
||||
{token.text}
|
||||
</Text>
|
||||
))
|
||||
: null}
|
||||
</Text>
|
||||
<Box key={lineIndex} height={1} overflow="hidden">
|
||||
<AnsiLineText line={line} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const AnsiLineText: React.FC<{ line: AnsiLine }> = ({ line }) => (
|
||||
<Text>
|
||||
{line.length > 0
|
||||
? line.map((token: AnsiToken, tokenIndex: number) => (
|
||||
<Text
|
||||
key={tokenIndex}
|
||||
color={token.fg}
|
||||
backgroundColor={token.bg}
|
||||
inverse={token.inverse}
|
||||
dimColor={token.dim}
|
||||
bold={token.bold}
|
||||
italic={token.italic}
|
||||
underline={token.underline}
|
||||
>
|
||||
{token.text}
|
||||
</Text>
|
||||
))
|
||||
: null}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,10 @@ import { MainContent } from './MainContent.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { ToolCallStatus } from '../types.js';
|
||||
import { SHELL_COMMAND_NAME } from '../constants.js';
|
||||
import type { UIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../contexts/AppContext.js', async () => {
|
||||
@@ -22,53 +26,10 @@ vi.mock('../contexts/AppContext.js', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../contexts/UIStateContext.js', async () => {
|
||||
const actual = await vi.importActual('../contexts/UIStateContext.js');
|
||||
return {
|
||||
...actual,
|
||||
useUIState: () => ({
|
||||
history: [
|
||||
{ id: 1, role: 'user', content: 'Hello' },
|
||||
{ id: 2, role: 'model', content: 'Hi there' },
|
||||
],
|
||||
pendingHistoryItems: [],
|
||||
mainAreaWidth: 80,
|
||||
staticAreaMaxItemHeight: 20,
|
||||
availableTerminalHeight: 24,
|
||||
slashCommands: [],
|
||||
constrainHeight: false,
|
||||
isEditorDialogOpen: false,
|
||||
activePtyId: undefined,
|
||||
embeddedShellFocused: false,
|
||||
historyRemountKey: 0,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../hooks/useAlternateBuffer.js', () => ({
|
||||
useAlternateBuffer: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./HistoryItemDisplay.js', () => ({
|
||||
HistoryItemDisplay: ({
|
||||
item,
|
||||
availableTerminalHeight,
|
||||
}: {
|
||||
item: { content: string };
|
||||
availableTerminalHeight?: number;
|
||||
}) => (
|
||||
<Box>
|
||||
<Text>
|
||||
HistoryItem: {item.content} (height:{' '}
|
||||
{availableTerminalHeight === undefined
|
||||
? 'undefined'
|
||||
: availableTerminalHeight}
|
||||
)
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./AppHeader.js', () => ({
|
||||
AppHeader: () => <Text>AppHeader</Text>,
|
||||
}));
|
||||
@@ -95,39 +56,169 @@ vi.mock('./shared/ScrollableList.js', () => ({
|
||||
SCROLL_TO_ITEM_END: 0,
|
||||
}));
|
||||
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
|
||||
describe('MainContent', () => {
|
||||
const defaultMockUiState = {
|
||||
history: [
|
||||
{ id: 1, type: 'user', text: 'Hello' },
|
||||
{ id: 2, type: 'gemini', text: 'Hi there' },
|
||||
],
|
||||
pendingHistoryItems: [],
|
||||
mainAreaWidth: 80,
|
||||
staticAreaMaxItemHeight: 20,
|
||||
availableTerminalHeight: 24,
|
||||
slashCommands: [],
|
||||
constrainHeight: false,
|
||||
isEditorDialogOpen: false,
|
||||
activePtyId: undefined,
|
||||
embeddedShellFocused: false,
|
||||
historyRemountKey: 0,
|
||||
bannerData: { defaultText: '', warningText: '' },
|
||||
bannerVisible: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('renders in normal buffer mode', async () => {
|
||||
const { lastFrame } = renderWithProviders(<MainContent />);
|
||||
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||
uiState: defaultMockUiState as Partial<UIState>,
|
||||
});
|
||||
await waitFor(() => expect(lastFrame()).toContain('AppHeader'));
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('HistoryItem: Hello (height: 20)');
|
||||
expect(output).toContain('HistoryItem: Hi there (height: 20)');
|
||||
expect(output).toContain('Hello');
|
||||
expect(output).toContain('Hi there');
|
||||
});
|
||||
|
||||
it('renders in alternate buffer mode', async () => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||
const { lastFrame } = renderWithProviders(<MainContent />);
|
||||
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||
uiState: defaultMockUiState as Partial<UIState>,
|
||||
});
|
||||
await waitFor(() => expect(lastFrame()).toContain('ScrollableList'));
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('AppHeader');
|
||||
expect(output).toContain('HistoryItem: Hello (height: undefined)');
|
||||
expect(output).toContain('HistoryItem: Hi there (height: undefined)');
|
||||
expect(output).toContain('Hello');
|
||||
expect(output).toContain('Hi there');
|
||||
});
|
||||
|
||||
it('does not constrain height in alternate buffer mode', async () => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||
const { lastFrame } = renderWithProviders(<MainContent />);
|
||||
await waitFor(() => expect(lastFrame()).toContain('HistoryItem: Hello'));
|
||||
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||
uiState: defaultMockUiState as Partial<UIState>,
|
||||
});
|
||||
await waitFor(() => expect(lastFrame()).toContain('Hello'));
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('MainContent Tool Output Height Logic', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'ASB mode - Focused shell should expand',
|
||||
isAlternateBuffer: true,
|
||||
embeddedShellFocused: true,
|
||||
constrainHeight: true,
|
||||
shouldShowLine1: true,
|
||||
},
|
||||
{
|
||||
name: 'ASB mode - Unfocused shell',
|
||||
isAlternateBuffer: true,
|
||||
embeddedShellFocused: false,
|
||||
constrainHeight: true,
|
||||
shouldShowLine1: false,
|
||||
},
|
||||
{
|
||||
name: 'Normal mode - Constrained height',
|
||||
isAlternateBuffer: false,
|
||||
embeddedShellFocused: false,
|
||||
constrainHeight: true,
|
||||
shouldShowLine1: false,
|
||||
},
|
||||
{
|
||||
name: 'Normal mode - Unconstrained height',
|
||||
isAlternateBuffer: false,
|
||||
embeddedShellFocused: false,
|
||||
constrainHeight: false,
|
||||
shouldShowLine1: false,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
'$name',
|
||||
async ({
|
||||
isAlternateBuffer,
|
||||
embeddedShellFocused,
|
||||
constrainHeight,
|
||||
shouldShowLine1,
|
||||
}) => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(isAlternateBuffer);
|
||||
const ptyId = 123;
|
||||
const uiState = {
|
||||
history: [],
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
type: 'tool_group' as const,
|
||||
id: 1,
|
||||
tools: [
|
||||
{
|
||||
callId: 'call_1',
|
||||
name: SHELL_COMMAND_NAME,
|
||||
status: ToolCallStatus.Executing,
|
||||
description: 'Running a long command...',
|
||||
// 20 lines of output.
|
||||
// Default max is 15, so Line 1-5 will be truncated/scrolled out if not expanded.
|
||||
resultDisplay: Array.from(
|
||||
{ length: 20 },
|
||||
(_, i) => `Line ${i + 1}`,
|
||||
).join('\n'),
|
||||
ptyId,
|
||||
confirmationDetails: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
availableTerminalHeight: 30, // In ASB mode, focused shell should get ~28 lines
|
||||
terminalHeight: 50,
|
||||
terminalWidth: 100,
|
||||
mainAreaWidth: 100,
|
||||
embeddedShellFocused,
|
||||
activePtyId: embeddedShellFocused ? ptyId : undefined,
|
||||
constrainHeight,
|
||||
isEditorDialogOpen: false,
|
||||
slashCommands: [],
|
||||
historyRemountKey: 0,
|
||||
bannerData: {
|
||||
defaultText: '',
|
||||
warningText: '',
|
||||
},
|
||||
bannerVisible: false,
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||
uiState: uiState as Partial<UIState>,
|
||||
useAlternateBuffer: isAlternateBuffer,
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
// Sanity checks - Use regex with word boundary to avoid matching "Line 10" etc.
|
||||
const line1Regex = /\bLine 1\b/;
|
||||
if (shouldShowLine1) {
|
||||
expect(output).toMatch(line1Regex);
|
||||
} else {
|
||||
expect(output).not.toMatch(line1Regex);
|
||||
}
|
||||
|
||||
// All cases should show the last line
|
||||
expect(output).toContain('Line 20');
|
||||
|
||||
// Snapshots for visual verification
|
||||
expect(output).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,7 +81,8 @@ export const MainContent = () => {
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
uiState.constrainHeight && !isAlternateBuffer
|
||||
(uiState.constrainHeight && !isAlternateBuffer) ||
|
||||
isAlternateBuffer
|
||||
? availableTerminalHeight
|
||||
: undefined
|
||||
}
|
||||
@@ -148,7 +149,7 @@ export const MainContent = () => {
|
||||
return (
|
||||
<ScrollableList
|
||||
ref={scrollableListRef}
|
||||
hasFocus={!uiState.isEditorDialogOpen}
|
||||
hasFocus={!uiState.isEditorDialogOpen && !uiState.embeddedShellFocused}
|
||||
width={uiState.terminalWidth}
|
||||
data={virtualizedData}
|
||||
renderItem={renderItem}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { render, persistentStateMock } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { Notifications } from './Notifications.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useAppContext, type AppState } from '../contexts/AppContext.js';
|
||||
@@ -172,7 +173,7 @@ describe('Notifications', () => {
|
||||
render(<Notifications />);
|
||||
|
||||
await act(async () => {
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(persistentStateMock.set).toHaveBeenCalledWith(
|
||||
'hasSeenScreenReaderNudge',
|
||||
true,
|
||||
|
||||
@@ -95,16 +95,64 @@ describe('ShellInputPrompt', () => {
|
||||
it.each([
|
||||
['up', -1],
|
||||
['down', 1],
|
||||
])('handles scroll %s (Ctrl+Shift+%s)', (key, direction) => {
|
||||
])('handles scroll %s (Command.SCROLL_%s)', (key, direction) => {
|
||||
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
|
||||
|
||||
const handler = mockUseKeypress.mock.calls[0][0];
|
||||
|
||||
handler({ name: key, shift: true, alt: false, ctrl: true, cmd: false });
|
||||
handler({ name: key, shift: true, alt: false, ctrl: false, cmd: false });
|
||||
|
||||
expect(mockScrollPty).toHaveBeenCalledWith(1, direction);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['pageup', -15],
|
||||
['pagedown', 15],
|
||||
])(
|
||||
'handles page scroll %s (Command.PAGE_%s) with default size',
|
||||
(key, expectedScroll) => {
|
||||
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
|
||||
|
||||
const handler = mockUseKeypress.mock.calls[0][0];
|
||||
|
||||
handler({ name: key, shift: false, alt: false, ctrl: false, cmd: false });
|
||||
|
||||
expect(mockScrollPty).toHaveBeenCalledWith(1, expectedScroll);
|
||||
},
|
||||
);
|
||||
|
||||
it('respects scrollPageSize prop', () => {
|
||||
render(
|
||||
<ShellInputPrompt
|
||||
activeShellPtyId={1}
|
||||
focus={true}
|
||||
scrollPageSize={10}
|
||||
/>,
|
||||
);
|
||||
|
||||
const handler = mockUseKeypress.mock.calls[0][0];
|
||||
|
||||
// PageDown
|
||||
handler({
|
||||
name: 'pagedown',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
});
|
||||
expect(mockScrollPty).toHaveBeenCalledWith(1, 10);
|
||||
|
||||
// PageUp
|
||||
handler({
|
||||
name: 'pageup',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
});
|
||||
expect(mockScrollPty).toHaveBeenCalledWith(1, -10);
|
||||
});
|
||||
|
||||
it('does not handle input when not focused', () => {
|
||||
render(<ShellInputPrompt activeShellPtyId={1} focus={false} />);
|
||||
|
||||
@@ -138,4 +186,21 @@ describe('ShellInputPrompt', () => {
|
||||
|
||||
expect(mockWriteToPty).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores Command.UNFOCUS_SHELL (Shift+Tab) to allow focus navigation', () => {
|
||||
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
|
||||
|
||||
const handler = mockUseKeypress.mock.calls[0][0];
|
||||
|
||||
const result = handler({
|
||||
name: 'tab',
|
||||
shift: true,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockWriteToPty).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,16 +9,19 @@ import type React from 'react';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { ShellExecutionService } from '@google/gemini-cli-core';
|
||||
import { keyToAnsi, type Key } from '../hooks/keyToAnsi.js';
|
||||
import { ACTIVE_SHELL_MAX_LINES } from '../constants.js';
|
||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
||||
|
||||
export interface ShellInputPromptProps {
|
||||
activeShellPtyId: number | null;
|
||||
focus?: boolean;
|
||||
scrollPageSize?: number;
|
||||
}
|
||||
|
||||
export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
||||
activeShellPtyId,
|
||||
focus = true,
|
||||
scrollPageSize = ACTIVE_SHELL_MAX_LINES,
|
||||
}) => {
|
||||
const handleShellInputSubmit = useCallback(
|
||||
(input: string) => {
|
||||
@@ -34,26 +37,33 @@ export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
||||
if (!focus || !activeShellPtyId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow background shell toggle to bubble up
|
||||
if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow unfocus to bubble up
|
||||
// Allow Shift+Tab to bubble up for focus navigation
|
||||
if (keyMatchers[Command.UNFOCUS_SHELL_INPUT](key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key.ctrl && key.shift && key.name === 'up') {
|
||||
if (keyMatchers[Command.SCROLL_UP](key)) {
|
||||
ShellExecutionService.scrollPty(activeShellPtyId, -1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key.ctrl && key.shift && key.name === 'down') {
|
||||
if (keyMatchers[Command.SCROLL_DOWN](key)) {
|
||||
ShellExecutionService.scrollPty(activeShellPtyId, 1);
|
||||
return true;
|
||||
}
|
||||
// TODO: Check pty service actually scrolls (request)[https://github.com/google-gemini/gemini-cli/pull/17438/changes/c9fdaf8967da0036bfef43592fcab5a69537df35#r2776479023].
|
||||
if (keyMatchers[Command.PAGE_UP](key)) {
|
||||
ShellExecutionService.scrollPty(activeShellPtyId, -scrollPageSize);
|
||||
return true;
|
||||
}
|
||||
if (keyMatchers[Command.PAGE_DOWN](key)) {
|
||||
ShellExecutionService.scrollPty(activeShellPtyId, scrollPageSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
const ansiSequence = keyToAnsi(key);
|
||||
if (ansiSequence) {
|
||||
@@ -63,7 +73,7 @@ export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
||||
|
||||
return false;
|
||||
},
|
||||
[focus, handleShellInputSubmit, activeShellPtyId],
|
||||
[focus, handleShellInputSubmit, activeShellPtyId, scrollPageSize],
|
||||
);
|
||||
|
||||
useKeypress(handleInput, { isActive: focus });
|
||||
|
||||
@@ -39,14 +39,14 @@ Tips for getting started:
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information.
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool1 Description for tool 1 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool2 Description for tool 2 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool1 Description for tool 1 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool2 Description for tool 2 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = `
|
||||
@@ -83,14 +83,14 @@ Tips for getting started:
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information.
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool1 Description for tool 1 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool2 Description for tool 2 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool1 Description for tool 1 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool2 Description for tool 2 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = `
|
||||
|
||||
@@ -1,8 +1,116 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Focused shell should expand' 1`] = `
|
||||
"ScrollableList
|
||||
AppHeader
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command Running a long command... │
|
||||
│ │
|
||||
│ Line 1 │
|
||||
│ Line 2 │
|
||||
│ Line 3 │
|
||||
│ Line 4 │
|
||||
│ Line 5 │
|
||||
│ Line 6 │
|
||||
│ Line 7 │
|
||||
│ Line 8 │
|
||||
│ Line 9 │
|
||||
│ Line 10 │
|
||||
│ Line 11 │
|
||||
│ Line 12 │
|
||||
│ Line 13 │
|
||||
│ Line 14 │
|
||||
│ Line 15 │
|
||||
│ Line 16 │
|
||||
│ Line 17 │
|
||||
│ Line 18 │
|
||||
│ Line 19 │
|
||||
│ Line 20 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
ShowMoreLines"
|
||||
`;
|
||||
|
||||
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocused shell' 1`] = `
|
||||
"ScrollableList
|
||||
AppHeader
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command Running a long command... │
|
||||
│ │
|
||||
│ Line 6 │
|
||||
│ Line 7 │
|
||||
│ Line 8 │
|
||||
│ Line 9 ▄ │
|
||||
│ Line 10 █ │
|
||||
│ Line 11 █ │
|
||||
│ Line 12 █ │
|
||||
│ Line 13 █ │
|
||||
│ Line 14 █ │
|
||||
│ Line 15 █ │
|
||||
│ Line 16 █ │
|
||||
│ Line 17 █ │
|
||||
│ Line 18 █ │
|
||||
│ Line 19 █ │
|
||||
│ Line 20 █ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
ShowMoreLines"
|
||||
`;
|
||||
|
||||
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = `
|
||||
"AppHeader
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command Running a long command... │
|
||||
│ │
|
||||
│ Line 6 │
|
||||
│ Line 7 │
|
||||
│ Line 8 │
|
||||
│ Line 9 │
|
||||
│ Line 10 │
|
||||
│ Line 11 │
|
||||
│ Line 12 │
|
||||
│ Line 13 │
|
||||
│ Line 14 │
|
||||
│ Line 15 │
|
||||
│ Line 16 │
|
||||
│ Line 17 │
|
||||
│ Line 18 │
|
||||
│ Line 19 │
|
||||
│ Line 20 │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
ShowMoreLines"
|
||||
`;
|
||||
|
||||
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = `
|
||||
"AppHeader
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command Running a long command... │
|
||||
│ │
|
||||
│ Line 6 │
|
||||
│ Line 7 │
|
||||
│ Line 8 │
|
||||
│ Line 9 │
|
||||
│ Line 10 │
|
||||
│ Line 11 │
|
||||
│ Line 12 │
|
||||
│ Line 13 │
|
||||
│ Line 14 │
|
||||
│ Line 15 │
|
||||
│ Line 16 │
|
||||
│ Line 17 │
|
||||
│ Line 18 │
|
||||
│ Line 19 │
|
||||
│ Line 20 │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
ShowMoreLines"
|
||||
`;
|
||||
|
||||
exports[`MainContent > does not constrain height in alternate buffer mode 1`] = `
|
||||
"ScrollableList
|
||||
AppHeader
|
||||
HistoryItem: Hello (height: undefined)
|
||||
HistoryItem: Hi there (height: undefined)"
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Hello
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
✦ Hi there
|
||||
ShowMoreLines
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -4,55 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { act } from 'react';
|
||||
import {
|
||||
ShellToolMessage,
|
||||
type ShellToolMessageProps,
|
||||
} from './ShellToolMessage.js';
|
||||
import { StreamingState, ToolCallStatus } from '../../types.js';
|
||||
import { Text } from 'ink';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { waitFor } from '../../../test-utils/async.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SHELL_TOOL_NAME } from '@google/gemini-cli-core';
|
||||
import { SHELL_COMMAND_NAME } from '../../constants.js';
|
||||
import { StreamingContext } from '../../contexts/StreamingContext.js';
|
||||
|
||||
vi.mock('../TerminalOutput.js', () => ({
|
||||
TerminalOutput: function MockTerminalOutput({
|
||||
cursor,
|
||||
}: {
|
||||
cursor: { x: number; y: number } | null;
|
||||
}) {
|
||||
return (
|
||||
<Text>
|
||||
MockCursor:({cursor?.x},{cursor?.y})
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock child components or utilities if they are complex or have side effects
|
||||
vi.mock('../GeminiRespondingSpinner.js', () => ({
|
||||
GeminiRespondingSpinner: ({
|
||||
nonRespondingDisplay,
|
||||
}: {
|
||||
nonRespondingDisplay?: string;
|
||||
}) => {
|
||||
const streamingState = React.useContext(StreamingContext)!;
|
||||
if (streamingState === StreamingState.Responding) {
|
||||
return <Text>MockRespondingSpinner</Text>;
|
||||
}
|
||||
return nonRespondingDisplay ? <Text>{nonRespondingDisplay}</Text> : null;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/MarkdownDisplay.js', () => ({
|
||||
MarkdownDisplay: function MockMarkdownDisplay({ text }: { text: string }) {
|
||||
return <Text>MockMarkdown:{text}</Text>;
|
||||
},
|
||||
}));
|
||||
import { SHELL_COMMAND_NAME, ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
|
||||
|
||||
describe('<ShellToolMessage />', () => {
|
||||
const baseProps: ShellToolMessageProps = {
|
||||
@@ -72,52 +35,36 @@ describe('<ShellToolMessage />', () => {
|
||||
} as unknown as Config,
|
||||
};
|
||||
|
||||
const LONG_OUTPUT = Array.from(
|
||||
{ length: 100 },
|
||||
(_, i) => `Line ${i + 1}`,
|
||||
).join('\n');
|
||||
|
||||
const mockSetEmbeddedShellFocused = vi.fn();
|
||||
const uiActions = {
|
||||
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
|
||||
};
|
||||
|
||||
const renderShell = (
|
||||
props: Partial<ShellToolMessageProps> = {},
|
||||
options: Parameters<typeof renderWithProviders>[1] = {},
|
||||
) =>
|
||||
renderWithProviders(<ShellToolMessage {...baseProps} {...props} />, {
|
||||
uiActions,
|
||||
...options,
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('interactive shell focus', () => {
|
||||
const shellProps: ShellToolMessageProps = {
|
||||
...baseProps,
|
||||
};
|
||||
|
||||
it('clicks inside the shell area sets focus to true', async () => {
|
||||
const { stdin, lastFrame, simulateClick } = renderWithProviders(
|
||||
<ShellToolMessage {...shellProps} />,
|
||||
{
|
||||
mouseEventsEnabled: true,
|
||||
uiActions,
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('A shell command'); // Wait for render
|
||||
});
|
||||
|
||||
await simulateClick(stdin, 2, 2); // Click at column 2, row 2 (1-based)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles focus for SHELL_TOOL_NAME (core shell tool)', async () => {
|
||||
const coreShellProps: ShellToolMessageProps = {
|
||||
...shellProps,
|
||||
name: SHELL_TOOL_NAME,
|
||||
};
|
||||
|
||||
const { stdin, lastFrame, simulateClick } = renderWithProviders(
|
||||
<ShellToolMessage {...coreShellProps} />,
|
||||
{
|
||||
mouseEventsEnabled: true,
|
||||
uiActions,
|
||||
},
|
||||
it.each([
|
||||
['SHELL_COMMAND_NAME', SHELL_COMMAND_NAME],
|
||||
['SHELL_TOOL_NAME', SHELL_TOOL_NAME],
|
||||
])('clicks inside the shell area sets focus for %s', async (_, name) => {
|
||||
const { stdin, lastFrame, simulateClick } = renderShell(
|
||||
{ name },
|
||||
{ mouseEventsEnabled: true },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -130,5 +77,136 @@ describe('<ShellToolMessage />', () => {
|
||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
it('resets focus when shell finishes', async () => {
|
||||
let updateStatus: (s: ToolCallStatus) => void = () => {};
|
||||
|
||||
const Wrapper = () => {
|
||||
const [status, setStatus] = React.useState(ToolCallStatus.Executing);
|
||||
updateStatus = setStatus;
|
||||
return (
|
||||
<ShellToolMessage
|
||||
{...baseProps}
|
||||
status={status}
|
||||
embeddedShellFocused={true}
|
||||
activeShellPtyId={1}
|
||||
ptyId={1}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(<Wrapper />, {
|
||||
uiActions,
|
||||
uiState: { streamingState: StreamingState.Idle },
|
||||
});
|
||||
|
||||
// Verify it is initially focused
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('(Shift+Tab to unfocus)');
|
||||
});
|
||||
|
||||
// Now update status to Success
|
||||
await act(async () => {
|
||||
updateStatus(ToolCallStatus.Success);
|
||||
});
|
||||
|
||||
// Should call setEmbeddedShellFocused(false) because isThisShellFocused became false
|
||||
await waitFor(() => {
|
||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(false);
|
||||
expect(lastFrame()).not.toContain('(Shift+Tab to unfocus)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshots', () => {
|
||||
it.each([
|
||||
[
|
||||
'renders in Executing state',
|
||||
{ status: ToolCallStatus.Executing },
|
||||
undefined,
|
||||
],
|
||||
[
|
||||
'renders in Success state (history mode)',
|
||||
{ status: ToolCallStatus.Success },
|
||||
undefined,
|
||||
],
|
||||
[
|
||||
'renders in Error state',
|
||||
{ status: ToolCallStatus.Error, resultDisplay: 'Error output' },
|
||||
undefined,
|
||||
],
|
||||
[
|
||||
'renders in Alternate Buffer mode while focused',
|
||||
{
|
||||
status: ToolCallStatus.Executing,
|
||||
embeddedShellFocused: true,
|
||||
activeShellPtyId: 1,
|
||||
ptyId: 1,
|
||||
},
|
||||
{ useAlternateBuffer: true },
|
||||
],
|
||||
[
|
||||
'renders in Alternate Buffer mode while unfocused',
|
||||
{
|
||||
status: ToolCallStatus.Executing,
|
||||
embeddedShellFocused: false,
|
||||
activeShellPtyId: 1,
|
||||
ptyId: 1,
|
||||
},
|
||||
{ useAlternateBuffer: true },
|
||||
],
|
||||
])('%s', async (_, props, options) => {
|
||||
const { lastFrame } = renderShell(props, options);
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Height Constraints', () => {
|
||||
it.each([
|
||||
[
|
||||
'respects availableTerminalHeight when it is smaller than ACTIVE_SHELL_MAX_LINES',
|
||||
10,
|
||||
8,
|
||||
false,
|
||||
],
|
||||
[
|
||||
'uses ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is large',
|
||||
100,
|
||||
ACTIVE_SHELL_MAX_LINES,
|
||||
false,
|
||||
],
|
||||
[
|
||||
'uses full availableTerminalHeight when focused in alternate buffer mode',
|
||||
100,
|
||||
98, // 100 - 2
|
||||
true,
|
||||
],
|
||||
[
|
||||
'defaults to ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is undefined',
|
||||
undefined,
|
||||
ACTIVE_SHELL_MAX_LINES,
|
||||
false,
|
||||
],
|
||||
])('%s', async (_, availableTerminalHeight, expectedMaxLines, focused) => {
|
||||
const { lastFrame } = renderShell(
|
||||
{
|
||||
resultDisplay: LONG_OUTPUT,
|
||||
renderOutputAsMarkdown: false,
|
||||
availableTerminalHeight,
|
||||
activeShellPtyId: 1,
|
||||
ptyId: focused ? 1 : 2,
|
||||
status: ToolCallStatus.Executing,
|
||||
embeddedShellFocused: focused,
|
||||
},
|
||||
{ useAlternateBuffer: true },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = lastFrame();
|
||||
expect(frame!.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
|
||||
expect(frame).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,12 @@ import {
|
||||
FocusHint,
|
||||
} from './ToolShared.js';
|
||||
import type { ToolMessageProps } from './ToolMessage.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import {
|
||||
ACTIVE_SHELL_MAX_LINES,
|
||||
COMPLETED_SHELL_MAX_LINES,
|
||||
} from '../../constants.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
export interface ShellToolMessageProps extends ToolMessageProps {
|
||||
@@ -61,6 +67,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
|
||||
borderDimColor,
|
||||
}) => {
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const isThisShellFocused = checkIsShellFocused(
|
||||
name,
|
||||
status,
|
||||
@@ -70,6 +77,18 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
);
|
||||
|
||||
const { setEmbeddedShellFocused } = useUIActions();
|
||||
const wasFocusedRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isThisShellFocused) {
|
||||
wasFocusedRef.current = true;
|
||||
} else if (wasFocusedRef.current) {
|
||||
if (embeddedShellFocused) {
|
||||
setEmbeddedShellFocused(false);
|
||||
}
|
||||
wasFocusedRef.current = false;
|
||||
}
|
||||
}, [isThisShellFocused, embeddedShellFocused, setEmbeddedShellFocused]);
|
||||
|
||||
const headerRef = React.useRef<DOMElement>(null);
|
||||
|
||||
@@ -139,12 +158,20 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
||||
hasFocus={isThisShellFocused}
|
||||
maxLines={getShellMaxLines(
|
||||
status,
|
||||
isAlternateBuffer,
|
||||
isThisShellFocused,
|
||||
availableTerminalHeight,
|
||||
)}
|
||||
/>
|
||||
{isThisShellFocused && config && (
|
||||
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
|
||||
<ShellInputPrompt
|
||||
activeShellPtyId={activeShellPtyId ?? null}
|
||||
focus={embeddedShellFocused}
|
||||
scrollPageSize={availableTerminalHeight ?? ACTIVE_SHELL_MAX_LINES}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -152,3 +179,39 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the maximum number of lines to display for shell output.
|
||||
*
|
||||
* For completed processes (Success, Error, Canceled), it returns COMPLETED_SHELL_MAX_LINES.
|
||||
* For active processes, it returns the available terminal height if in alternate buffer mode
|
||||
* and focused. Otherwise, it returns ACTIVE_SHELL_MAX_LINES.
|
||||
*
|
||||
* This function ensures a finite number of lines is always returned to prevent performance issues.
|
||||
*/
|
||||
function getShellMaxLines(
|
||||
status: ToolCallStatus,
|
||||
isAlternateBuffer: boolean,
|
||||
isThisShellFocused: boolean,
|
||||
availableTerminalHeight: number | undefined,
|
||||
): number {
|
||||
if (
|
||||
status === ToolCallStatus.Success ||
|
||||
status === ToolCallStatus.Error ||
|
||||
status === ToolCallStatus.Canceled
|
||||
) {
|
||||
return COMPLETED_SHELL_MAX_LINES;
|
||||
}
|
||||
|
||||
if (availableTerminalHeight === undefined) {
|
||||
return ACTIVE_SHELL_MAX_LINES;
|
||||
}
|
||||
|
||||
const maxLinesBasedOnHeight = Math.max(1, availableTerminalHeight - 2);
|
||||
|
||||
if (isAlternateBuffer && isThisShellFocused) {
|
||||
return maxLinesBasedOnHeight;
|
||||
}
|
||||
|
||||
return Math.min(maxLinesBasedOnHeight, ACTIVE_SHELL_MAX_LINES);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ const isAskUserInProgress = (t: IndividualToolCallDisplay): boolean =>
|
||||
].includes(t.status);
|
||||
|
||||
// Main component renders the border and maps the tools using ToolMessage
|
||||
const TOOL_MESSAGE_HORIZONTAL_MARGIN = 4;
|
||||
const TOOL_CONFIRMATION_INTERNAL_PADDING = 4;
|
||||
|
||||
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
toolCalls: allToolCalls,
|
||||
availableTerminalHeight,
|
||||
@@ -142,6 +145,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
|
||||
|
||||
return (
|
||||
// This box doesn't have a border even though it conceptually does because
|
||||
// we need to allow the sticky headers to render the borders themselves so
|
||||
@@ -155,6 +160,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
cause tearing.
|
||||
*/
|
||||
width={terminalWidth}
|
||||
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
|
||||
>
|
||||
{visibleToolCalls.map((tool, index) => {
|
||||
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
||||
@@ -164,7 +170,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
const commonProps = {
|
||||
...tool,
|
||||
availableTerminalHeight: availableTerminalHeightPerToolMessage,
|
||||
terminalWidth,
|
||||
terminalWidth: contentWidth,
|
||||
emphasis: isConfirming
|
||||
? ('high' as const)
|
||||
: toolAwaitingApproval
|
||||
@@ -183,7 +189,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
key={tool.callId}
|
||||
flexDirection="column"
|
||||
minHeight={1}
|
||||
width={terminalWidth}
|
||||
width={contentWidth}
|
||||
>
|
||||
{isShellToolCall ? (
|
||||
<ShellToolMessage
|
||||
@@ -218,7 +224,9 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
availableTerminalHeight={
|
||||
availableTerminalHeightPerToolMessage
|
||||
}
|
||||
terminalWidth={terminalWidth - 4}
|
||||
terminalWidth={
|
||||
contentWidth - TOOL_CONFIRMATION_INTERNAL_PADDING
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{tool.outputFile && (
|
||||
@@ -240,7 +248,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && (
|
||||
<Box
|
||||
height={0}
|
||||
width={terminalWidth}
|
||||
width={contentWidth}
|
||||
borderLeft={true}
|
||||
borderRight={true}
|
||||
borderTop={false}
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { ToolMessageProps } from './ToolMessage.js';
|
||||
import type React from 'react';
|
||||
import { ToolMessage, type ToolMessageProps } from './ToolMessage.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ToolMessage } from './ToolMessage.js';
|
||||
import { StreamingState, ToolCallStatus } from '../../types.js';
|
||||
import { Text } from 'ink';
|
||||
import { StreamingContext } from '../../contexts/StreamingContext.js';
|
||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { tryParseJSON } from '../../../utils/jsonoutput.js';
|
||||
@@ -29,45 +27,6 @@ vi.mock('../TerminalOutput.js', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../AnsiOutput.js', () => ({
|
||||
AnsiOutputText: function MockAnsiOutputText({ data }: { data: AnsiOutput }) {
|
||||
// Simple serialization for snapshot stability
|
||||
const serialized = data
|
||||
.map((line) => line.map((token) => token.text || '').join(''))
|
||||
.join('\n');
|
||||
return <Text>MockAnsiOutput:{serialized}</Text>;
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock child components or utilities if they are complex or have side effects
|
||||
vi.mock('../GeminiRespondingSpinner.js', () => ({
|
||||
GeminiRespondingSpinner: ({
|
||||
nonRespondingDisplay,
|
||||
}: {
|
||||
nonRespondingDisplay?: string;
|
||||
}) => {
|
||||
const streamingState = React.useContext(StreamingContext)!;
|
||||
if (streamingState === StreamingState.Responding) {
|
||||
return <Text>MockRespondingSpinner</Text>;
|
||||
}
|
||||
return nonRespondingDisplay ? <Text>{nonRespondingDisplay}</Text> : null;
|
||||
},
|
||||
}));
|
||||
vi.mock('./DiffRenderer.js', () => ({
|
||||
DiffRenderer: function MockDiffRenderer({
|
||||
diffContent,
|
||||
}: {
|
||||
diffContent: string;
|
||||
}) {
|
||||
return <Text>MockDiff:{diffContent}</Text>;
|
||||
},
|
||||
}));
|
||||
vi.mock('../../utils/MarkdownDisplay.js', () => ({
|
||||
MarkdownDisplay: function MockMarkdownDisplay({ text }: { text: string }) {
|
||||
return <Text>MockMarkdown:{text}</Text>;
|
||||
},
|
||||
}));
|
||||
|
||||
describe('<ToolMessage />', () => {
|
||||
const baseProps: ToolMessageProps = {
|
||||
callId: 'tool-123',
|
||||
@@ -131,7 +90,6 @@ describe('<ToolMessage />', () => {
|
||||
expect(output).toContain('"a": 1');
|
||||
expect(output).toContain('"b": [');
|
||||
// Should not use markdown renderer for JSON
|
||||
expect(output).not.toContain('MockMarkdown:');
|
||||
});
|
||||
|
||||
it('renders pretty JSON in ink frame', () => {
|
||||
@@ -143,9 +101,6 @@ describe('<ToolMessage />', () => {
|
||||
const frame = lastFrame();
|
||||
|
||||
expect(frame).toMatchSnapshot();
|
||||
expect(frame).not.toContain('MockMarkdown:');
|
||||
expect(frame).not.toContain('MockAnsiOutput:');
|
||||
expect(frame).not.toMatch(/MockDiff:/);
|
||||
});
|
||||
|
||||
it('uses JSON renderer even when renderOutputAsMarkdown=true is true', () => {
|
||||
@@ -167,7 +122,6 @@ describe('<ToolMessage />', () => {
|
||||
expect(output).toContain('"a": 1');
|
||||
expect(output).toContain('"b": [');
|
||||
// Should not use markdown renderer for JSON even when renderOutputAsMarkdown=true
|
||||
expect(output).not.toContain('MockMarkdown:');
|
||||
});
|
||||
it('falls back to plain text for malformed JSON', () => {
|
||||
const testJSONstring = 'a": 1, "b": [2, 3]}';
|
||||
|
||||
@@ -113,6 +113,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
||||
hasFocus={isThisShellFocused}
|
||||
/>
|
||||
{isThisShellFocused && config && (
|
||||
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
|
||||
|
||||
@@ -4,34 +4,21 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock child components to simplify testing
|
||||
vi.mock('./DiffRenderer.js', () => ({
|
||||
DiffRenderer: ({
|
||||
diffContent,
|
||||
filename,
|
||||
}: {
|
||||
diffContent: string;
|
||||
filename: string;
|
||||
}) => (
|
||||
<Box>
|
||||
<Text>
|
||||
DiffRenderer: {filename} - {diffContent}
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock UIStateContext
|
||||
// Mock UIStateContext partially
|
||||
const mockUseUIState = vi.fn();
|
||||
vi.mock('../../contexts/UIStateContext.js', () => ({
|
||||
useUIState: () => mockUseUIState(),
|
||||
}));
|
||||
vi.mock('../../contexts/UIStateContext.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../contexts/UIStateContext.js')>();
|
||||
return {
|
||||
...actual,
|
||||
useUIState: () => mockUseUIState(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock useAlternateBuffer
|
||||
const mockUseAlternateBuffer = vi.fn();
|
||||
@@ -39,28 +26,6 @@ vi.mock('../../hooks/useAlternateBuffer.js', () => ({
|
||||
useAlternateBuffer: () => mockUseAlternateBuffer(),
|
||||
}));
|
||||
|
||||
// Mock useSettings
|
||||
vi.mock('../../contexts/SettingsContext.js', () => ({
|
||||
useSettings: () => ({
|
||||
merged: {
|
||||
ui: {
|
||||
useAlternateBuffer: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock useOverflowActions
|
||||
vi.mock('../../contexts/OverflowContext.js', () => ({
|
||||
useOverflowActions: () => ({
|
||||
addOverflowingId: vi.fn(),
|
||||
removeOverflowingId: vi.fn(),
|
||||
}),
|
||||
useOverflowState: () => ({
|
||||
overflowingIds: new Set(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ToolResultDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -68,6 +33,66 @@ describe('ToolResultDisplay', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(false);
|
||||
});
|
||||
|
||||
// Helper to use renderWithProviders
|
||||
const render = (ui: React.ReactElement) => renderWithProviders(ui);
|
||||
|
||||
it('uses ScrollableList for ANSI output in alternate buffer mode', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(true);
|
||||
const content = 'ansi content';
|
||||
const ansiResult: AnsiOutput = [
|
||||
[
|
||||
{
|
||||
text: content,
|
||||
fg: 'red',
|
||||
bg: 'black',
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
dim: false,
|
||||
inverse: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={ansiResult}
|
||||
terminalWidth={80}
|
||||
maxLines={10}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain(content);
|
||||
});
|
||||
|
||||
it('uses Scrollable for non-ANSI output in alternate buffer mode', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(true);
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="**Markdown content**"
|
||||
terminalWidth={80}
|
||||
maxLines={10}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// With real components, we check for the content itself
|
||||
expect(output).toContain('Markdown content');
|
||||
});
|
||||
|
||||
it('passes hasFocus prop to scrollable components', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(true);
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="Some result"
|
||||
terminalWidth={80}
|
||||
hasFocus={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Some result');
|
||||
});
|
||||
|
||||
it('renders string result as markdown by default', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay resultDisplay="**Some result**" terminalWidth={80} />,
|
||||
@@ -194,4 +219,86 @@ describe('ToolResultDisplay', () => {
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('truncates ANSI output when maxLines is provided', () => {
|
||||
const ansiResult: AnsiOutput = [
|
||||
[
|
||||
{
|
||||
text: 'Line 1',
|
||||
fg: '',
|
||||
bg: '',
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
dim: false,
|
||||
inverse: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: 'Line 2',
|
||||
fg: '',
|
||||
bg: '',
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
dim: false,
|
||||
inverse: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: 'Line 3',
|
||||
fg: '',
|
||||
bg: '',
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
dim: false,
|
||||
inverse: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={ansiResult}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
maxLines={2}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).not.toContain('Line 1');
|
||||
expect(output).toContain('Line 2');
|
||||
expect(output).toContain('Line 3');
|
||||
});
|
||||
|
||||
it('truncates ANSI output when maxLines is provided, even if availableTerminalHeight is undefined', () => {
|
||||
const ansiResult: AnsiOutput = Array.from({ length: 50 }, (_, i) => [
|
||||
{
|
||||
text: `Line ${i + 1}`,
|
||||
fg: '',
|
||||
bg: '',
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
dim: false,
|
||||
inverse: false,
|
||||
},
|
||||
]);
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={ansiResult}
|
||||
terminalWidth={80}
|
||||
maxLines={25}
|
||||
availableTerminalHeight={undefined}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// It SHOULD truncate to 25 lines because maxLines is provided
|
||||
expect(output).not.toContain('Line 1');
|
||||
expect(output).toContain('Line 50');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,12 +8,17 @@ import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { AnsiOutputText } from '../AnsiOutput.js';
|
||||
import { AnsiOutputText, AnsiLineText } from '../AnsiOutput.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
||||
import type { AnsiOutput, AnsiLine } from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { tryParseJSON } from '../../../utils/jsonoutput.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
import { Scrollable } from '../shared/Scrollable.js';
|
||||
import { ScrollableList } from '../shared/ScrollableList.js';
|
||||
import { SCROLL_TO_ITEM_END } from '../shared/VirtualizedList.js';
|
||||
import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
|
||||
|
||||
const STATIC_HEIGHT = 1;
|
||||
const RESERVED_LINE_COUNT = 6; // for tool name, status, padding, and 'ShowMoreLines' hint
|
||||
@@ -28,6 +33,8 @@ export interface ToolResultDisplayProps {
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
renderOutputAsMarkdown?: boolean;
|
||||
maxLines?: number;
|
||||
hasFocus?: boolean;
|
||||
}
|
||||
|
||||
interface FileDiffResult {
|
||||
@@ -40,30 +47,100 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
renderOutputAsMarkdown = true,
|
||||
maxLines,
|
||||
hasFocus = false,
|
||||
}) => {
|
||||
const { renderMarkdown } = useUIState();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
|
||||
const availableHeight = availableTerminalHeight
|
||||
let availableHeight = availableTerminalHeight
|
||||
? Math.max(
|
||||
availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT,
|
||||
MIN_LINES_SHOWN + 1, // enforce minimum lines shown
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (maxLines && availableHeight) {
|
||||
availableHeight = Math.min(availableHeight, maxLines);
|
||||
}
|
||||
|
||||
const combinedPaddingAndBorderWidth = 4;
|
||||
const childWidth = terminalWidth - combinedPaddingAndBorderWidth;
|
||||
|
||||
const keyExtractor = React.useCallback(
|
||||
(_: AnsiLine, index: number) => index.toString(),
|
||||
[],
|
||||
);
|
||||
|
||||
const renderVirtualizedAnsiLine = React.useCallback(
|
||||
({ item }: { item: AnsiLine }) => (
|
||||
<Box height={1} overflow="hidden">
|
||||
<AnsiLineText line={item} />
|
||||
</Box>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const truncatedResultDisplay = React.useMemo(() => {
|
||||
if (typeof resultDisplay === 'string') {
|
||||
if (resultDisplay.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
||||
return '...' + resultDisplay.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
||||
// Only truncate string output if not in alternate buffer mode to ensure
|
||||
// we can scroll through the full output.
|
||||
if (typeof resultDisplay === 'string' && !isAlternateBuffer) {
|
||||
let text = resultDisplay;
|
||||
if (text.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
||||
text = '...' + text.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
||||
}
|
||||
if (maxLines) {
|
||||
const hasTrailingNewline = text.endsWith('\n');
|
||||
const contentText = hasTrailingNewline ? text.slice(0, -1) : text;
|
||||
const lines = contentText.split('\n');
|
||||
if (lines.length > maxLines) {
|
||||
text =
|
||||
lines.slice(-maxLines).join('\n') +
|
||||
(hasTrailingNewline ? '\n' : '');
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return resultDisplay;
|
||||
}, [resultDisplay]);
|
||||
}, [resultDisplay, isAlternateBuffer, maxLines]);
|
||||
|
||||
if (!truncatedResultDisplay) return null;
|
||||
|
||||
// 1. Early return for background tools (Todos)
|
||||
if (
|
||||
typeof truncatedResultDisplay === 'object' &&
|
||||
'todos' in truncatedResultDisplay
|
||||
) {
|
||||
// display nothing, as the TodoTray will handle rendering todos
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. High-performance path: Virtualized ANSI in interactive mode
|
||||
if (isAlternateBuffer && Array.isArray(truncatedResultDisplay)) {
|
||||
// If availableHeight is undefined, fallback to a safe default to prevents infinite loop
|
||||
// where Container grows -> List renders more -> Container grows.
|
||||
const limit = maxLines ?? availableHeight ?? ACTIVE_SHELL_MAX_LINES;
|
||||
const listHeight = Math.min(
|
||||
(truncatedResultDisplay as AnsiOutput).length,
|
||||
limit,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box width={childWidth} flexDirection="column" maxHeight={listHeight}>
|
||||
<ScrollableList
|
||||
width={childWidth}
|
||||
data={truncatedResultDisplay as AnsiOutput}
|
||||
renderItem={renderVirtualizedAnsiLine}
|
||||
estimatedItemHeight={() => 1}
|
||||
keyExtractor={keyExtractor}
|
||||
initialScrollIndex={SCROLL_TO_ITEM_END}
|
||||
hasFocus={hasFocus}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Compute content node for non-virtualized paths
|
||||
// Check if string content is valid JSON and pretty-print it
|
||||
const prettyJSON =
|
||||
typeof truncatedResultDisplay === 'string'
|
||||
@@ -113,22 +190,38 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
typeof truncatedResultDisplay === 'object' &&
|
||||
'todos' in truncatedResultDisplay
|
||||
) {
|
||||
// display nothing, as the TodoTray will handle rendering todos
|
||||
return null;
|
||||
} else {
|
||||
const shouldDisableTruncation =
|
||||
isAlternateBuffer ||
|
||||
(availableTerminalHeight === undefined && maxLines === undefined);
|
||||
|
||||
content = (
|
||||
<AnsiOutputText
|
||||
data={truncatedResultDisplay as AnsiOutput}
|
||||
availableTerminalHeight={availableHeight}
|
||||
availableTerminalHeight={
|
||||
isAlternateBuffer ? undefined : availableHeight
|
||||
}
|
||||
width={childWidth}
|
||||
maxLines={isAlternateBuffer ? undefined : maxLines}
|
||||
disableTruncation={shouldDisableTruncation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Final render based on session mode
|
||||
if (isAlternateBuffer) {
|
||||
return (
|
||||
<Scrollable
|
||||
width={childWidth}
|
||||
maxHeight={maxLines ?? availableHeight}
|
||||
hasFocus={hasFocus} // Allow scrolling via keyboard (Shift+Up/Down)
|
||||
scrollToBottom={true}
|
||||
>
|
||||
{content}
|
||||
</Scrollable>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box width={childWidth} flexDirection="column">
|
||||
<MaxSizedBox maxHeight={availableHeight} maxWidth={childWidth}>
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('ToolResultDisplay Overflow', () => {
|
||||
streamingState: StreamingState.Idle,
|
||||
constrainHeight: true,
|
||||
},
|
||||
useAlternateBuffer: false,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ShellToolMessage /> > Height Constraints > defaults to ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is undefined 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command A shell command │
|
||||
│ │
|
||||
│ Line 86 │
|
||||
│ Line 87 │
|
||||
│ Line 88 │
|
||||
│ Line 89 │
|
||||
│ Line 90 │
|
||||
│ Line 91 │
|
||||
│ Line 92 │
|
||||
│ Line 93 │
|
||||
│ Line 94 │
|
||||
│ Line 95 │
|
||||
│ Line 96 │
|
||||
│ Line 97 │
|
||||
│ Line 98 ▄ │
|
||||
│ Line 99 █ │
|
||||
│ Line 100 █ │"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Height Constraints > respects availableTerminalHeight when it is smaller than ACTIVE_SHELL_MAX_LINES 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command A shell command │
|
||||
│ │
|
||||
│ Line 93 │
|
||||
│ Line 94 │
|
||||
│ Line 95 │
|
||||
│ Line 96 │
|
||||
│ Line 97 │
|
||||
│ Line 98 │
|
||||
│ Line 99 │
|
||||
│ Line 100 █ │"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Height Constraints > uses ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is large 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command A shell command │
|
||||
│ │
|
||||
│ Line 86 │
|
||||
│ Line 87 │
|
||||
│ Line 88 │
|
||||
│ Line 89 │
|
||||
│ Line 90 │
|
||||
│ Line 91 │
|
||||
│ Line 92 │
|
||||
│ Line 93 │
|
||||
│ Line 94 │
|
||||
│ Line 95 │
|
||||
│ Line 96 │
|
||||
│ Line 97 │
|
||||
│ Line 98 ▄ │
|
||||
│ Line 99 █ │
|
||||
│ Line 100 █ │"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Height Constraints > uses full availableTerminalHeight when focused in alternate buffer mode 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command A shell command (Shift+Tab to unfocus) │
|
||||
│ │
|
||||
│ Line 3 │
|
||||
│ Line 4 │
|
||||
│ Line 5 █ │
|
||||
│ Line 6 █ │
|
||||
│ Line 7 █ │
|
||||
│ Line 8 █ │
|
||||
│ Line 9 █ │
|
||||
│ Line 10 █ │
|
||||
│ Line 11 █ │
|
||||
│ Line 12 █ │
|
||||
│ Line 13 █ │
|
||||
│ Line 14 █ │
|
||||
│ Line 15 █ │
|
||||
│ Line 16 █ │
|
||||
│ Line 17 █ │
|
||||
│ Line 18 █ │
|
||||
│ Line 19 █ │
|
||||
│ Line 20 █ │
|
||||
│ Line 21 █ │
|
||||
│ Line 22 █ │
|
||||
│ Line 23 █ │
|
||||
│ Line 24 █ │
|
||||
│ Line 25 █ │
|
||||
│ Line 26 █ │
|
||||
│ Line 27 █ │
|
||||
│ Line 28 █ │
|
||||
│ Line 29 █ │
|
||||
│ Line 30 █ │
|
||||
│ Line 31 █ │
|
||||
│ Line 32 █ │
|
||||
│ Line 33 █ │
|
||||
│ Line 34 █ │
|
||||
│ Line 35 █ │
|
||||
│ Line 36 █ │
|
||||
│ Line 37 █ │
|
||||
│ Line 38 █ │
|
||||
│ Line 39 █ │
|
||||
│ Line 40 █ │
|
||||
│ Line 41 █ │
|
||||
│ Line 42 █ │
|
||||
│ Line 43 █ │
|
||||
│ Line 44 █ │
|
||||
│ Line 45 █ │
|
||||
│ Line 46 █ │
|
||||
│ Line 47 █ │
|
||||
│ Line 48 █ │
|
||||
│ Line 49 █ │
|
||||
│ Line 50 █ │
|
||||
│ Line 51 █ │
|
||||
│ Line 52 █ │
|
||||
│ Line 53 █ │
|
||||
│ Line 54 █ │
|
||||
│ Line 55 █ │
|
||||
│ Line 56 █ │
|
||||
│ Line 57 █ │
|
||||
│ Line 58 █ │
|
||||
│ Line 59 █ │
|
||||
│ Line 60 █ │
|
||||
│ Line 61 █ │
|
||||
│ Line 62 █ │
|
||||
│ Line 63 █ │
|
||||
│ Line 64 █ │
|
||||
│ Line 65 █ │
|
||||
│ Line 66 █ │
|
||||
│ Line 67 █ │
|
||||
│ Line 68 █ │
|
||||
│ Line 69 █ │
|
||||
│ Line 70 █ │
|
||||
│ Line 71 █ │
|
||||
│ Line 72 █ │
|
||||
│ Line 73 █ │
|
||||
│ Line 74 █ │
|
||||
│ Line 75 █ │
|
||||
│ Line 76 █ │
|
||||
│ Line 77 █ │
|
||||
│ Line 78 █ │
|
||||
│ Line 79 █ │
|
||||
│ Line 80 █ │
|
||||
│ Line 81 █ │
|
||||
│ Line 82 █ │
|
||||
│ Line 83 █ │
|
||||
│ Line 84 █ │
|
||||
│ Line 85 █ │
|
||||
│ Line 86 █ │
|
||||
│ Line 87 █ │
|
||||
│ Line 88 █ │
|
||||
│ Line 89 █ │
|
||||
│ Line 90 █ │
|
||||
│ Line 91 █ │
|
||||
│ Line 92 █ │
|
||||
│ Line 93 █ │
|
||||
│ Line 94 █ │
|
||||
│ Line 95 █ │
|
||||
│ Line 96 █ │
|
||||
│ Line 97 █ │
|
||||
│ Line 98 █ │
|
||||
│ Line 99 █ │
|
||||
│ Line 100 █ │
|
||||
│ │"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode while focused 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command A shell command (Shift+Tab to unfocus) │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode while unfocused 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command A shell command │
|
||||
│ │
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Error state 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ x Shell Command A shell command │
|
||||
│ │
|
||||
│ Error output │"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Executing state 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command A shell command │
|
||||
│ │
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Success state (history mode) 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ Shell Command A shell command │
|
||||
│ │
|
||||
│ Test result │"
|
||||
`;
|
||||
@@ -1,18 +1,18 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolConfirmationMessage Overflow > should display "press ctrl-o" hint when content overflows in ToolGroupMessage 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? test-tool a test tool ← │
|
||||
│ │
|
||||
│ ... first 49 lines hidden ... │
|
||||
│ 50 line 50 │
|
||||
│ Apply this change? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. Modify with external editor │
|
||||
│ 4. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? test-tool a test tool ← │
|
||||
│ │
|
||||
│ ... first 49 lines hidden ... │
|
||||
│ 50 line 50 │
|
||||
│ Apply this change? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. Modify with external editor │
|
||||
│ 4. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
Press ctrl-o to show more lines"
|
||||
`;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Error 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ x Ask User │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ x Ask User │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Success 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ Ask User │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ Ask User │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Confirming 1`] = `""`;
|
||||
@@ -23,89 +23,89 @@ exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when s
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Pending 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > shows other tools when ask_user is filtered out 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ other-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ other-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ ✓ another-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ ✓ another-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shell commands even when successful 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ run_shell_command A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ run_shell_command A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border when tools are pending 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ o test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ o test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Confirmation Handling > renders confirmation with permanent approval disabled 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? confirm-tool A tool for testing ← │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Do you want to proceed? │
|
||||
│ Do you want to proceed? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? confirm-tool A tool for testing ← │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Do you want to proceed? │
|
||||
│ Do you want to proceed? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Confirmation Handling > renders confirmation with permanent approval enabled 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? confirm-tool A tool for testing ← │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Do you want to proceed? │
|
||||
│ Do you want to proceed? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. Allow for all future sessions │
|
||||
│ 4. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? confirm-tool A tool for testing ← │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Do you want to proceed? │
|
||||
│ Do you want to proceed? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. Allow for all future sessions │
|
||||
│ 4. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? first-confirm A tool for testing ← │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Confirm first tool │
|
||||
│ Do you want to proceed? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. No, suggest changes (esc) │
|
||||
│ │
|
||||
│ │
|
||||
│ ? second-confirm A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? first-confirm A tool for testing ← │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Confirm first tool │
|
||||
│ Do you want to proceed? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. No, suggest changes (esc) │
|
||||
│ │
|
||||
│ │
|
||||
│ ? second-confirm A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools when event-driven scheduler is enabled 1`] = `""`;
|
||||
@@ -113,148 +113,148 @@ exports[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools
|
||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > renders nothing when only tool is in-progress AskUser with borderBottom=false 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > shows only successful tools when mixed with confirming tools 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ success-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ success-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-1 Description 1. This is a long description that will need to be tr… │
|
||||
│──────────────────────────────────────────────────────────────────────────────│
|
||||
│ line5 │ █
|
||||
│ │ █
|
||||
│ ✓ tool-2 Description 2 │ █
|
||||
│ │ █
|
||||
│ line1 │ █
|
||||
│ line2 │ █
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯ █"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-1 Description 1. This is a long description that will need to b… │
|
||||
│──────────────────────────────────────────────────────────────────────────│
|
||||
│ line5 │ █
|
||||
│ │ █
|
||||
│ ✓ tool-2 Description 2 │ █
|
||||
│ │ █
|
||||
│ line1 │ █
|
||||
│ line2 │ █
|
||||
╰──────────────────────────────────────────────────────────────────────────╯ █"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls including shell command 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ read_file Read a file │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ ⊷ run_shell_command Run command │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ o write_file Write to file │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ read_file Read a file │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ ⊷ run_shell_command Run command │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ o write_file Write to file │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders multiple tool calls with different statuses 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ successful-tool This tool succeeded │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ o pending-tool This tool is pending │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ x error-tool This tool failed │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ successful-tool This tool succeeded │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ o pending-tool This tool is pending │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │
|
||||
│ x error-tool This tool failed │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders shell command with yellow border 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ run_shell_command Execute shell command │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ run_shell_command Execute shell command │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders single successful tool call 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call awaiting confirmation 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? confirmation-tool This tool needs confirmation ← │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Are you sure you want to proceed? │
|
||||
│ Do you want to proceed? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? confirmation-tool This tool needs confirmation ← │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Are you sure you want to proceed? │
|
||||
│ Do you want to proceed? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call with outputFile 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-with-file Tool that saved output to file │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Output too long and was saved to: /path/to/output.txt │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-with-file Tool that saved output to file │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ Output too long and was saved to: /path/to/output.txt │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders two tool groups where only the last line of the previous group is visible 1`] = `
|
||||
"╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-2 Description 2 │
|
||||
│ │ ▄
|
||||
│ line1 │ █
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯ █"
|
||||
"╰──────────────────────────────────────────────────────────────────────────╯
|
||||
╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-2 Description 2 │
|
||||
│ │ ▄
|
||||
│ line1 │ █
|
||||
╰──────────────────────────────────────────────────────────────────────────╯ █"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders when not focused 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-with-result Tool with output │
|
||||
│ │
|
||||
│ This is a long result that might need height constraints │
|
||||
│ │
|
||||
│ ✓ another-tool Another tool │
|
||||
│ │
|
||||
│ More output here │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-with-result Tool with output │
|
||||
│ │
|
||||
│ This is a long result that might need height constraints │
|
||||
│ │
|
||||
│ ✓ another-tool Another tool │
|
||||
│ │
|
||||
│ More output here │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with narrow terminal width 1`] = `
|
||||
"╭──────────────────────────────────────╮
|
||||
│ ✓ very-long-tool-name-that-might-w… │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────╮
|
||||
│ ✓ very-long-tool-name-that-mig… │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Height Calculation > calculates available height correctly with multiple tools with results 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Result 1 │
|
||||
│ │
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Result 2 │
|
||||
│ │
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Result 1 │
|
||||
│ │
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Result 2 │
|
||||
│ │
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -14,93 +14,90 @@ exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ? for Confirmin
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows - for Canceled status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ - test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows MockRespondingSpinner for Executing status when streamingState is Responding 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ MockRespondingSpinnertest-tool A tool for testing │
|
||||
│ ⊶ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows o for Pending status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ o test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is Idle 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is WaitingForConfirmation 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows x for Error status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ x test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ✓ for Success status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders AnsiOutputText for AnsiOutput results 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockAnsiOutput:hello │"
|
||||
│ hello │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders DiffRenderer for diff results 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockDiff:--- a/file.txt │
|
||||
│ +++ b/file.txt │
|
||||
│ @@ -1 +1 @@ │
|
||||
│ -old │
|
||||
│ +new │"
|
||||
│ 1 - old │
|
||||
│ 1 + new │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders basic tool information 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders emphasis correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing ← │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders emphasis correctly 2`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ MockMarkdown:Test result │"
|
||||
│ Test result │"
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,13 @@ exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with ava
|
||||
|
||||
exports[`ToolResultDisplay > renders ANSI output result 1`] = `"ansi content"`;
|
||||
|
||||
exports[`ToolResultDisplay > renders file diff result 1`] = `"DiffRenderer: test.ts - diff content"`;
|
||||
exports[`ToolResultDisplay > renders file diff result 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ No changes detected. │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`ToolResultDisplay > renders nothing for todos result 1`] = `""`;
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolResultDisplay Overflow > should display "press ctrl-o" hint when content overflows in ToolGroupMessage 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool a test tool │
|
||||
│ │
|
||||
│ ... first 46 lines hidden ... │
|
||||
│ line 47 │
|
||||
│ line 48 │
|
||||
│ line 49 │
|
||||
│ line 50 │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool a test tool │
|
||||
│ │
|
||||
│ ... first 46 lines hidden ... │
|
||||
│ line 47 │
|
||||
│ line 48 │
|
||||
│ line 49 │
|
||||
│ line 50 │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
Press ctrl-o to show more lines"
|
||||
`;
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────╮ █
|
||||
│ ✓ Shell Command Description for Shell Command │ █
|
||||
│ │
|
||||
│ shell-01 │
|
||||
│ shell-02 │"
|
||||
"╭────────────────────────────────────────────────────────────────────────╮ █
|
||||
│ ✓ Shell Command Description for Shell Command │ █
|
||||
│ │
|
||||
│ shell-01 │
|
||||
│ shell-02 │"
|
||||
`;
|
||||
|
||||
exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 2`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ Shell Command Description for Shell Command │ ▄
|
||||
│────────────────────────────────────────────────────────────────────────────│ █
|
||||
│ shell-06 │ ▀
|
||||
│ shell-07 │"
|
||||
"╭────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ Shell Command Description for Shell Command │ ▄
|
||||
│────────────────────────────────────────────────────────────────────────│ █
|
||||
│ shell-06 │ ▀
|
||||
│ shell-07 │"
|
||||
`;
|
||||
|
||||
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────╮ █
|
||||
│ ✓ tool-1 Description for tool-1 │
|
||||
│ │
|
||||
│ c1-01 │
|
||||
│ c1-02 │"
|
||||
"╭────────────────────────────────────────────────────────────────────────╮ █
|
||||
│ ✓ tool-1 Description for tool-1 │
|
||||
│ │
|
||||
│ c1-01 │
|
||||
│ c1-02 │"
|
||||
`;
|
||||
|
||||
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 2`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-1 Description for tool-1 │ █
|
||||
│────────────────────────────────────────────────────────────────────────────│
|
||||
│ c1-06 │
|
||||
│ c1-07 │"
|
||||
"╭────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-1 Description for tool-1 │ █
|
||||
│────────────────────────────────────────────────────────────────────────│
|
||||
│ c1-06 │
|
||||
│ c1-07 │"
|
||||
`;
|
||||
|
||||
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 3`] = `
|
||||
"│ │
|
||||
│ ✓ tool-2 Description for tool-2 │
|
||||
│────────────────────────────────────────────────────────────────────────────│
|
||||
│ c2-10 │
|
||||
╰────────────────────────────────────────────────────────────────────────────╯ █"
|
||||
"│ │
|
||||
│ ✓ tool-2 Description for tool-2 │
|
||||
│────────────────────────────────────────────────────────────────────────│
|
||||
│ c2-10 │
|
||||
╰────────────────────────────────────────────────────────────────────────╯ █"
|
||||
`;
|
||||
|
||||
@@ -117,4 +117,91 @@ describe('<Scrollable />', () => {
|
||||
});
|
||||
expect(capturedEntry.getScrollState().scrollTop).toBe(1);
|
||||
});
|
||||
|
||||
describe('keypress handling', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'scrolls down when overflow exists and not at bottom',
|
||||
initialScrollTop: 0,
|
||||
scrollHeight: 10,
|
||||
keySequence: '\u001B[1;2B', // Shift+Down
|
||||
expectedScrollTop: 1,
|
||||
},
|
||||
{
|
||||
name: 'scrolls up when overflow exists and not at top',
|
||||
initialScrollTop: 2,
|
||||
scrollHeight: 10,
|
||||
keySequence: '\u001B[1;2A', // Shift+Up
|
||||
expectedScrollTop: 1,
|
||||
},
|
||||
{
|
||||
name: 'does not scroll up when at top (allows event to bubble)',
|
||||
initialScrollTop: 0,
|
||||
scrollHeight: 10,
|
||||
keySequence: '\u001B[1;2A', // Shift+Up
|
||||
expectedScrollTop: 0,
|
||||
},
|
||||
{
|
||||
name: 'does not scroll down when at bottom (allows event to bubble)',
|
||||
initialScrollTop: 5, // maxScroll = 10 - 5 = 5
|
||||
scrollHeight: 10,
|
||||
keySequence: '\u001B[1;2B', // Shift+Down
|
||||
expectedScrollTop: 5,
|
||||
},
|
||||
{
|
||||
name: 'does not scroll when content fits (allows event to bubble)',
|
||||
initialScrollTop: 0,
|
||||
scrollHeight: 5, // Same as innerHeight (5)
|
||||
keySequence: '\u001B[1;2B', // Shift+Down
|
||||
expectedScrollTop: 0,
|
||||
},
|
||||
])(
|
||||
'$name',
|
||||
async ({
|
||||
initialScrollTop,
|
||||
scrollHeight,
|
||||
keySequence,
|
||||
expectedScrollTop,
|
||||
}) => {
|
||||
// Dynamically import ink to mock getScrollHeight
|
||||
const ink = await import('ink');
|
||||
vi.mocked(ink.getScrollHeight).mockReturnValue(scrollHeight);
|
||||
|
||||
let capturedEntry: ScrollProviderModule.ScrollableEntry | undefined;
|
||||
vi.spyOn(ScrollProviderModule, 'useScrollable').mockImplementation(
|
||||
(entry, isActive) => {
|
||||
if (isActive) {
|
||||
capturedEntry = entry as ScrollProviderModule.ScrollableEntry;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const { stdin } = renderWithProviders(
|
||||
<Scrollable hasFocus={true} height={5}>
|
||||
<Text>Content</Text>
|
||||
</Scrollable>,
|
||||
);
|
||||
|
||||
// Ensure initial state using existing scrollBy method
|
||||
act(() => {
|
||||
// Reset to top first, then scroll to desired start position
|
||||
capturedEntry!.scrollBy(-100);
|
||||
if (initialScrollTop > 0) {
|
||||
capturedEntry!.scrollBy(initialScrollTop);
|
||||
}
|
||||
});
|
||||
expect(capturedEntry!.getScrollState().scrollTop).toBe(
|
||||
initialScrollTop,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
stdin.write(keySequence);
|
||||
});
|
||||
|
||||
expect(capturedEntry!.getScrollState().scrollTop).toBe(
|
||||
expectedScrollTop,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { useScrollable } from '../../contexts/ScrollProvider.js';
|
||||
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
||||
import { useBatchedScroll } from '../../hooks/useBatchedScroll.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
|
||||
interface ScrollableProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -103,14 +104,38 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
|
||||
useKeypress(
|
||||
(key: Key) => {
|
||||
if (key.shift) {
|
||||
if (key.name === 'up') {
|
||||
scrollByWithAnimation(-1);
|
||||
const { scrollHeight, innerHeight } = sizeRef.current;
|
||||
const scrollTop = getScrollTop();
|
||||
const maxScroll = Math.max(0, scrollHeight - innerHeight);
|
||||
|
||||
// Only capture scroll-up events if there's room;
|
||||
// otherwise allow events to bubble.
|
||||
if (scrollTop > 0) {
|
||||
if (keyMatchers[Command.PAGE_UP](key)) {
|
||||
scrollByWithAnimation(-innerHeight);
|
||||
return true;
|
||||
}
|
||||
if (key.name === 'down') {
|
||||
scrollByWithAnimation(1);
|
||||
if (keyMatchers[Command.SCROLL_UP](key)) {
|
||||
scrollByWithAnimation(-1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only capture scroll-down events if there's room;
|
||||
// otherwise allow events to bubble.
|
||||
if (scrollTop < maxScroll) {
|
||||
if (keyMatchers[Command.PAGE_DOWN](key)) {
|
||||
scrollByWithAnimation(innerHeight);
|
||||
return true;
|
||||
}
|
||||
if (keyMatchers[Command.SCROLL_DOWN](key)) {
|
||||
scrollByWithAnimation(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// bubble keypress
|
||||
return false;
|
||||
},
|
||||
{ isActive: hasFocus },
|
||||
);
|
||||
@@ -137,7 +162,7 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
[getScrollState, scrollByWithAnimation, hasFocusCallback, flashScrollbar],
|
||||
);
|
||||
|
||||
useScrollable(scrollableEntry, hasFocus && ref.current !== null);
|
||||
useScrollable(scrollableEntry, true);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -186,9 +186,11 @@ function ScrollableList<T>(
|
||||
if (keyMatchers[Command.SCROLL_UP](key)) {
|
||||
stopSmoothScroll();
|
||||
scrollByWithAnimation(-1);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SCROLL_DOWN](key)) {
|
||||
stopSmoothScroll();
|
||||
scrollByWithAnimation(1);
|
||||
return true;
|
||||
} else if (
|
||||
keyMatchers[Command.PAGE_UP](key) ||
|
||||
keyMatchers[Command.PAGE_DOWN](key)
|
||||
@@ -200,11 +202,15 @@ function ScrollableList<T>(
|
||||
: scrollState.scrollTop;
|
||||
const innerHeight = scrollState.innerHeight;
|
||||
smoothScrollTo(current + direction * innerHeight);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SCROLL_HOME](key)) {
|
||||
smoothScrollTo(0);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SCROLL_END](key)) {
|
||||
smoothScrollTo(SCROLL_TO_ITEM_END);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ isActive: hasFocus },
|
||||
);
|
||||
@@ -229,7 +235,7 @@ function ScrollableList<T>(
|
||||
],
|
||||
);
|
||||
|
||||
useScrollable(scrollableEntry, hasFocus);
|
||||
useScrollable(scrollableEntry, true);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -39,3 +39,9 @@ export const DEFAULT_BACKGROUND_OPACITY = 0.08;
|
||||
export const KEYBOARD_SHORTCUTS_URL =
|
||||
'https://geminicli.com/docs/cli/keyboard-shortcuts/';
|
||||
export const LRU_BUFFER_PERF_CACHE_LIMIT = 20000;
|
||||
|
||||
// Max lines to show for active shell output when not focused
|
||||
export const ACTIVE_SHELL_MAX_LINES = 15;
|
||||
|
||||
// Max lines to preserve in history for completed shell commands
|
||||
export const COMPLETED_SHELL_MAX_LINES = 15;
|
||||
|
||||
@@ -47,7 +47,7 @@ const findScrollableCandidates = (
|
||||
const candidates: Array<ScrollableEntry & { area: number }> = [];
|
||||
|
||||
for (const entry of scrollables.values()) {
|
||||
if (!entry.ref.current || !entry.hasFocus()) {
|
||||
if (!entry.ref.current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { act } from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { ToolActionsProvider, useToolActions } from './ToolActionsContext.js';
|
||||
import {
|
||||
type Config,
|
||||
@@ -155,7 +156,7 @@ describe('ToolActionsContext', () => {
|
||||
|
||||
// Wait for IdeClient initialization in useEffect
|
||||
await act(async () => {
|
||||
await vi.waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
|
||||
await waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
|
||||
// Give React a chance to update state
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
@@ -195,7 +196,7 @@ describe('ToolActionsContext', () => {
|
||||
|
||||
// Wait for initialization
|
||||
await act(async () => {
|
||||
await vi.waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
|
||||
await waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ vi.mock('node:os', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
vi.mock('node:crypto');
|
||||
vi.mock('../utils/textUtils.js');
|
||||
|
||||
import {
|
||||
useShellCommandProcessor,
|
||||
|
||||
@@ -245,5 +245,34 @@ describe('toolMapping', () => {
|
||||
expect(displayTool.status).toBe(ToolCallStatus.Canceled);
|
||||
expect(displayTool.resultDisplay).toBe('User cancelled');
|
||||
});
|
||||
|
||||
it('propagates borderTop and borderBottom options correctly', () => {
|
||||
const toolCall: ScheduledToolCall = {
|
||||
status: 'scheduled',
|
||||
request: mockRequest,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation,
|
||||
};
|
||||
|
||||
const result = mapToDisplay(toolCall, {
|
||||
borderTop: true,
|
||||
borderBottom: false,
|
||||
});
|
||||
expect(result.borderTop).toBe(true);
|
||||
expect(result.borderBottom).toBe(false);
|
||||
});
|
||||
|
||||
it('sets resultDisplay to undefined for pre-execution statuses', () => {
|
||||
const toolCall: ScheduledToolCall = {
|
||||
status: 'scheduled',
|
||||
request: mockRequest,
|
||||
tool: mockTool,
|
||||
invocation: mockInvocation,
|
||||
};
|
||||
|
||||
const result = mapToDisplay(toolCall);
|
||||
expect(result.tools[0].resultDisplay).toBeUndefined();
|
||||
expect(result.tools[0].status).toBe(ToolCallStatus.Pending);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,21 +166,27 @@ describe('keyMatchers', () => {
|
||||
{
|
||||
command: Command.SCROLL_UP,
|
||||
positive: [createKey('up', { shift: true })],
|
||||
negative: [createKey('up'), createKey('up', { ctrl: true })],
|
||||
negative: [createKey('up')],
|
||||
},
|
||||
{
|
||||
command: Command.SCROLL_DOWN,
|
||||
positive: [createKey('down', { shift: true })],
|
||||
negative: [createKey('down'), createKey('down', { ctrl: true })],
|
||||
negative: [createKey('down')],
|
||||
},
|
||||
{
|
||||
command: Command.SCROLL_HOME,
|
||||
positive: [createKey('home', { ctrl: true })],
|
||||
positive: [
|
||||
createKey('home', { ctrl: true }),
|
||||
createKey('home', { shift: true }),
|
||||
],
|
||||
negative: [createKey('end'), createKey('home')],
|
||||
},
|
||||
{
|
||||
command: Command.SCROLL_END,
|
||||
positive: [createKey('end', { ctrl: true })],
|
||||
positive: [
|
||||
createKey('end', { ctrl: true }),
|
||||
createKey('end', { shift: true }),
|
||||
],
|
||||
negative: [createKey('home'), createKey('end')],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user