feat(cli): support Ctrl-Z suspension (#18931)

Co-authored-by: Bharat Kunwar <brtkwr@gmail.com>
This commit is contained in:
Tommaso Sciortino
2026-02-12 09:55:56 -08:00
committed by GitHub
parent 868f43927e
commit 375ebca2da
9 changed files with 515 additions and 61 deletions

View File

@@ -135,6 +135,7 @@ vi.mock('./hooks/vim.js');
vi.mock('./hooks/useFocus.js');
vi.mock('./hooks/useBracketedPaste.js');
vi.mock('./hooks/useLoadingIndicator.js');
vi.mock('./hooks/useSuspend.js');
vi.mock('./hooks/useFolderTrust.js');
vi.mock('./hooks/useIdeTrustListener.js');
vi.mock('./hooks/useMessageQueue.js');
@@ -199,6 +200,7 @@ import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
import { useKeypress, type Key } from './hooks/useKeypress.js';
import * as useKeypressModule from './hooks/useKeypress.js';
import { useSuspend } from './hooks/useSuspend.js';
import { measureElement } from 'ink';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import {
@@ -271,6 +273,7 @@ describe('AppContainer State Management', () => {
const mockedUseTextBuffer = useTextBuffer as Mock;
const mockedUseLogger = useLogger as Mock;
const mockedUseLoadingIndicator = useLoadingIndicator as Mock;
const mockedUseSuspend = useSuspend as Mock;
const mockedUseInputHistoryStore = useInputHistoryStore as Mock;
const mockedUseHookDisplayState = useHookDisplayState as Mock;
const mockedUseTerminalTheme = useTerminalTheme as Mock;
@@ -402,6 +405,9 @@ describe('AppContainer State Management', () => {
elapsedTime: '0.0s',
currentLoadingPhrase: '',
});
mockedUseSuspend.mockReturnValue({
handleSuspend: vi.fn(),
});
mockedUseHookDisplayState.mockReturnValue([]);
mockedUseTerminalTheme.mockReturnValue(undefined);
mockedUseShellInactivityStatus.mockReturnValue({
@@ -441,8 +447,8 @@ describe('AppContainer State Management', () => {
...defaultMergedSettings.ui,
showStatusInTitle: false,
hideWindowTitle: false,
useAlternateBuffer: false,
},
useAlternateBuffer: false,
},
} as unknown as LoadedSettings;
@@ -728,10 +734,10 @@ describe('AppContainer State Management', () => {
getChatRecordingService: vi.fn(() => mockChatRecordingService),
};
const configWithRecording = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
expect(() => {
renderAppContainer({
@@ -762,11 +768,13 @@ describe('AppContainer State Management', () => {
setHistory: vi.fn(),
};
const configWithRecording = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
getSessionId: vi.fn(() => 'test-session-123'),
} as unknown as Config;
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
vi.spyOn(configWithRecording, 'getSessionId').mockReturnValue(
'test-session-123',
);
expect(() => {
renderAppContainer({
@@ -802,10 +810,10 @@ describe('AppContainer State Management', () => {
getUserTier: vi.fn(),
};
const configWithRecording = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
renderAppContainer({
config: configWithRecording,
@@ -836,10 +844,10 @@ describe('AppContainer State Management', () => {
})),
};
const configWithClient = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithClient = makeFakeConfig();
vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
const resumedData = {
conversation: {
@@ -892,10 +900,10 @@ describe('AppContainer State Management', () => {
getChatRecordingService: vi.fn(),
};
const configWithClient = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithClient = makeFakeConfig();
vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
const resumedData = {
conversation: {
@@ -945,10 +953,10 @@ describe('AppContainer State Management', () => {
getUserTier: vi.fn(),
};
const configWithRecording = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
renderAppContainer({
config: configWithRecording,
@@ -1943,6 +1951,19 @@ describe('AppContainer State Management', () => {
});
});
describe('CTRL+Z', () => {
it('should call handleSuspend', async () => {
const handleSuspend = vi.fn();
mockedUseSuspend.mockReturnValue({ handleSuspend });
await setupKeypressTest();
pressKey('\x1A'); // Ctrl+Z
expect(handleSuspend).toHaveBeenCalledTimes(1);
unmount();
});
});
describe('Focus Handling (Tab / Shift+Tab)', () => {
beforeEach(() => {
// Mock activePtyId to enable focus