mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Merge branch 'google-gemini:main' into show_thinking
This commit is contained in:
@@ -48,14 +48,15 @@ jobs:
|
|||||||
org: context.repo.owner,
|
org: context.repo.owner,
|
||||||
team_slug: 'gemini-cli-maintainers'
|
team_slug: 'gemini-cli-maintainers'
|
||||||
});
|
});
|
||||||
maintainerLogins = new Set(members.map(m => m.login));
|
maintainerLogins = new Set(members.map(m => m.login.toLowerCase()));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
core.warning('Failed to fetch team members');
|
core.warning('Failed to fetch team members');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMaintainer = (login, assoc) => {
|
const isMaintainer = (login, assoc) => {
|
||||||
if (maintainerLogins.size > 0) return maintainerLogins.has(login);
|
const isTeamMember = maintainerLogins.has(login.toLowerCase());
|
||||||
return ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
|
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
|
||||||
|
return isTeamMember || isRepoMaintainer;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Determine which PRs to check
|
// 2. Determine which PRs to check
|
||||||
|
|||||||
@@ -371,7 +371,9 @@ describe('AppContainer State Management', () => {
|
|||||||
mockedUseTextBuffer.mockReturnValue({
|
mockedUseTextBuffer.mockReturnValue({
|
||||||
text: '',
|
text: '',
|
||||||
setText: vi.fn(),
|
setText: vi.fn(),
|
||||||
// Add other properties if AppContainer uses them
|
lines: [''],
|
||||||
|
cursor: [0, 0],
|
||||||
|
handleInput: vi.fn().mockReturnValue(false),
|
||||||
});
|
});
|
||||||
mockedUseLogger.mockReturnValue({
|
mockedUseLogger.mockReturnValue({
|
||||||
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
|
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
|
||||||
@@ -1900,7 +1902,7 @@ describe('AppContainer State Management', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Keyboard Input Handling (CTRL+C / CTRL+D)', () => {
|
describe('Keyboard Input Handling (CTRL+C / CTRL+D)', () => {
|
||||||
let handleGlobalKeypress: (key: Key) => void;
|
let handleGlobalKeypress: (key: Key) => boolean;
|
||||||
let mockHandleSlashCommand: Mock;
|
let mockHandleSlashCommand: Mock;
|
||||||
let mockCancelOngoingRequest: Mock;
|
let mockCancelOngoingRequest: Mock;
|
||||||
let rerender: () => void;
|
let rerender: () => void;
|
||||||
@@ -1935,9 +1937,11 @@ describe('AppContainer State Management', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Capture the keypress handler from the AppContainer
|
// Capture the keypress handler from the AppContainer
|
||||||
mockedUseKeypress.mockImplementation((callback: (key: Key) => void) => {
|
mockedUseKeypress.mockImplementation(
|
||||||
|
(callback: (key: Key) => boolean) => {
|
||||||
handleGlobalKeypress = callback;
|
handleGlobalKeypress = callback;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Mock slash command handler
|
// Mock slash command handler
|
||||||
mockHandleSlashCommand = vi.fn();
|
mockHandleSlashCommand = vi.fn();
|
||||||
@@ -1961,6 +1965,9 @@ describe('AppContainer State Management', () => {
|
|||||||
mockedUseTextBuffer.mockReturnValue({
|
mockedUseTextBuffer.mockReturnValue({
|
||||||
text: '',
|
text: '',
|
||||||
setText: vi.fn(),
|
setText: vi.fn(),
|
||||||
|
lines: [''],
|
||||||
|
cursor: [0, 0],
|
||||||
|
handleInput: vi.fn().mockReturnValue(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
@@ -2020,19 +2027,6 @@ describe('AppContainer State Management', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('CTRL+D', () => {
|
describe('CTRL+D', () => {
|
||||||
it('should do nothing if text buffer is not empty', async () => {
|
|
||||||
mockedUseTextBuffer.mockReturnValue({
|
|
||||||
text: 'some text',
|
|
||||||
setText: vi.fn(),
|
|
||||||
});
|
|
||||||
await setupKeypressTest();
|
|
||||||
|
|
||||||
pressKey({ name: 'd', ctrl: true }, 2);
|
|
||||||
|
|
||||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
|
||||||
unmount();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should quit on second press if buffer is empty', async () => {
|
it('should quit on second press if buffer is empty', async () => {
|
||||||
await setupKeypressTest();
|
await setupKeypressTest();
|
||||||
|
|
||||||
@@ -2047,6 +2041,50 @@ describe('AppContainer State Management', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should NOT quit if buffer is not empty (bubbles from InputPrompt)', async () => {
|
||||||
|
mockedUseTextBuffer.mockReturnValue({
|
||||||
|
text: 'some text',
|
||||||
|
setText: vi.fn(),
|
||||||
|
lines: ['some text'],
|
||||||
|
cursor: [0, 9], // At the end
|
||||||
|
handleInput: vi.fn().mockReturnValue(false),
|
||||||
|
});
|
||||||
|
await setupKeypressTest();
|
||||||
|
|
||||||
|
// Capture return value
|
||||||
|
let result = true;
|
||||||
|
const originalPressKey = (key: Partial<Key>) => {
|
||||||
|
act(() => {
|
||||||
|
result = handleGlobalKeypress({
|
||||||
|
name: 'd',
|
||||||
|
shift: false,
|
||||||
|
alt: false,
|
||||||
|
ctrl: true,
|
||||||
|
cmd: false,
|
||||||
|
...key,
|
||||||
|
} as Key);
|
||||||
|
});
|
||||||
|
rerender();
|
||||||
|
};
|
||||||
|
|
||||||
|
originalPressKey({ name: 'd', ctrl: true });
|
||||||
|
|
||||||
|
// AppContainer's handler should return true if it reaches it
|
||||||
|
expect(result).toBe(true);
|
||||||
|
// But it should only be called once, so count is 1, not quitting yet.
|
||||||
|
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
originalPressKey({ name: 'd', ctrl: true });
|
||||||
|
// Now count is 2, it should quit.
|
||||||
|
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
|
||||||
|
'/quit',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
it('should reset press count after a timeout', async () => {
|
it('should reset press count after a timeout', async () => {
|
||||||
await setupKeypressTest();
|
await setupKeypressTest();
|
||||||
|
|
||||||
@@ -2066,7 +2104,7 @@ describe('AppContainer State Management', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Copy Mode (CTRL+S)', () => {
|
describe('Copy Mode (CTRL+S)', () => {
|
||||||
let handleGlobalKeypress: (key: Key) => void;
|
let handleGlobalKeypress: (key: Key) => boolean;
|
||||||
let rerender: () => void;
|
let rerender: () => void;
|
||||||
let unmount: () => void;
|
let unmount: () => void;
|
||||||
|
|
||||||
@@ -2096,9 +2134,11 @@ describe('AppContainer State Management', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.mockStdout.write.mockClear();
|
mocks.mockStdout.write.mockClear();
|
||||||
mockedUseKeypress.mockImplementation((callback: (key: Key) => void) => {
|
mockedUseKeypress.mockImplementation(
|
||||||
|
(callback: (key: Key) => boolean) => {
|
||||||
handleGlobalKeypress = callback;
|
handleGlobalKeypress = callback;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -532,6 +532,14 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
shellModeActive,
|
shellModeActive,
|
||||||
getPreferredEditor,
|
getPreferredEditor,
|
||||||
});
|
});
|
||||||
|
const bufferRef = useRef(buffer);
|
||||||
|
useEffect(() => {
|
||||||
|
bufferRef.current = buffer;
|
||||||
|
}, [buffer]);
|
||||||
|
|
||||||
|
const stableSetText = useCallback((text: string) => {
|
||||||
|
bufferRef.current.setText(text);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Initialize input history from logger (past sessions)
|
// Initialize input history from logger (past sessions)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -826,7 +834,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setText: (text: string) => buffer.setText(text),
|
setText: stableSetText,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
setAuthState,
|
setAuthState,
|
||||||
@@ -844,7 +852,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
openPermissionsDialog,
|
openPermissionsDialog,
|
||||||
addConfirmUpdateExtensionRequest,
|
addConfirmUpdateExtensionRequest,
|
||||||
toggleDebugProfiler,
|
toggleDebugProfiler,
|
||||||
buffer,
|
stableSetText,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1405,7 +1413,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
if (ctrlCPressCount > 1) {
|
if (ctrlCPressCount > 1) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
handleSlashCommand('/quit', undefined, undefined, false);
|
handleSlashCommand('/quit', undefined, undefined, false);
|
||||||
} else {
|
} else if (ctrlCPressCount > 0) {
|
||||||
ctrlCTimerRef.current = setTimeout(() => {
|
ctrlCTimerRef.current = setTimeout(() => {
|
||||||
setCtrlCPressCount(0);
|
setCtrlCPressCount(0);
|
||||||
ctrlCTimerRef.current = null;
|
ctrlCTimerRef.current = null;
|
||||||
@@ -1424,7 +1432,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
if (ctrlDPressCount > 1) {
|
if (ctrlDPressCount > 1) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
handleSlashCommand('/quit', undefined, undefined, false);
|
handleSlashCommand('/quit', undefined, undefined, false);
|
||||||
} else {
|
} else if (ctrlDPressCount > 0) {
|
||||||
ctrlDTimerRef.current = setTimeout(() => {
|
ctrlDTimerRef.current = setTimeout(() => {
|
||||||
setCtrlDPressCount(0);
|
setCtrlDPressCount(0);
|
||||||
ctrlDTimerRef.current = null;
|
ctrlDTimerRef.current = null;
|
||||||
@@ -1465,7 +1473,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleGlobalKeypress = useCallback(
|
const handleGlobalKeypress = useCallback(
|
||||||
(key: Key) => {
|
(key: Key): boolean => {
|
||||||
if (copyModeEnabled) {
|
if (copyModeEnabled) {
|
||||||
setCopyModeEnabled(false);
|
setCopyModeEnabled(false);
|
||||||
enableMouseEvents();
|
enableMouseEvents();
|
||||||
@@ -1492,9 +1500,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
setCtrlCPressCount((prev) => prev + 1);
|
setCtrlCPressCount((prev) => prev + 1);
|
||||||
return true;
|
return true;
|
||||||
} else if (keyMatchers[Command.EXIT](key)) {
|
} else if (keyMatchers[Command.EXIT](key)) {
|
||||||
if (buffer.text.length > 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
setCtrlDPressCount((prev) => prev + 1);
|
setCtrlDPressCount((prev) => prev + 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1538,9 +1543,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
return true;
|
return true;
|
||||||
} else if (
|
} else if (
|
||||||
keyMatchers[Command.FOCUS_SHELL_INPUT](key) &&
|
keyMatchers[Command.FOCUS_SHELL_INPUT](key) &&
|
||||||
(activePtyId ||
|
(activePtyId || (isBackgroundShellVisible && backgroundShells.size > 0))
|
||||||
(isBackgroundShellVisible && backgroundShells.size > 0)) &&
|
|
||||||
buffer.text.length === 0
|
|
||||||
) {
|
) {
|
||||||
if (key.name === 'tab' && key.shift) {
|
if (key.name === 'tab' && key.shift) {
|
||||||
// Always change focus
|
// Always change focus
|
||||||
@@ -1625,7 +1628,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
config,
|
config,
|
||||||
ideContextState,
|
ideContextState,
|
||||||
setCtrlCPressCount,
|
setCtrlCPressCount,
|
||||||
buffer.text.length,
|
|
||||||
setCtrlDPressCount,
|
setCtrlDPressCount,
|
||||||
handleSlashCommand,
|
handleSlashCommand,
|
||||||
cancelOngoingRequest,
|
cancelOngoingRequest,
|
||||||
@@ -1647,7 +1649,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeypress(handleGlobalKeypress, { isActive: true, priority: true });
|
useKeypress(handleGlobalKeypress, { isActive: true });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Respect hideWindowTitle settings
|
// Respect hideWindowTitle settings
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
|||||||
import { waitFor } from '../../test-utils/async.js';
|
import { waitFor } from '../../test-utils/async.js';
|
||||||
import { AskUserDialog } from './AskUserDialog.js';
|
import { AskUserDialog } from './AskUserDialog.js';
|
||||||
import { QuestionType, type Question } from '@google/gemini-cli-core';
|
import { QuestionType, type Question } from '@google/gemini-cli-core';
|
||||||
|
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
|
||||||
|
|
||||||
// Helper to write to stdin with proper act() wrapping
|
// Helper to write to stdin with proper act() wrapping
|
||||||
const writeKey = (stdin: { write: (data: string) => void }, key: string) => {
|
const writeKey = (stdin: { write: (data: string) => void }, key: string) => {
|
||||||
@@ -42,7 +43,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -108,7 +108,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -129,7 +128,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -159,7 +157,13 @@ describe('AskUserDialog', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows scroll arrows when options exceed available height', async () => {
|
describe.each([
|
||||||
|
{ useAlternateBuffer: true, expectedArrows: false },
|
||||||
|
{ useAlternateBuffer: false, expectedArrows: true },
|
||||||
|
])(
|
||||||
|
'Scroll Arrows (useAlternateBuffer: $useAlternateBuffer)',
|
||||||
|
({ useAlternateBuffer, expectedArrows }) => {
|
||||||
|
it(`shows scroll arrows correctly when useAlternateBuffer is ${useAlternateBuffer}`, async () => {
|
||||||
const questions: Question[] = [
|
const questions: Question[] = [
|
||||||
{
|
{
|
||||||
question: 'Choose an option',
|
question: 'Choose an option',
|
||||||
@@ -180,12 +184,22 @@ describe('AskUserDialog', () => {
|
|||||||
width={80}
|
width={80}
|
||||||
availableHeight={10} // Small height to force scrolling
|
availableHeight={10} // Small height to force scrolling
|
||||||
/>,
|
/>,
|
||||||
|
{ useAlternateBuffer },
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
if (expectedArrows) {
|
||||||
|
expect(lastFrame()).toContain('▲');
|
||||||
|
expect(lastFrame()).toContain('▼');
|
||||||
|
} else {
|
||||||
|
expect(lastFrame()).not.toContain('▲');
|
||||||
|
expect(lastFrame()).not.toContain('▼');
|
||||||
|
}
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it('navigates to custom option when typing unbound characters (Type-to-Jump)', async () => {
|
it('navigates to custom option when typing unbound characters (Type-to-Jump)', async () => {
|
||||||
const { stdin, lastFrame } = renderWithProviders(
|
const { stdin, lastFrame } = renderWithProviders(
|
||||||
@@ -194,7 +208,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -246,7 +259,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -261,7 +273,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -276,7 +287,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -308,7 +318,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -351,7 +360,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -420,7 +428,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -450,7 +457,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -496,7 +502,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -533,7 +538,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -567,7 +571,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -590,7 +593,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -613,7 +615,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -649,7 +650,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -681,7 +681,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -729,7 +728,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -780,7 +778,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -807,7 +804,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -854,7 +850,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -914,7 +909,6 @@ describe('AskUserDialog', () => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onCancel={vi.fn()}
|
onCancel={vi.fn()}
|
||||||
width={120}
|
width={120}
|
||||||
availableHeight={20}
|
|
||||||
/>,
|
/>,
|
||||||
{ width: 120 },
|
{ width: 120 },
|
||||||
);
|
);
|
||||||
@@ -946,4 +940,72 @@ describe('AskUserDialog', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses availableTerminalHeight from UIStateContext if availableHeight prop is missing', () => {
|
||||||
|
const questions: Question[] = [
|
||||||
|
{
|
||||||
|
question: 'Choose an option',
|
||||||
|
header: 'Context Test',
|
||||||
|
options: Array.from({ length: 10 }, (_, i) => ({
|
||||||
|
label: `Option ${i + 1}`,
|
||||||
|
description: `Description ${i + 1}`,
|
||||||
|
})),
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockUIState = {
|
||||||
|
availableTerminalHeight: 5, // Small height to force scroll arrows
|
||||||
|
} as UIState;
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<UIStateContext.Provider value={mockUIState}>
|
||||||
|
<AskUserDialog
|
||||||
|
questions={questions}
|
||||||
|
onSubmit={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
width={80}
|
||||||
|
/>
|
||||||
|
</UIStateContext.Provider>,
|
||||||
|
{ useAlternateBuffer: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// With height 5 and alternate buffer disabled, it should show scroll arrows (▲)
|
||||||
|
expect(lastFrame()).toContain('▲');
|
||||||
|
expect(lastFrame()).toContain('▼');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT truncate the question when in alternate buffer mode even with small height', () => {
|
||||||
|
const longQuestion =
|
||||||
|
'This is a very long question ' + 'with many words '.repeat(10);
|
||||||
|
const questions: Question[] = [
|
||||||
|
{
|
||||||
|
question: longQuestion,
|
||||||
|
header: 'Alternate Buffer Test',
|
||||||
|
options: [{ label: 'Option 1', description: 'Desc 1' }],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockUIState = {
|
||||||
|
availableTerminalHeight: 5,
|
||||||
|
} as UIState;
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<UIStateContext.Provider value={mockUIState}>
|
||||||
|
<AskUserDialog
|
||||||
|
questions={questions}
|
||||||
|
onSubmit={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
width={40} // Small width to force wrapping
|
||||||
|
/>
|
||||||
|
</UIStateContext.Provider>,
|
||||||
|
{ useAlternateBuffer: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should NOT contain the truncation message
|
||||||
|
expect(lastFrame()).not.toContain('hidden ...');
|
||||||
|
// Should contain the full long question (or at least its parts)
|
||||||
|
expect(lastFrame()).toContain('This is a very long question');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useCallback, useMemo, useRef, useEffect, useReducer } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useReducer,
|
||||||
|
useContext,
|
||||||
|
} from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import type { Question } from '@google/gemini-cli-core';
|
import type { Question } from '@google/gemini-cli-core';
|
||||||
@@ -21,6 +28,8 @@ import { getCachedStringWidth } from '../utils/textUtils.js';
|
|||||||
import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js';
|
import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js';
|
||||||
import { DialogFooter } from './shared/DialogFooter.js';
|
import { DialogFooter } from './shared/DialogFooter.js';
|
||||||
import { MaxSizedBox } from './shared/MaxSizedBox.js';
|
import { MaxSizedBox } from './shared/MaxSizedBox.js';
|
||||||
|
import { UIStateContext } from '../contexts/UIStateContext.js';
|
||||||
|
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||||
|
|
||||||
interface AskUserDialogState {
|
interface AskUserDialogState {
|
||||||
answers: { [key: string]: string };
|
answers: { [key: string]: string };
|
||||||
@@ -121,7 +130,7 @@ interface AskUserDialogProps {
|
|||||||
/**
|
/**
|
||||||
* Height constraint for scrollable content.
|
* Height constraint for scrollable content.
|
||||||
*/
|
*/
|
||||||
availableHeight: number;
|
availableHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReviewViewProps {
|
interface ReviewViewProps {
|
||||||
@@ -199,7 +208,7 @@ interface TextQuestionViewProps {
|
|||||||
onSelectionChange?: (answer: string) => void;
|
onSelectionChange?: (answer: string) => void;
|
||||||
onEditingCustomOption?: (editing: boolean) => void;
|
onEditingCustomOption?: (editing: boolean) => void;
|
||||||
availableWidth: number;
|
availableWidth: number;
|
||||||
availableHeight: number;
|
availableHeight?: number;
|
||||||
initialAnswer?: string;
|
initialAnswer?: string;
|
||||||
progressHeader?: React.ReactNode;
|
progressHeader?: React.ReactNode;
|
||||||
keyboardHints?: React.ReactNode;
|
keyboardHints?: React.ReactNode;
|
||||||
@@ -216,6 +225,7 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
|||||||
progressHeader,
|
progressHeader,
|
||||||
keyboardHints,
|
keyboardHints,
|
||||||
}) => {
|
}) => {
|
||||||
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
const prefix = '> ';
|
const prefix = '> ';
|
||||||
const horizontalPadding = 1; // 1 for cursor
|
const horizontalPadding = 1; // 1 for cursor
|
||||||
const bufferWidth =
|
const bufferWidth =
|
||||||
@@ -279,13 +289,20 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
|||||||
const INPUT_HEIGHT = 2; // TextInput + margin
|
const INPUT_HEIGHT = 2; // TextInput + margin
|
||||||
const FOOTER_HEIGHT = 2; // DialogFooter + margin
|
const FOOTER_HEIGHT = 2; // DialogFooter + margin
|
||||||
const overhead = HEADER_HEIGHT + INPUT_HEIGHT + FOOTER_HEIGHT;
|
const overhead = HEADER_HEIGHT + INPUT_HEIGHT + FOOTER_HEIGHT;
|
||||||
const questionHeight = Math.max(1, availableHeight - overhead);
|
const questionHeight =
|
||||||
|
availableHeight && !isAlternateBuffer
|
||||||
|
? Math.max(1, availableHeight - overhead)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{progressHeader}
|
{progressHeader}
|
||||||
<Box marginBottom={1}>
|
<Box marginBottom={1}>
|
||||||
<MaxSizedBox maxHeight={questionHeight} maxWidth={availableWidth}>
|
<MaxSizedBox
|
||||||
|
maxHeight={questionHeight}
|
||||||
|
maxWidth={availableWidth}
|
||||||
|
overflowDirection="bottom"
|
||||||
|
>
|
||||||
<Text bold color={theme.text.primary}>
|
<Text bold color={theme.text.primary}>
|
||||||
{question.question}
|
{question.question}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -389,7 +406,7 @@ interface ChoiceQuestionViewProps {
|
|||||||
onSelectionChange?: (answer: string) => void;
|
onSelectionChange?: (answer: string) => void;
|
||||||
onEditingCustomOption?: (editing: boolean) => void;
|
onEditingCustomOption?: (editing: boolean) => void;
|
||||||
availableWidth: number;
|
availableWidth: number;
|
||||||
availableHeight: number;
|
availableHeight?: number;
|
||||||
initialAnswer?: string;
|
initialAnswer?: string;
|
||||||
progressHeader?: React.ReactNode;
|
progressHeader?: React.ReactNode;
|
||||||
keyboardHints?: React.ReactNode;
|
keyboardHints?: React.ReactNode;
|
||||||
@@ -406,6 +423,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
|||||||
progressHeader,
|
progressHeader,
|
||||||
keyboardHints,
|
keyboardHints,
|
||||||
}) => {
|
}) => {
|
||||||
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
const numOptions =
|
const numOptions =
|
||||||
(question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0);
|
(question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0);
|
||||||
const numLen = String(numOptions).length;
|
const numLen = String(numOptions).length;
|
||||||
@@ -711,18 +729,27 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
|||||||
const TITLE_MARGIN = 1;
|
const TITLE_MARGIN = 1;
|
||||||
const FOOTER_HEIGHT = 2; // DialogFooter + margin
|
const FOOTER_HEIGHT = 2; // DialogFooter + margin
|
||||||
const overhead = HEADER_HEIGHT + TITLE_MARGIN + FOOTER_HEIGHT;
|
const overhead = HEADER_HEIGHT + TITLE_MARGIN + FOOTER_HEIGHT;
|
||||||
const listHeight = Math.max(1, availableHeight - overhead);
|
const listHeight = availableHeight
|
||||||
const questionHeight = Math.min(3, Math.max(1, listHeight - 4));
|
? Math.max(1, availableHeight - overhead)
|
||||||
const maxItemsToShow = Math.max(
|
: undefined;
|
||||||
1,
|
const questionHeight =
|
||||||
Math.floor((listHeight - questionHeight) / 2),
|
listHeight && !isAlternateBuffer
|
||||||
);
|
? Math.min(15, Math.max(1, listHeight - 4))
|
||||||
|
: undefined;
|
||||||
|
const maxItemsToShow =
|
||||||
|
listHeight && questionHeight
|
||||||
|
? Math.max(1, Math.floor((listHeight - questionHeight) / 2))
|
||||||
|
: selectionItems.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{progressHeader}
|
{progressHeader}
|
||||||
<Box marginBottom={TITLE_MARGIN}>
|
<Box marginBottom={TITLE_MARGIN}>
|
||||||
<MaxSizedBox maxHeight={questionHeight} maxWidth={availableWidth}>
|
<MaxSizedBox
|
||||||
|
maxHeight={questionHeight}
|
||||||
|
maxWidth={availableWidth}
|
||||||
|
overflowDirection="bottom"
|
||||||
|
>
|
||||||
<Text bold color={theme.text.primary}>
|
<Text bold color={theme.text.primary}>
|
||||||
{question.question}
|
{question.question}
|
||||||
{question.multiSelect && (
|
{question.multiSelect && (
|
||||||
@@ -824,8 +851,15 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onActiveTextInputChange,
|
onActiveTextInputChange,
|
||||||
width,
|
width,
|
||||||
availableHeight,
|
availableHeight: availableHeightProp,
|
||||||
}) => {
|
}) => {
|
||||||
|
const uiState = useContext(UIStateContext);
|
||||||
|
const availableHeight =
|
||||||
|
availableHeightProp ??
|
||||||
|
(uiState?.constrainHeight !== false
|
||||||
|
? uiState?.availableTerminalHeight
|
||||||
|
: undefined);
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(askUserDialogReducerLogic, initialState);
|
const [state, dispatch] = useReducer(askUserDialogReducerLogic, initialState);
|
||||||
const { answers, isEditingCustomOption, submitted } = state;
|
const { answers, isEditingCustomOption, submitted } = state;
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ import { StreamingState } from '../types.js';
|
|||||||
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
|
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
|
||||||
import type { UIState } from '../contexts/UIStateContext.js';
|
import type { UIState } from '../contexts/UIStateContext.js';
|
||||||
import { isLowColorDepth } from '../utils/terminalUtils.js';
|
import { isLowColorDepth } from '../utils/terminalUtils.js';
|
||||||
|
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||||
|
import type { Key } from '../hooks/useKeypress.js';
|
||||||
|
|
||||||
vi.mock('../hooks/useShellHistory.js');
|
vi.mock('../hooks/useShellHistory.js');
|
||||||
vi.mock('../hooks/useCommandCompletion.js');
|
vi.mock('../hooks/useCommandCompletion.js');
|
||||||
@@ -169,7 +171,16 @@ describe('InputPrompt', () => {
|
|||||||
allVisualLines: [''],
|
allVisualLines: [''],
|
||||||
visualCursor: [0, 0],
|
visualCursor: [0, 0],
|
||||||
visualScrollRow: 0,
|
visualScrollRow: 0,
|
||||||
handleInput: vi.fn(),
|
handleInput: vi.fn((key: Key) => {
|
||||||
|
if (keyMatchers[Command.CLEAR_INPUT](key)) {
|
||||||
|
if (mockBuffer.text.length > 0) {
|
||||||
|
mockBuffer.setText('');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
move: vi.fn(),
|
move: vi.fn(),
|
||||||
moveToOffset: vi.fn((offset: number) => {
|
moveToOffset: vi.fn((offset: number) => {
|
||||||
mockBuffer.cursor = [0, offset];
|
mockBuffer.cursor = [0, offset];
|
||||||
@@ -499,6 +510,23 @@ describe('InputPrompt', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should clear the buffer and reset completion on Ctrl+C', async () => {
|
||||||
|
mockBuffer.text = 'some text';
|
||||||
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||||
|
uiActions,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\u0003'); // Ctrl+C
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockBuffer.setText).toHaveBeenCalledWith('');
|
||||||
|
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
describe('clipboard image paste', () => {
|
describe('clipboard image paste', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
|
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
|
||||||
|
|||||||
@@ -604,6 +604,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyMatchers[Command.CLEAR_SCREEN](key)) {
|
||||||
|
setBannerVisible(false);
|
||||||
|
onClearScreen();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (shellModeActive && keyMatchers[Command.REVERSE_SEARCH](key)) {
|
if (shellModeActive && keyMatchers[Command.REVERSE_SEARCH](key)) {
|
||||||
setReverseSearchActive(true);
|
setReverseSearchActive(true);
|
||||||
setTextBeforeReverseSearch(buffer.text);
|
setTextBeforeReverseSearch(buffer.text);
|
||||||
@@ -611,12 +617,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyMatchers[Command.CLEAR_SCREEN](key)) {
|
|
||||||
setBannerVisible(false);
|
|
||||||
onClearScreen();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reverseSearchActive || commandSearchActive) {
|
if (reverseSearchActive || commandSearchActive) {
|
||||||
const isCommandSearch = commandSearchActive;
|
const isCommandSearch = commandSearchActive;
|
||||||
|
|
||||||
@@ -881,14 +881,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
buffer.move('end');
|
buffer.move('end');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Ctrl+C (Clear input)
|
|
||||||
if (keyMatchers[Command.CLEAR_INPUT](key)) {
|
|
||||||
if (buffer.text.length > 0) {
|
|
||||||
buffer.setText('');
|
|
||||||
resetCompletionState();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kill line commands
|
// Kill line commands
|
||||||
if (keyMatchers[Command.KILL_LINE_RIGHT](key)) {
|
if (keyMatchers[Command.KILL_LINE_RIGHT](key)) {
|
||||||
@@ -933,6 +925,11 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
// Fall back to the text buffer's default input handling for all other keys
|
// Fall back to the text buffer's default input handling for all other keys
|
||||||
const handled = buffer.handleInput(key);
|
const handled = buffer.handleInput(key);
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
if (keyMatchers[Command.CLEAR_INPUT](key)) {
|
||||||
|
resetCompletionState();
|
||||||
|
}
|
||||||
|
|
||||||
// Clear ghost text when user types regular characters (not navigation/control keys)
|
// Clear ghost text when user types regular characters (not navigation/control keys)
|
||||||
if (
|
if (
|
||||||
completion.promptCompletion.text &&
|
completion.promptCompletion.text &&
|
||||||
@@ -945,6 +942,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
completion.promptCompletion.clear();
|
completion.promptCompletion.clear();
|
||||||
setExpandedSuggestionIndex(-1);
|
setExpandedSuggestionIndex(-1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return handled;
|
return handled;
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -982,7 +980,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeypress(handleInput, { isActive: !isEmbeddedShellFocused });
|
useKeypress(handleInput, {
|
||||||
|
isActive: !isEmbeddedShellFocused,
|
||||||
|
priority: true,
|
||||||
|
});
|
||||||
|
|
||||||
const linesToRender = buffer.viewportVisualLines;
|
const linesToRender = buffer.viewportVisualLines;
|
||||||
const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
|
const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
|
||||||
|
|||||||
@@ -1,5 +1,56 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 1`] = `
|
||||||
|
"Choose an option
|
||||||
|
|
||||||
|
▲
|
||||||
|
● 1. Option 1
|
||||||
|
Description 1
|
||||||
|
2. Option 2
|
||||||
|
Description 2
|
||||||
|
▼
|
||||||
|
|
||||||
|
Enter to select · ↑/↓ to navigate · Esc to cancel"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 1`] = `
|
||||||
|
"Choose an option
|
||||||
|
|
||||||
|
● 1. Option 1
|
||||||
|
Description 1
|
||||||
|
2. Option 2
|
||||||
|
Description 2
|
||||||
|
3. Option 3
|
||||||
|
Description 3
|
||||||
|
4. Option 4
|
||||||
|
Description 4
|
||||||
|
5. Option 5
|
||||||
|
Description 5
|
||||||
|
6. Option 6
|
||||||
|
Description 6
|
||||||
|
7. Option 7
|
||||||
|
Description 7
|
||||||
|
8. Option 8
|
||||||
|
Description 8
|
||||||
|
9. Option 9
|
||||||
|
Description 9
|
||||||
|
10. Option 10
|
||||||
|
Description 10
|
||||||
|
11. Option 11
|
||||||
|
Description 11
|
||||||
|
12. Option 12
|
||||||
|
Description 12
|
||||||
|
13. Option 13
|
||||||
|
Description 13
|
||||||
|
14. Option 14
|
||||||
|
Description 14
|
||||||
|
15. Option 15
|
||||||
|
Description 15
|
||||||
|
16. Enter a custom value
|
||||||
|
|
||||||
|
Enter to select · ↑/↓ to navigate · Esc to cancel"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`AskUserDialog > Text type questions > renders text input for type: "text" 1`] = `
|
exports[`AskUserDialog > Text type questions > renders text input for type: "text" 1`] = `
|
||||||
"What should we name this component?
|
"What should we name this component?
|
||||||
|
|
||||||
@@ -104,19 +155,6 @@ Which database should we use?
|
|||||||
Enter to select · ←/→ to switch questions · Esc to cancel"
|
Enter to select · ←/→ to switch questions · Esc to cancel"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`AskUserDialog > shows scroll arrows when options exceed available height 1`] = `
|
|
||||||
"Choose an option
|
|
||||||
|
|
||||||
▲
|
|
||||||
● 1. Option 1
|
|
||||||
Description 1
|
|
||||||
2. Option 2
|
|
||||||
Description 2
|
|
||||||
▼
|
|
||||||
|
|
||||||
Enter to select · ↑/↓ to navigate · Esc to cancel"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`AskUserDialog > shows warning for unanswered questions on Review tab 1`] = `
|
exports[`AskUserDialog > shows warning for unanswered questions on Review tab 1`] = `
|
||||||
"← □ License │ □ README │ ≡ Review →
|
"← □ License │ □ README │ ≡ Review →
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
handleConfirm(ToolConfirmationOutcome.Cancel);
|
handleConfirm(ToolConfirmationOutcome.Cancel);
|
||||||
}}
|
}}
|
||||||
width={terminalWidth}
|
width={terminalWidth}
|
||||||
availableHeight={availableBodyContentHeight() ?? 10}
|
availableHeight={availableBodyContentHeight()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return { question: '', bodyContent, options: [] };
|
return { question: '', bodyContent, options: [] };
|
||||||
|
|||||||
@@ -1515,6 +1515,50 @@ describe('useTextBuffer', () => {
|
|||||||
expect(getBufferState(result).text).toBe('');
|
expect(getBufferState(result).text).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle CLEAR_INPUT (Ctrl+C)', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useTextBuffer({
|
||||||
|
initialText: 'hello',
|
||||||
|
viewport,
|
||||||
|
isValidPath: () => false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(getBufferState(result).text).toBe('hello');
|
||||||
|
let handled = false;
|
||||||
|
act(() => {
|
||||||
|
handled = result.current.handleInput({
|
||||||
|
name: 'c',
|
||||||
|
shift: false,
|
||||||
|
alt: false,
|
||||||
|
ctrl: true,
|
||||||
|
cmd: false,
|
||||||
|
insertable: false,
|
||||||
|
sequence: '\u0003',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(handled).toBe(true);
|
||||||
|
expect(getBufferState(result).text).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT handle CLEAR_INPUT if buffer is empty', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||||
|
);
|
||||||
|
let handled = true;
|
||||||
|
act(() => {
|
||||||
|
handled = result.current.handleInput({
|
||||||
|
name: 'c',
|
||||||
|
shift: false,
|
||||||
|
alt: false,
|
||||||
|
ctrl: true,
|
||||||
|
cmd: false,
|
||||||
|
insertable: false,
|
||||||
|
sequence: '\u0003',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(handled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle "Backspace" key', () => {
|
it('should handle "Backspace" key', () => {
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useTextBuffer({
|
useTextBuffer({
|
||||||
|
|||||||
@@ -2930,6 +2930,13 @@ export function useTextBuffer({
|
|||||||
move('end');
|
move('end');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (keyMatchers[Command.CLEAR_INPUT](key)) {
|
||||||
|
if (text.length > 0) {
|
||||||
|
setText('');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (keyMatchers[Command.DELETE_WORD_BACKWARD](key)) {
|
if (keyMatchers[Command.DELETE_WORD_BACKWARD](key)) {
|
||||||
deleteWordLeft();
|
deleteWordLeft();
|
||||||
return true;
|
return true;
|
||||||
@@ -2943,6 +2950,13 @@ export function useTextBuffer({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyMatchers[Command.DELETE_CHAR_RIGHT](key)) {
|
if (keyMatchers[Command.DELETE_CHAR_RIGHT](key)) {
|
||||||
|
const lastLineIdx = lines.length - 1;
|
||||||
|
if (
|
||||||
|
cursorRow === lastLineIdx &&
|
||||||
|
cursorCol === cpLen(lines[lastLineIdx] ?? '')
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
del();
|
del();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2974,6 +2988,8 @@ export function useTextBuffer({
|
|||||||
cursorCol,
|
cursorCol,
|
||||||
lines,
|
lines,
|
||||||
singleLine,
|
singleLine,
|
||||||
|
setText,
|
||||||
|
text,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user