Fixes for Ink 6.4.0 (#12352)

This commit is contained in:
Jacob Richman
2025-10-31 07:43:12 -07:00
committed by GitHub
parent 9da3cb7e43
commit ab8c24f5ea
12 changed files with 114 additions and 77 deletions

View File

@@ -5,6 +5,7 @@
*/
import { render as inkRender } from 'ink-testing-library';
import { Box } from 'ink';
import type React from 'react';
import { act } from 'react';
import { LoadedSettings, type Settings } from '../config/settings.js';
@@ -22,6 +23,7 @@ import { type Config } from '@google/gemini-cli-core';
// Wrapper around ink-testing-library's render that ensures act() is called
export const render = (
tree: React.ReactElement,
terminalWidth?: number,
): ReturnType<typeof inkRender> => {
let renderResult: ReturnType<typeof inkRender> =
undefined as unknown as ReturnType<typeof inkRender>;
@@ -29,6 +31,19 @@ export const render = (
renderResult = inkRender(tree);
});
if (terminalWidth !== undefined && renderResult?.stdout) {
// Override the columns getter on the stdout instance provided by ink-testing-library
Object.defineProperty(renderResult.stdout, 'columns', {
get: () => terminalWidth,
configurable: true,
});
// Trigger a rerender so Ink can pick up the new terminal width
act(() => {
renderResult.rerender(tree);
});
}
const originalUnmount = renderResult.unmount;
const originalRerender = renderResult.rerender;
@@ -148,13 +163,21 @@ export const renderWithProviders = (
<VimModeProvider settings={settings}>
<ShellFocusContext.Provider value={shellFocus}>
<KeypressProvider kittyProtocolEnabled={kittyProtocolEnabled}>
{component}
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{component}
</Box>
</KeypressProvider>
</ShellFocusContext.Provider>
</VimModeProvider>
</UIStateContext.Provider>
</SettingsContext.Provider>
</ConfigContext.Provider>,
terminalWidth,
);
};

View File

@@ -47,6 +47,7 @@ const renderWithContext = (
<StreamingContext.Provider value={contextValue}>
{ui}
</StreamingContext.Provider>,
width,
);
};

View File

@@ -67,7 +67,10 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
</Text>
)}
{!isNarrow && cancelAndTimerContent && (
<Text color={theme.text.secondary}> {cancelAndTimerContent}</Text>
<>
<Box flexShrink={0} width={1} />
<Text color={theme.text.secondary}>{cancelAndTimerContent}</Text>
</>
)}
</Box>
{!isNarrow && <Box flexGrow={1}>{/* Spacer */}</Box>}

View File

@@ -14,6 +14,7 @@ describe('LoopDetectionConfirmation', () => {
it('renders correctly', () => {
const { lastFrame } = renderWithProviders(
<LoopDetectionConfirmation onComplete={onComplete} />,
{ width: 101 },
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -21,6 +22,7 @@ describe('LoopDetectionConfirmation', () => {
it('contains the expected options', () => {
const { lastFrame } = renderWithProviders(
<LoopDetectionConfirmation onComplete={onComplete} />,
{ width: 100 },
);
const output = lastFrame()!.toString();

View File

@@ -50,37 +50,39 @@ export function LoopDetectionConfirmation({
];
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.status.warning}
width="100%"
marginLeft={1}
>
<Box paddingX={1} paddingY={0} flexDirection="column">
<Box minHeight={1}>
<Box minWidth={3}>
<Text color={theme.status.warning} aria-label="Loop detected:">
?
</Text>
<Box width="100%" flexDirection="row">
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.status.warning}
flexGrow={1}
marginLeft={1}
>
<Box paddingX={1} paddingY={0} flexDirection="column">
<Box minHeight={1}>
<Box minWidth={3}>
<Text color={theme.status.warning} aria-label="Loop detected:">
?
</Text>
</Box>
<Box>
<Text wrap="truncate-end">
<Text color={theme.text.primary} bold>
A potential loop was detected
</Text>{' '}
</Text>
</Box>
</Box>
<Box>
<Text wrap="truncate-end">
<Text color={theme.text.primary} bold>
A potential loop was detected
</Text>{' '}
</Text>
</Box>
</Box>
<Box width="100%" marginTop={1}>
<Box flexDirection="column">
<Text color={theme.text.secondary}>
This can happen due to repetitive tool calls or other model
behavior. Do you want to keep loop detection enabled or disable it
for this session?
</Text>
<Box marginTop={1}>
<RadioButtonSelect items={OPTIONS} onSelect={onComplete} />
<Box marginTop={1}>
<Box flexDirection="column">
<Text color={theme.text.secondary}>
This can happen due to repetitive tool calls or other model
behavior. Do you want to keep loop detection enabled or disable
it for this session?
</Text>
<Box marginTop={1}>
<RadioButtonSelect items={OPTIONS} onSelect={onComplete} />
</Box>
</Box>
</Box>
</Box>

View File

@@ -73,13 +73,14 @@ describe('PrepareLabel', () => {
textColor={color}
isExpanded={true}
/>,
100,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('creates centered window around match when collapsed', () => {
const prefix = 'cd /very/long/path/that/keeps/going/'.repeat(3);
const prefix = 'cd_/very/long/path/that/keeps/going/'.repeat(3);
const core = 'search-here';
const suffix = '/and/then/some/more/components/'.repeat(3);
const label = prefix + core + suffix;
@@ -92,6 +93,7 @@ describe('PrepareLabel', () => {
textColor={color}
isExpanded={false}
/>,
100,
);
const out = lastFrame();
const f = flat(out);

View File

@@ -19,6 +19,7 @@ describe('ShellConfirmationDialog', () => {
it('renders correctly', () => {
const { lastFrame } = renderWithProviders(
<ShellConfirmationDialog request={request} />,
{ width: 101 },
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -45,6 +46,7 @@ describe('ShellConfirmationDialog', () => {
it('calls onConfirm with Cancel when "No (esc)" is selected', () => {
const { lastFrame } = renderWithProviders(
<ShellConfirmationDialog request={request} />,
{ width: 100 },
);
const select = lastFrame()!.toString();
// Simulate selecting the third option

View File

@@ -68,41 +68,43 @@ export const ShellConfirmationDialog: React.FC<
];
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.status.warning}
padding={1}
width="100%"
marginLeft={1}
>
<Box flexDirection="column" marginBottom={1}>
<Text bold color={theme.text.primary}>
Shell Command Execution
</Text>
<Text color={theme.text.primary}>
A custom command wants to run the following shell commands:
</Text>
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.border.default}
paddingX={1}
marginTop={1}
>
{commands.map((cmd) => (
<Text key={cmd} color={theme.text.link}>
<RenderInline text={cmd} />
</Text>
))}
<Box flexDirection="row" width="100%">
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.status.warning}
padding={1}
flexGrow={1}
marginLeft={1}
>
<Box flexDirection="column" marginBottom={1}>
<Text bold color={theme.text.primary}>
Shell Command Execution
</Text>
<Text color={theme.text.primary}>
A custom command wants to run the following shell commands:
</Text>
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.border.default}
paddingX={1}
marginTop={1}
>
{commands.map((cmd) => (
<Text key={cmd} color={theme.text.link}>
<RenderInline text={cmd} />
</Text>
))}
</Box>
</Box>
</Box>
<Box marginBottom={1}>
<Text color={theme.text.primary}>Do you want to proceed?</Text>
</Box>
<Box marginBottom={1}>
<Text color={theme.text.primary}>Do you want to proceed?</Text>
</Box>
<RadioButtonSelect items={options} onSelect={handleSelect} isFocused />
<RadioButtonSelect items={options} onSelect={handleSelect} isFocused />
</Box>
</Box>
);
};

View File

@@ -112,7 +112,7 @@ export function SuggestionsDisplay({
</Box>
)}
{isActive && isLong && (
<Box>
<Box width={3} flexShrink={0}>
<Text color={Colors.Gray}>{isExpanded ? ' ← ' : ' → '}</Text>
</Box>
)}

View File

@@ -2,7 +2,7 @@
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-collapsed-match 1`] = `
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ (r:) Type your message or @path/to/file │
│ (r:) Type your message or @path/to/file
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll →
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
@@ -11,7 +11,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-expanded-match 1`] = `
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ (r:) Type your message or @path/to/file │
│ (r:) Type your message or @path/to/file
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ←
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
@@ -20,38 +20,38 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = `
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ (r:) commit │
│ (r:) commit
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
git commit -m "feat: add search" in src/app"
`;
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 1`] = `
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ (r:) commit │
│ (r:) commit
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
git commit -m "feat: add search" in src/app"
`;
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ > Type your message or @path/to/file │
│ > Type your message or @path/to/file
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = `
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ! Type your message or @path/to/file │
│ ! Type your message or @path/to/file
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`InputPrompt > snapshots > should render correctly in yolo mode 1`] = `
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ * Type your message or @path/to/file │
│ * Type your message or @path/to/file
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = `
"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ > Type your message or @path/to/file │
│ > Type your message or @path/to/file
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<LoadingIndicator /> > should truncate long primary text instead of wrapping 1`] = `
"MockResponding This is an extremely long loading phrase that should be truncated in t (esc to
Spinner cancel, 5s)"
"MockRespondin This is an extremely long loading phrase that shoul (esc to
gSpinner cancel, 5s)"
`;

View File

@@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`PrepareLabel > creates centered window around match when collapsed 1`] = `
"...ry/long/path/that/keeps/going/cd /very/long/path/that/keeps/going/search-here/and/then/some/more/
"...ry/long/path/that/keeps/going/cd_/very/long/path/that/keeps/going/search-here/and/then/some/more/
components//and/then/some/more/components//and/..."
`;