fix(input) Resolve cases where escape was broken (#6304)
This commit is contained in:
parent
1a2906a8ad
commit
bd5e49c5ff
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from 'ink-testing-library';
|
||||||
|
import React from 'react';
|
||||||
|
import { KeypressProvider } from '../ui/contexts/KeypressContext.js';
|
||||||
|
|
||||||
|
export const renderWithProviders = (
|
||||||
|
component: React.ReactElement,
|
||||||
|
): ReturnType<typeof render> =>
|
||||||
|
render(
|
||||||
|
<KeypressProvider kittyProtocolEnabled={true}>
|
||||||
|
{component}
|
||||||
|
</KeypressProvider>,
|
||||||
|
);
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
|
||||||
import { render } from 'ink-testing-library';
|
import { renderWithProviders } from '../test-utils/render.js';
|
||||||
import { AppWrapper as App } from './App.js';
|
import { AppWrapper as App } from './App.js';
|
||||||
import {
|
import {
|
||||||
Config as ServerConfig,
|
Config as ServerConfig,
|
||||||
|
@ -371,7 +371,7 @@ describe('App UI', () => {
|
||||||
mockedCheckForUpdates.mockResolvedValue(info);
|
mockedCheckForUpdates.mockResolvedValue(info);
|
||||||
const { spawn } = await import('node:child_process');
|
const { spawn } = await import('node:child_process');
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -397,7 +397,7 @@ describe('App UI', () => {
|
||||||
};
|
};
|
||||||
mockedCheckForUpdates.mockResolvedValue(info);
|
mockedCheckForUpdates.mockResolvedValue(info);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -427,7 +427,7 @@ describe('App UI', () => {
|
||||||
};
|
};
|
||||||
mockedCheckForUpdates.mockResolvedValue(info);
|
mockedCheckForUpdates.mockResolvedValue(info);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -457,7 +457,7 @@ describe('App UI', () => {
|
||||||
};
|
};
|
||||||
mockedCheckForUpdates.mockResolvedValue(info);
|
mockedCheckForUpdates.mockResolvedValue(info);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -491,7 +491,7 @@ describe('App UI', () => {
|
||||||
mockedCheckForUpdates.mockResolvedValue(info);
|
mockedCheckForUpdates.mockResolvedValue(info);
|
||||||
const { spawn } = await import('node:child_process');
|
const { spawn } = await import('node:child_process');
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -520,7 +520,7 @@ describe('App UI', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -539,7 +539,7 @@ describe('App UI', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -575,7 +575,7 @@ describe('App UI', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -603,7 +603,7 @@ describe('App UI', () => {
|
||||||
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
|
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
|
||||||
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['GEMINI.md']);
|
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['GEMINI.md']);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -624,7 +624,7 @@ describe('App UI', () => {
|
||||||
mockConfig.getDebugMode.mockReturnValue(false);
|
mockConfig.getDebugMode.mockReturnValue(false);
|
||||||
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -645,7 +645,7 @@ describe('App UI', () => {
|
||||||
mockConfig.getDebugMode.mockReturnValue(false);
|
mockConfig.getDebugMode.mockReturnValue(false);
|
||||||
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -666,7 +666,7 @@ describe('App UI', () => {
|
||||||
mockConfig.getDebugMode.mockReturnValue(false);
|
mockConfig.getDebugMode.mockReturnValue(false);
|
||||||
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -693,7 +693,7 @@ describe('App UI', () => {
|
||||||
mockConfig.getDebugMode.mockReturnValue(false);
|
mockConfig.getDebugMode.mockReturnValue(false);
|
||||||
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -718,7 +718,7 @@ describe('App UI', () => {
|
||||||
mockConfig.getDebugMode.mockReturnValue(false);
|
mockConfig.getDebugMode.mockReturnValue(false);
|
||||||
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -739,7 +739,7 @@ describe('App UI', () => {
|
||||||
mockConfig.getDebugMode.mockReturnValue(false);
|
mockConfig.getDebugMode.mockReturnValue(false);
|
||||||
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -763,7 +763,7 @@ describe('App UI', () => {
|
||||||
mockConfig.getDebugMode.mockReturnValue(false);
|
mockConfig.getDebugMode.mockReturnValue(false);
|
||||||
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -785,7 +785,7 @@ describe('App UI', () => {
|
||||||
mockConfig.getDebugMode.mockReturnValue(false);
|
mockConfig.getDebugMode.mockReturnValue(false);
|
||||||
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
mockConfig.getShowMemoryUsage.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -798,7 +798,7 @@ describe('App UI', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display Tips component by default', async () => {
|
it('should display Tips component by default', async () => {
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -817,7 +817,7 @@ describe('App UI', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -831,7 +831,7 @@ describe('App UI', () => {
|
||||||
|
|
||||||
it('should display Header component by default', async () => {
|
it('should display Header component by default', async () => {
|
||||||
const { Header } = await import('./components/Header.js');
|
const { Header } = await import('./components/Header.js');
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -849,7 +849,7 @@ describe('App UI', () => {
|
||||||
user: { hideBanner: true },
|
user: { hideBanner: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -868,7 +868,7 @@ describe('App UI', () => {
|
||||||
workspace: { hideTips: true },
|
workspace: { hideTips: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -898,7 +898,7 @@ describe('App UI', () => {
|
||||||
it('should display theme dialog if NO_COLOR is not set', async () => {
|
it('should display theme dialog if NO_COLOR is not set', async () => {
|
||||||
delete process.env.NO_COLOR;
|
delete process.env.NO_COLOR;
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -913,7 +913,7 @@ describe('App UI', () => {
|
||||||
it('should display a message if NO_COLOR is set', async () => {
|
it('should display a message if NO_COLOR is set', async () => {
|
||||||
process.env.NO_COLOR = 'true';
|
process.env.NO_COLOR = 'true';
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -928,7 +928,7 @@ describe('App UI', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the initial UI correctly', () => {
|
it('should render the initial UI correctly', () => {
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -948,7 +948,7 @@ describe('App UI', () => {
|
||||||
thought: null,
|
thought: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -978,7 +978,7 @@ describe('App UI', () => {
|
||||||
getUserTier: vi.fn(),
|
getUserTier: vi.fn(),
|
||||||
} as unknown as GeminiClient);
|
} as unknown as GeminiClient);
|
||||||
|
|
||||||
const { unmount, rerender } = render(
|
const { unmount, rerender } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -1020,7 +1020,7 @@ describe('App UI', () => {
|
||||||
clearConsoleMessages: vi.fn(),
|
clearConsoleMessages: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -1046,7 +1046,7 @@ describe('App UI', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -1068,7 +1068,7 @@ describe('App UI', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -1088,7 +1088,7 @@ describe('App UI', () => {
|
||||||
rows: 24,
|
rows: 24,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -1108,7 +1108,7 @@ describe('App UI', () => {
|
||||||
handleFolderTrustSelect: vi.fn(),
|
handleFolderTrustSelect: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -1128,7 +1128,7 @@ describe('App UI', () => {
|
||||||
});
|
});
|
||||||
mockConfig.isTrustedFolder.mockReturnValue(false);
|
mockConfig.isTrustedFolder.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
@ -1148,7 +1148,7 @@ describe('App UI', () => {
|
||||||
});
|
});
|
||||||
mockConfig.isTrustedFolder.mockReturnValue(false);
|
mockConfig.isTrustedFolder.mockReturnValue(false);
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<App
|
<App
|
||||||
config={mockConfig as unknown as ServerConfig}
|
config={mockConfig as unknown as ServerConfig}
|
||||||
settings={mockSettings}
|
settings={mockSettings}
|
||||||
|
|
|
@ -80,6 +80,7 @@ import { useTextBuffer } from './components/shared/text-buffer.js';
|
||||||
import { useVimMode, VimModeProvider } from './contexts/VimModeContext.js';
|
import { useVimMode, VimModeProvider } from './contexts/VimModeContext.js';
|
||||||
import { useVim } from './hooks/vim.js';
|
import { useVim } from './hooks/vim.js';
|
||||||
import { useKeypress, Key } from './hooks/useKeypress.js';
|
import { useKeypress, Key } from './hooks/useKeypress.js';
|
||||||
|
import { KeypressProvider } from './contexts/KeypressContext.js';
|
||||||
import { useKittyKeyboardProtocol } from './hooks/useKittyKeyboardProtocol.js';
|
import { useKittyKeyboardProtocol } from './hooks/useKittyKeyboardProtocol.js';
|
||||||
import { keyMatchers, Command } from './keyMatchers.js';
|
import { keyMatchers, Command } from './keyMatchers.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
@ -109,13 +110,21 @@ interface AppProps {
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppWrapper = (props: AppProps) => (
|
export const AppWrapper = (props: AppProps) => {
|
||||||
<SessionStatsProvider>
|
const kittyProtocolStatus = useKittyKeyboardProtocol();
|
||||||
<VimModeProvider settings={props.settings}>
|
return (
|
||||||
<App {...props} />
|
<KeypressProvider
|
||||||
</VimModeProvider>
|
kittyProtocolEnabled={kittyProtocolStatus.enabled}
|
||||||
</SessionStatsProvider>
|
config={props.config}
|
||||||
);
|
>
|
||||||
|
<SessionStatsProvider>
|
||||||
|
<VimModeProvider settings={props.settings}>
|
||||||
|
<App {...props} />
|
||||||
|
</VimModeProvider>
|
||||||
|
</SessionStatsProvider>
|
||||||
|
</KeypressProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
const isFocused = useFocus();
|
const isFocused = useFocus();
|
||||||
|
@ -611,7 +620,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
const { elapsedTime, currentLoadingPhrase } =
|
const { elapsedTime, currentLoadingPhrase } =
|
||||||
useLoadingIndicator(streamingState);
|
useLoadingIndicator(streamingState);
|
||||||
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config });
|
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config });
|
||||||
const kittyProtocolStatus = useKittyKeyboardProtocol();
|
|
||||||
|
|
||||||
const handleExit = useCallback(
|
const handleExit = useCallback(
|
||||||
(
|
(
|
||||||
|
@ -706,8 +714,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
|
|
||||||
useKeypress(handleGlobalKeypress, {
|
useKeypress(handleGlobalKeypress, {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
kittyProtocolEnabled: kittyProtocolStatus.enabled,
|
|
||||||
config,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from 'ink-testing-library';
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { AuthDialog } from './AuthDialog.js';
|
import { AuthDialog } from './AuthDialog.js';
|
||||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||||
import { AuthType } from '@google/gemini-cli-core';
|
import { AuthType } from '@google/gemini-cli-core';
|
||||||
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
|
|
||||||
describe('AuthDialog', () => {
|
describe('AuthDialog', () => {
|
||||||
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
@ -47,7 +47,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<AuthDialog
|
<AuthDialog
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
|
@ -84,7 +84,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
<AuthDialog onSelect={() => {}} settings={settings} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
<AuthDialog onSelect={() => {}} settings={settings} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
<AuthDialog onSelect={() => {}} settings={settings} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
<AuthDialog onSelect={() => {}} settings={settings} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
<AuthDialog onSelect={() => {}} settings={settings} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -244,7 +244,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
<AuthDialog onSelect={() => {}} settings={settings} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -279,7 +279,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { lastFrame, stdin, unmount } = render(
|
const { lastFrame, stdin, unmount } = renderWithProviders(
|
||||||
<AuthDialog onSelect={onSelect} settings={settings} />,
|
<AuthDialog onSelect={onSelect} settings={settings} />,
|
||||||
);
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
@ -318,7 +318,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { lastFrame, stdin, unmount } = render(
|
const { lastFrame, stdin, unmount } = renderWithProviders(
|
||||||
<AuthDialog
|
<AuthDialog
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
|
@ -360,7 +360,7 @@ describe('AuthDialog', () => {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<AuthDialog onSelect={onSelect} settings={settings} />,
|
<AuthDialog onSelect={onSelect} settings={settings} />,
|
||||||
);
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
|
@ -4,14 +4,16 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from 'ink-testing-library';
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
import { waitFor } from '@testing-library/react';
|
import { waitFor } from '@testing-library/react';
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import { FolderTrustDialog, FolderTrustChoice } from './FolderTrustDialog.js';
|
import { FolderTrustDialog, FolderTrustChoice } from './FolderTrustDialog.js';
|
||||||
|
|
||||||
describe('FolderTrustDialog', () => {
|
describe('FolderTrustDialog', () => {
|
||||||
it('should render the dialog with title and description', () => {
|
it('should render the dialog with title and description', () => {
|
||||||
const { lastFrame } = render(<FolderTrustDialog onSelect={vi.fn()} />);
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<FolderTrustDialog onSelect={vi.fn()} />,
|
||||||
|
);
|
||||||
|
|
||||||
expect(lastFrame()).toContain('Do you trust this folder?');
|
expect(lastFrame()).toContain('Do you trust this folder?');
|
||||||
expect(lastFrame()).toContain(
|
expect(lastFrame()).toContain(
|
||||||
|
@ -21,7 +23,9 @@ describe('FolderTrustDialog', () => {
|
||||||
|
|
||||||
it('should call onSelect with DO_NOT_TRUST when escape is pressed', async () => {
|
it('should call onSelect with DO_NOT_TRUST when escape is pressed', async () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
const { stdin } = render(<FolderTrustDialog onSelect={onSelect} />);
|
const { stdin } = renderWithProviders(
|
||||||
|
<FolderTrustDialog onSelect={onSelect} />,
|
||||||
|
);
|
||||||
|
|
||||||
stdin.write('\x1b');
|
stdin.write('\x1b');
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from 'ink-testing-library';
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
import { waitFor } from '@testing-library/react';
|
import { waitFor } from '@testing-library/react';
|
||||||
import { InputPrompt, InputPromptProps } from './InputPrompt.js';
|
import { InputPrompt, InputPromptProps } from './InputPrompt.js';
|
||||||
import type { TextBuffer } from './shared/text-buffer.js';
|
import type { TextBuffer } from './shared/text-buffer.js';
|
||||||
|
@ -197,7 +197,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
it('should call shellHistory.getPreviousCommand on up arrow in shell mode', async () => {
|
it('should call shellHistory.getPreviousCommand on up arrow in shell mode', async () => {
|
||||||
props.shellModeActive = true;
|
props.shellModeActive = true;
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\u001B[A');
|
stdin.write('\u001B[A');
|
||||||
|
@ -209,7 +209,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
it('should call shellHistory.getNextCommand on down arrow in shell mode', async () => {
|
it('should call shellHistory.getNextCommand on down arrow in shell mode', async () => {
|
||||||
props.shellModeActive = true;
|
props.shellModeActive = true;
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\u001B[B');
|
stdin.write('\u001B[B');
|
||||||
|
@ -224,7 +224,7 @@ describe('InputPrompt', () => {
|
||||||
vi.mocked(mockShellHistory.getPreviousCommand).mockReturnValue(
|
vi.mocked(mockShellHistory.getPreviousCommand).mockReturnValue(
|
||||||
'previous command',
|
'previous command',
|
||||||
);
|
);
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\u001B[A');
|
stdin.write('\u001B[A');
|
||||||
|
@ -238,7 +238,7 @@ describe('InputPrompt', () => {
|
||||||
it('should call shellHistory.addCommandToHistory on submit in shell mode', async () => {
|
it('should call shellHistory.addCommandToHistory on submit in shell mode', async () => {
|
||||||
props.shellModeActive = true;
|
props.shellModeActive = true;
|
||||||
props.buffer.setText('ls -l');
|
props.buffer.setText('ls -l');
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\r');
|
stdin.write('\r');
|
||||||
|
@ -251,7 +251,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
it('should NOT call shell history methods when not in shell mode', async () => {
|
it('should NOT call shell history methods when not in shell mode', async () => {
|
||||||
props.buffer.setText('some text');
|
props.buffer.setText('some text');
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\u001B[A'); // Up arrow
|
stdin.write('\u001B[A'); // Up arrow
|
||||||
|
@ -283,7 +283,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
props.buffer.setText('/mem');
|
props.buffer.setText('/mem');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
// Test up arrow
|
// Test up arrow
|
||||||
|
@ -309,7 +309,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('/mem');
|
props.buffer.setText('/mem');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
// Test down arrow
|
// Test down arrow
|
||||||
|
@ -331,7 +331,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('some text');
|
props.buffer.setText('some text');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\u001B[A'); // Up arrow
|
stdin.write('\u001B[A'); // Up arrow
|
||||||
|
@ -363,7 +363,9 @@ describe('InputPrompt', () => {
|
||||||
'/test/.gemini-clipboard/clipboard-123.png',
|
'/test/.gemini-clipboard/clipboard-123.png',
|
||||||
);
|
);
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
// Send Ctrl+V
|
// Send Ctrl+V
|
||||||
|
@ -384,7 +386,9 @@ describe('InputPrompt', () => {
|
||||||
it('should not insert anything when clipboard has no image', async () => {
|
it('should not insert anything when clipboard has no image', async () => {
|
||||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
|
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x16'); // Ctrl+V
|
stdin.write('\x16'); // Ctrl+V
|
||||||
|
@ -400,7 +404,9 @@ describe('InputPrompt', () => {
|
||||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
|
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
|
||||||
vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(null);
|
vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(null);
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x16'); // Ctrl+V
|
stdin.write('\x16'); // Ctrl+V
|
||||||
|
@ -426,7 +432,9 @@ describe('InputPrompt', () => {
|
||||||
mockBuffer.lines = ['Hello world'];
|
mockBuffer.lines = ['Hello world'];
|
||||||
mockBuffer.replaceRangeByOffset = vi.fn();
|
mockBuffer.replaceRangeByOffset = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x16'); // Ctrl+V
|
stdin.write('\x16'); // Ctrl+V
|
||||||
|
@ -454,7 +462,9 @@ describe('InputPrompt', () => {
|
||||||
new Error('Clipboard error'),
|
new Error('Clipboard error'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x16'); // Ctrl+V
|
stdin.write('\x16'); // Ctrl+V
|
||||||
|
@ -481,7 +491,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('/mem');
|
props.buffer.setText('/mem');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\t'); // Press Tab
|
stdin.write('\t'); // Press Tab
|
||||||
|
@ -504,7 +514,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('/memory ');
|
props.buffer.setText('/memory ');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\t'); // Press Tab
|
stdin.write('\t'); // Press Tab
|
||||||
|
@ -528,7 +538,7 @@ describe('InputPrompt', () => {
|
||||||
// The user has backspaced, so the query is now just '/memory'
|
// The user has backspaced, so the query is now just '/memory'
|
||||||
props.buffer.setText('/memory');
|
props.buffer.setText('/memory');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\t'); // Press Tab
|
stdin.write('\t'); // Press Tab
|
||||||
|
@ -549,7 +559,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('/chat resume fi-');
|
props.buffer.setText('/chat resume fi-');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\t'); // Press Tab
|
stdin.write('\t'); // Press Tab
|
||||||
|
@ -568,7 +578,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('/mem');
|
props.buffer.setText('/mem');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\r');
|
stdin.write('\r');
|
||||||
|
@ -599,7 +609,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('/?');
|
props.buffer.setText('/?');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\t'); // Press Tab for autocomplete
|
stdin.write('\t'); // Press Tab for autocomplete
|
||||||
|
@ -612,7 +622,7 @@ describe('InputPrompt', () => {
|
||||||
it('should not submit on Enter when the buffer is empty or only contains whitespace', async () => {
|
it('should not submit on Enter when the buffer is empty or only contains whitespace', async () => {
|
||||||
props.buffer.setText(' '); // Set buffer to whitespace
|
props.buffer.setText(' '); // Set buffer to whitespace
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\r'); // Press Enter
|
stdin.write('\r'); // Press Enter
|
||||||
|
@ -630,7 +640,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('/clear');
|
props.buffer.setText('/clear');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\r');
|
stdin.write('\r');
|
||||||
|
@ -648,7 +658,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('/clear');
|
props.buffer.setText('/clear');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\r');
|
stdin.write('\r');
|
||||||
|
@ -667,7 +677,7 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
props.buffer.setText('@src/components/');
|
props.buffer.setText('@src/components/');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\r');
|
stdin.write('\r');
|
||||||
|
@ -684,7 +694,7 @@ describe('InputPrompt', () => {
|
||||||
mockBuffer.cursor = [0, 11];
|
mockBuffer.cursor = [0, 11];
|
||||||
mockBuffer.lines = ['first line\\'];
|
mockBuffer.lines = ['first line\\'];
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\r');
|
stdin.write('\r');
|
||||||
|
@ -698,7 +708,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
it('should clear the buffer on Ctrl+C if it has text', async () => {
|
it('should clear the buffer on Ctrl+C if it has text', async () => {
|
||||||
props.buffer.setText('some text to clear');
|
props.buffer.setText('some text to clear');
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x03'); // Ctrl+C character
|
stdin.write('\x03'); // Ctrl+C character
|
||||||
|
@ -712,7 +722,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
it('should NOT clear the buffer on Ctrl+C if it is empty', async () => {
|
it('should NOT clear the buffer on Ctrl+C if it is empty', async () => {
|
||||||
props.buffer.text = '';
|
props.buffer.text = '';
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x03'); // Ctrl+C character
|
stdin.write('\x03'); // Ctrl+C character
|
||||||
|
@ -735,7 +745,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [{ label: 'Button.tsx', value: 'Button.tsx' }],
|
suggestions: [{ label: 'Button.tsx', value: 'Button.tsx' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
// Verify useCompletion was called with correct signature
|
// Verify useCompletion was called with correct signature
|
||||||
|
@ -763,7 +773,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [{ label: 'show', value: 'show' }],
|
suggestions: [{ label: 'show', value: 'show' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -790,7 +800,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -817,7 +827,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -844,7 +854,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -871,7 +881,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
// Verify useCompletion was called with the buffer
|
// Verify useCompletion was called with the buffer
|
||||||
|
@ -899,7 +909,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [{ label: 'show', value: 'show' }],
|
suggestions: [{ label: 'show', value: 'show' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -927,7 +937,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [{ label: 'file👍.txt', value: 'file👍.txt' }],
|
suggestions: [{ label: 'file👍.txt', value: 'file👍.txt' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -955,7 +965,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -983,7 +993,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [{ label: 'my file.txt', value: 'my file.txt' }],
|
suggestions: [{ label: 'my file.txt', value: 'my file.txt' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -1011,7 +1021,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -1041,7 +1051,7 @@ describe('InputPrompt', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -1069,7 +1079,7 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [{ label: 'test-command', value: 'test-command' }],
|
suggestions: [{ label: 'test-command', value: 'test-command' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -1099,7 +1109,7 @@ describe('InputPrompt', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unmount } = render(<InputPrompt {...props} />);
|
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
|
||||||
|
@ -1120,7 +1130,9 @@ describe('InputPrompt', () => {
|
||||||
it('should not call buffer.handleInput when vim mode is enabled and vim handles the input', async () => {
|
it('should not call buffer.handleInput when vim mode is enabled and vim handles the input', async () => {
|
||||||
props.vimModeEnabled = true;
|
props.vimModeEnabled = true;
|
||||||
props.vimHandleInput = vi.fn().mockReturnValue(true); // Mock that vim handled it.
|
props.vimHandleInput = vi.fn().mockReturnValue(true); // Mock that vim handled it.
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('i');
|
stdin.write('i');
|
||||||
|
@ -1134,7 +1146,9 @@ describe('InputPrompt', () => {
|
||||||
it('should call buffer.handleInput when vim mode is enabled but vim does not handle the input', async () => {
|
it('should call buffer.handleInput when vim mode is enabled but vim does not handle the input', async () => {
|
||||||
props.vimModeEnabled = true;
|
props.vimModeEnabled = true;
|
||||||
props.vimHandleInput = vi.fn().mockReturnValue(false); // Mock that vim did NOT handle it.
|
props.vimHandleInput = vi.fn().mockReturnValue(false); // Mock that vim did NOT handle it.
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('i');
|
stdin.write('i');
|
||||||
|
@ -1148,7 +1162,9 @@ describe('InputPrompt', () => {
|
||||||
it('should call handleInput when vim mode is disabled', async () => {
|
it('should call handleInput when vim mode is disabled', async () => {
|
||||||
// Mock vimHandleInput to return false (vim didn't handle the input)
|
// Mock vimHandleInput to return false (vim didn't handle the input)
|
||||||
props.vimHandleInput = vi.fn().mockReturnValue(false);
|
props.vimHandleInput = vi.fn().mockReturnValue(false);
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('i');
|
stdin.write('i');
|
||||||
|
@ -1163,7 +1179,9 @@ describe('InputPrompt', () => {
|
||||||
describe('unfocused paste', () => {
|
describe('unfocused paste', () => {
|
||||||
it('should handle bracketed paste when not focused', async () => {
|
it('should handle bracketed paste when not focused', async () => {
|
||||||
props.focus = false;
|
props.focus = false;
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x1B[200~pasted text\x1B[201~');
|
stdin.write('\x1B[200~pasted text\x1B[201~');
|
||||||
|
@ -1180,7 +1198,9 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
it('should ignore regular keypresses when not focused', async () => {
|
it('should ignore regular keypresses when not focused', async () => {
|
||||||
props.focus = false;
|
props.focus = false;
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('a');
|
stdin.write('a');
|
||||||
|
@ -1197,7 +1217,9 @@ describe('InputPrompt', () => {
|
||||||
props.onEscapePromptChange = onEscapePromptChange;
|
props.onEscapePromptChange = onEscapePromptChange;
|
||||||
props.buffer.setText('text to clear');
|
props.buffer.setText('text to clear');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x1B');
|
stdin.write('\x1B');
|
||||||
|
@ -1216,7 +1238,9 @@ describe('InputPrompt', () => {
|
||||||
props.onEscapePromptChange = onEscapePromptChange;
|
props.onEscapePromptChange = onEscapePromptChange;
|
||||||
props.buffer.setText('some text');
|
props.buffer.setText('some text');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
|
|
||||||
stdin.write('\x1B');
|
stdin.write('\x1B');
|
||||||
|
|
||||||
|
@ -1235,7 +1259,9 @@ describe('InputPrompt', () => {
|
||||||
it('should handle ESC in shell mode by disabling shell mode', async () => {
|
it('should handle ESC in shell mode by disabling shell mode', async () => {
|
||||||
props.shellModeActive = true;
|
props.shellModeActive = true;
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x1B');
|
stdin.write('\x1B');
|
||||||
|
@ -1252,7 +1278,9 @@ describe('InputPrompt', () => {
|
||||||
suggestions: [{ label: 'suggestion', value: 'suggestion' }],
|
suggestions: [{ label: 'suggestion', value: 'suggestion' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x1B');
|
stdin.write('\x1B');
|
||||||
|
@ -1266,7 +1294,9 @@ describe('InputPrompt', () => {
|
||||||
props.onEscapePromptChange = undefined;
|
props.onEscapePromptChange = undefined;
|
||||||
props.buffer.setText('some text');
|
props.buffer.setText('some text');
|
||||||
|
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x1B');
|
stdin.write('\x1B');
|
||||||
|
@ -1276,7 +1306,9 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not interfere with existing keyboard shortcuts', async () => {
|
it('should not interfere with existing keyboard shortcuts', async () => {
|
||||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x0C');
|
stdin.write('\x0C');
|
||||||
|
@ -1306,7 +1338,9 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invokes reverse search on Ctrl+R', async () => {
|
it('invokes reverse search on Ctrl+R', async () => {
|
||||||
const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, stdout, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x12');
|
stdin.write('\x12');
|
||||||
|
@ -1322,7 +1356,9 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resets reverse search state on Escape', async () => {
|
it('resets reverse search state on Escape', async () => {
|
||||||
const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, stdout, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
stdin.write('\x12');
|
stdin.write('\x12');
|
||||||
|
@ -1339,7 +1375,9 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('completes the highlighted entry on Tab and exits reverse-search', async () => {
|
it('completes the highlighted entry on Tab and exits reverse-search', async () => {
|
||||||
const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, stdout, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
stdin.write('\x12');
|
stdin.write('\x12');
|
||||||
await wait();
|
await wait();
|
||||||
stdin.write('\t');
|
stdin.write('\t');
|
||||||
|
@ -1353,7 +1391,9 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submits the highlighted entry on Enter and exits reverse-search', async () => {
|
it('submits the highlighted entry on Enter and exits reverse-search', async () => {
|
||||||
const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, stdout, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
stdin.write('\x12');
|
stdin.write('\x12');
|
||||||
await wait();
|
await wait();
|
||||||
expect(stdout.lastFrame()).toContain('(r:)');
|
expect(stdout.lastFrame()).toContain('(r:)');
|
||||||
|
@ -1370,7 +1410,9 @@ describe('InputPrompt', () => {
|
||||||
it('text and cursor position should be restored after reverse search', async () => {
|
it('text and cursor position should be restored after reverse search', async () => {
|
||||||
props.buffer.setText('initial text');
|
props.buffer.setText('initial text');
|
||||||
props.buffer.cursor = [0, 3];
|
props.buffer.cursor = [0, 3];
|
||||||
const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
|
const { stdin, stdout, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
);
|
||||||
stdin.write('\x12');
|
stdin.write('\x12');
|
||||||
await wait();
|
await wait();
|
||||||
expect(stdout.lastFrame()).toContain('(r:)');
|
expect(stdout.lastFrame()).toContain('(r:)');
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { useShellHistory } from '../hooks/useShellHistory.js';
|
||||||
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
|
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
|
||||||
import { useCommandCompletion } from '../hooks/useCommandCompletion.js';
|
import { useCommandCompletion } from '../hooks/useCommandCompletion.js';
|
||||||
import { useKeypress, Key } from '../hooks/useKeypress.js';
|
import { useKeypress, Key } from '../hooks/useKeypress.js';
|
||||||
import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js';
|
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||||
import { CommandContext, SlashCommand } from '../commands/types.js';
|
import { CommandContext, SlashCommand } from '../commands/types.js';
|
||||||
import { Config } from '@google/gemini-cli-core';
|
import { Config } from '@google/gemini-cli-core';
|
||||||
|
@ -67,7 +66,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
const [escPressCount, setEscPressCount] = useState(0);
|
const [escPressCount, setEscPressCount] = useState(0);
|
||||||
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
||||||
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const kittyProtocolStatus = useKittyKeyboardProtocol();
|
|
||||||
|
|
||||||
const [dirs, setDirs] = useState<readonly string[]>(
|
const [dirs, setDirs] = useState<readonly string[]>(
|
||||||
config.getWorkspaceContext().getDirectories(),
|
config.getWorkspaceContext().getDirectories(),
|
||||||
|
@ -529,8 +527,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
|
|
||||||
useKeypress(handleInput, {
|
useKeypress(handleInput, {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
kittyProtocolEnabled: kittyProtocolStatus.enabled,
|
|
||||||
config,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const linesToRender = buffer.viewportVisualLines;
|
const linesToRender = buffer.viewportVisualLines;
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from 'ink-testing-library';
|
|
||||||
import { waitFor } from '@testing-library/react';
|
import { waitFor } from '@testing-library/react';
|
||||||
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { SettingsDialog } from './SettingsDialog.js';
|
import { SettingsDialog } from './SettingsDialog.js';
|
||||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||||
|
@ -102,7 +102,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -194,7 +194,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -212,7 +212,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -227,7 +227,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -242,7 +242,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -280,7 +280,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame, stdin, unmount } = render(
|
const { lastFrame, stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -308,7 +308,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onRestartRequest = vi.fn();
|
const onRestartRequest = vi.fn();
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
|
@ -327,7 +327,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onRestartRequest = vi.fn();
|
const onRestartRequest = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
|
@ -349,7 +349,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -368,7 +368,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings({ vimMode: true });
|
const settings = createMockSettings({ vimMode: true });
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -392,7 +392,7 @@ describe('SettingsDialog', () => {
|
||||||
);
|
);
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -409,7 +409,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -427,7 +427,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -449,7 +449,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -468,7 +468,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<VimModeProvider settings={settings}>
|
<VimModeProvider settings={settings}>
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
</VimModeProvider>,
|
</VimModeProvider>,
|
||||||
|
@ -492,7 +492,7 @@ describe('SettingsDialog', () => {
|
||||||
);
|
);
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -505,7 +505,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -521,7 +521,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -541,7 +541,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -559,7 +559,7 @@ describe('SettingsDialog', () => {
|
||||||
);
|
);
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -576,7 +576,7 @@ describe('SettingsDialog', () => {
|
||||||
);
|
);
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -591,7 +591,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -610,7 +610,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings({ vimMode: true }); // Start with vimMode enabled
|
const settings = createMockSettings({ vimMode: true }); // Start with vimMode enabled
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -626,7 +626,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings({ vimMode: true }); // Start with vimMode enabled
|
const settings = createMockSettings({ vimMode: true }); // Start with vimMode enabled
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -642,7 +642,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -659,7 +659,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame, stdin, unmount } = render(
|
const { lastFrame, stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -692,7 +692,7 @@ describe('SettingsDialog', () => {
|
||||||
);
|
);
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -705,7 +705,7 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
// Should not crash even if some settings are missing definitions
|
// Should not crash even if some settings are missing definitions
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -718,7 +718,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -763,7 +763,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -792,7 +792,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings({ vimMode: true });
|
const settings = createMockSettings({ vimMode: true });
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -816,7 +816,7 @@ describe('SettingsDialog', () => {
|
||||||
const settings = createMockSettings();
|
const settings = createMockSettings();
|
||||||
const onRestartRequest = vi.fn();
|
const onRestartRequest = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = renderWithProviders(
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from 'ink-testing-library';
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { ShellConfirmationDialog } from './ShellConfirmationDialog.js';
|
import { ShellConfirmationDialog } from './ShellConfirmationDialog.js';
|
||||||
|
|
||||||
|
@ -17,12 +17,16 @@ describe('ShellConfirmationDialog', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const { lastFrame } = render(<ShellConfirmationDialog request={request} />);
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<ShellConfirmationDialog request={request} />,
|
||||||
|
);
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onConfirm with ProceedOnce when "Yes, allow once" is selected', () => {
|
it('calls onConfirm with ProceedOnce when "Yes, allow once" is selected', () => {
|
||||||
const { lastFrame } = render(<ShellConfirmationDialog request={request} />);
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<ShellConfirmationDialog request={request} />,
|
||||||
|
);
|
||||||
const select = lastFrame()!.toString();
|
const select = lastFrame()!.toString();
|
||||||
// Simulate selecting the first option
|
// Simulate selecting the first option
|
||||||
// This is a simplified way to test the selection
|
// This is a simplified way to test the selection
|
||||||
|
@ -30,14 +34,18 @@ describe('ShellConfirmationDialog', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onConfirm with ProceedAlways when "Yes, allow always for this session" is selected', () => {
|
it('calls onConfirm with ProceedAlways when "Yes, allow always for this session" is selected', () => {
|
||||||
const { lastFrame } = render(<ShellConfirmationDialog request={request} />);
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<ShellConfirmationDialog request={request} />,
|
||||||
|
);
|
||||||
const select = lastFrame()!.toString();
|
const select = lastFrame()!.toString();
|
||||||
// Simulate selecting the second option
|
// Simulate selecting the second option
|
||||||
expect(select).toContain('Yes, allow always for this session');
|
expect(select).toContain('Yes, allow always for this session');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onConfirm with Cancel when "No (esc)" is selected', () => {
|
it('calls onConfirm with Cancel when "No (esc)" is selected', () => {
|
||||||
const { lastFrame } = render(<ShellConfirmationDialog request={request} />);
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<ShellConfirmationDialog request={request} />,
|
||||||
|
);
|
||||||
const select = lastFrame()!.toString();
|
const select = lastFrame()!.toString();
|
||||||
// Simulate selecting the third option
|
// Simulate selecting the third option
|
||||||
expect(select).toContain('No (esc)');
|
expect(select).toContain('No (esc)');
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from 'ink-testing-library';
|
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||||
import { ToolCallConfirmationDetails } from '@google/gemini-cli-core';
|
import { ToolCallConfirmationDetails } from '@google/gemini-cli-core';
|
||||||
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
|
|
||||||
describe('ToolConfirmationMessage', () => {
|
describe('ToolConfirmationMessage', () => {
|
||||||
it('should not display urls if prompt and url are the same', () => {
|
it('should not display urls if prompt and url are the same', () => {
|
||||||
|
@ -19,7 +19,7 @@ describe('ToolConfirmationMessage', () => {
|
||||||
onConfirm: vi.fn(),
|
onConfirm: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<ToolConfirmationMessage
|
<ToolConfirmationMessage
|
||||||
confirmationDetails={confirmationDetails}
|
confirmationDetails={confirmationDetails}
|
||||||
availableTerminalHeight={30}
|
availableTerminalHeight={30}
|
||||||
|
@ -42,7 +42,7 @@ describe('ToolConfirmationMessage', () => {
|
||||||
onConfirm: vi.fn(),
|
onConfirm: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<ToolConfirmationMessage
|
<ToolConfirmationMessage
|
||||||
confirmationDetails={confirmationDetails}
|
confirmationDetails={confirmationDetails}
|
||||||
availableTerminalHeight={30}
|
availableTerminalHeight={30}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from 'ink-testing-library';
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
import { waitFor } from '@testing-library/react';
|
import { waitFor } from '@testing-library/react';
|
||||||
import {
|
import {
|
||||||
RadioButtonSelect,
|
RadioButtonSelect,
|
||||||
|
@ -20,21 +20,21 @@ const ITEMS: Array<RadioSelectItem<string>> = [
|
||||||
|
|
||||||
describe('<RadioButtonSelect />', () => {
|
describe('<RadioButtonSelect />', () => {
|
||||||
it('renders a list of items and matches snapshot', () => {
|
it('renders a list of items and matches snapshot', () => {
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<RadioButtonSelect items={ITEMS} onSelect={() => {}} isFocused={true} />,
|
<RadioButtonSelect items={ITEMS} onSelect={() => {}} isFocused={true} />,
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with the second item selected and matches snapshot', () => {
|
it('renders with the second item selected and matches snapshot', () => {
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<RadioButtonSelect items={ITEMS} initialIndex={1} onSelect={() => {}} />,
|
<RadioButtonSelect items={ITEMS} initialIndex={1} onSelect={() => {}} />,
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with numbers hidden and matches snapshot', () => {
|
it('renders with numbers hidden and matches snapshot', () => {
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<RadioButtonSelect
|
<RadioButtonSelect
|
||||||
items={ITEMS}
|
items={ITEMS}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
|
@ -49,7 +49,7 @@ describe('<RadioButtonSelect />', () => {
|
||||||
label: `Item ${i + 1}`,
|
label: `Item ${i + 1}`,
|
||||||
value: `item-${i + 1}`,
|
value: `item-${i + 1}`,
|
||||||
}));
|
}));
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<RadioButtonSelect
|
<RadioButtonSelect
|
||||||
items={manyItems}
|
items={manyItems}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
|
@ -75,7 +75,7 @@ describe('<RadioButtonSelect />', () => {
|
||||||
themeTypeDisplay: '(Dark)',
|
themeTypeDisplay: '(Dark)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<RadioButtonSelect items={themeItems} onSelect={() => {}} />,
|
<RadioButtonSelect items={themeItems} onSelect={() => {}} />,
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
@ -86,14 +86,14 @@ describe('<RadioButtonSelect />', () => {
|
||||||
label: `Item ${i + 1}`,
|
label: `Item ${i + 1}`,
|
||||||
value: `item-${i + 1}`,
|
value: `item-${i + 1}`,
|
||||||
}));
|
}));
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<RadioButtonSelect items={manyItems} onSelect={() => {}} />,
|
<RadioButtonSelect items={manyItems} onSelect={() => {}} />,
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders nothing when no items are provided', () => {
|
it('renders nothing when no items are provided', () => {
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<RadioButtonSelect items={[]} onSelect={() => {}} isFocused={true} />,
|
<RadioButtonSelect items={[]} onSelect={() => {}} isFocused={true} />,
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toBe('');
|
expect(lastFrame()).toBe('');
|
||||||
|
@ -103,7 +103,7 @@ describe('<RadioButtonSelect />', () => {
|
||||||
describe('keyboard navigation', () => {
|
describe('keyboard navigation', () => {
|
||||||
it('should call onSelect when "enter" is pressed', () => {
|
it('should call onSelect when "enter" is pressed', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
const { stdin } = render(
|
const { stdin } = renderWithProviders(
|
||||||
<RadioButtonSelect items={ITEMS} onSelect={onSelect} />,
|
<RadioButtonSelect items={ITEMS} onSelect={onSelect} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ describe('keyboard navigation', () => {
|
||||||
describe('when isFocused is false', () => {
|
describe('when isFocused is false', () => {
|
||||||
it('should not handle any keyboard input', () => {
|
it('should not handle any keyboard input', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
const { stdin } = render(
|
const { stdin } = renderWithProviders(
|
||||||
<RadioButtonSelect
|
<RadioButtonSelect
|
||||||
items={ITEMS}
|
items={ITEMS}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
@ -137,7 +137,7 @@ describe('keyboard navigation', () => {
|
||||||
])('$description', ({ isFocused }) => {
|
])('$description', ({ isFocused }) => {
|
||||||
it('should navigate down with arrow key and select with enter', async () => {
|
it('should navigate down with arrow key and select with enter', async () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
const { stdin, lastFrame } = render(
|
const { stdin, lastFrame } = renderWithProviders(
|
||||||
<RadioButtonSelect
|
<RadioButtonSelect
|
||||||
items={ITEMS}
|
items={ITEMS}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
@ -158,7 +158,7 @@ describe('keyboard navigation', () => {
|
||||||
|
|
||||||
it('should navigate up with arrow key and select with enter', async () => {
|
it('should navigate up with arrow key and select with enter', async () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
const { stdin, lastFrame } = render(
|
const { stdin, lastFrame } = renderWithProviders(
|
||||||
<RadioButtonSelect
|
<RadioButtonSelect
|
||||||
items={ITEMS}
|
items={ITEMS}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
|
|
@ -0,0 +1,405 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Config,
|
||||||
|
KittySequenceOverflowEvent,
|
||||||
|
logKittySequenceOverflow,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
|
import { useStdin } from 'ink';
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import readline from 'readline';
|
||||||
|
import { PassThrough } from 'stream';
|
||||||
|
import {
|
||||||
|
BACKSLASH_ENTER_DETECTION_WINDOW_MS,
|
||||||
|
KITTY_CTRL_C,
|
||||||
|
MAX_KITTY_SEQUENCE_LENGTH,
|
||||||
|
} from '../utils/platformConstants.js';
|
||||||
|
|
||||||
|
import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
|
||||||
|
|
||||||
|
const ESC = '\u001B';
|
||||||
|
export const PASTE_MODE_PREFIX = `${ESC}[200~`;
|
||||||
|
export const PASTE_MODE_SUFFIX = `${ESC}[201~`;
|
||||||
|
|
||||||
|
export interface Key {
|
||||||
|
name: string;
|
||||||
|
ctrl: boolean;
|
||||||
|
meta: boolean;
|
||||||
|
shift: boolean;
|
||||||
|
paste: boolean;
|
||||||
|
sequence: string;
|
||||||
|
kittyProtocol?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KeypressHandler = (key: Key) => void;
|
||||||
|
|
||||||
|
interface KeypressContextValue {
|
||||||
|
subscribe: (handler: KeypressHandler) => void;
|
||||||
|
unsubscribe: (handler: KeypressHandler) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeypressContext = createContext<KeypressContextValue | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useKeypressContext() {
|
||||||
|
const context = useContext(KeypressContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useKeypressContext must be used within a KeypressProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KeypressProvider({
|
||||||
|
children,
|
||||||
|
kittyProtocolEnabled,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
kittyProtocolEnabled: boolean;
|
||||||
|
config?: Config;
|
||||||
|
}) {
|
||||||
|
const { stdin, setRawMode } = useStdin();
|
||||||
|
const subscribers = useRef<Set<KeypressHandler>>(new Set()).current;
|
||||||
|
|
||||||
|
const subscribe = useCallback(
|
||||||
|
(handler: KeypressHandler) => {
|
||||||
|
subscribers.add(handler);
|
||||||
|
},
|
||||||
|
[subscribers],
|
||||||
|
);
|
||||||
|
|
||||||
|
const unsubscribe = useCallback(
|
||||||
|
(handler: KeypressHandler) => {
|
||||||
|
subscribers.delete(handler);
|
||||||
|
},
|
||||||
|
[subscribers],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRawMode(true);
|
||||||
|
|
||||||
|
const keypressStream = new PassThrough();
|
||||||
|
let usePassthrough = false;
|
||||||
|
const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10);
|
||||||
|
if (
|
||||||
|
nodeMajorVersion < 20 ||
|
||||||
|
process.env['PASTE_WORKAROUND'] === '1' ||
|
||||||
|
process.env['PASTE_WORKAROUND'] === 'true'
|
||||||
|
) {
|
||||||
|
usePassthrough = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isPaste = false;
|
||||||
|
let pasteBuffer = Buffer.alloc(0);
|
||||||
|
let kittySequenceBuffer = '';
|
||||||
|
let backslashTimeout: NodeJS.Timeout | null = null;
|
||||||
|
let waitingForEnterAfterBackslash = false;
|
||||||
|
|
||||||
|
const parseKittySequence = (sequence: string): Key | null => {
|
||||||
|
const kittyPattern = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])$`);
|
||||||
|
const match = sequence.match(kittyPattern);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const keyCode = parseInt(match[1], 10);
|
||||||
|
const modifiers = match[3] ? parseInt(match[3], 10) : 1;
|
||||||
|
const modifierBits = modifiers - 1;
|
||||||
|
const shift = (modifierBits & 1) === 1;
|
||||||
|
const alt = (modifierBits & 2) === 2;
|
||||||
|
const ctrl = (modifierBits & 4) === 4;
|
||||||
|
|
||||||
|
if (keyCode === 27) {
|
||||||
|
return {
|
||||||
|
name: 'escape',
|
||||||
|
ctrl,
|
||||||
|
meta: alt,
|
||||||
|
shift,
|
||||||
|
paste: false,
|
||||||
|
sequence,
|
||||||
|
kittyProtocol: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode === 13) {
|
||||||
|
return {
|
||||||
|
name: 'return',
|
||||||
|
ctrl,
|
||||||
|
meta: alt,
|
||||||
|
shift,
|
||||||
|
paste: false,
|
||||||
|
sequence,
|
||||||
|
kittyProtocol: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode >= 97 && keyCode <= 122 && ctrl) {
|
||||||
|
const letter = String.fromCharCode(keyCode);
|
||||||
|
return {
|
||||||
|
name: letter,
|
||||||
|
ctrl: true,
|
||||||
|
meta: alt,
|
||||||
|
shift,
|
||||||
|
paste: false,
|
||||||
|
sequence,
|
||||||
|
kittyProtocol: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const broadcast = (key: Key) => {
|
||||||
|
for (const handler of subscribers) {
|
||||||
|
handler(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeypress = (_: unknown, key: Key) => {
|
||||||
|
if (key.name === 'return' && waitingForEnterAfterBackslash) {
|
||||||
|
if (backslashTimeout) {
|
||||||
|
clearTimeout(backslashTimeout);
|
||||||
|
backslashTimeout = null;
|
||||||
|
}
|
||||||
|
waitingForEnterAfterBackslash = false;
|
||||||
|
broadcast({
|
||||||
|
...key,
|
||||||
|
shift: true,
|
||||||
|
sequence: '\r', // Corrected escaping for newline
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.sequence === '\\' && !key.name) {
|
||||||
|
// Corrected escaping for backslash
|
||||||
|
waitingForEnterAfterBackslash = true;
|
||||||
|
backslashTimeout = setTimeout(() => {
|
||||||
|
waitingForEnterAfterBackslash = false;
|
||||||
|
backslashTimeout = null;
|
||||||
|
broadcast(key);
|
||||||
|
}, BACKSLASH_ENTER_DETECTION_WINDOW_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitingForEnterAfterBackslash && key.name !== 'return') {
|
||||||
|
if (backslashTimeout) {
|
||||||
|
clearTimeout(backslashTimeout);
|
||||||
|
backslashTimeout = null;
|
||||||
|
}
|
||||||
|
waitingForEnterAfterBackslash = false;
|
||||||
|
broadcast({
|
||||||
|
name: '',
|
||||||
|
sequence: '\\',
|
||||||
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
shift: false,
|
||||||
|
paste: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['up', 'down', 'left', 'right'].includes(key.name)) {
|
||||||
|
broadcast(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(key.ctrl && key.name === 'c') ||
|
||||||
|
key.sequence === `${ESC}${KITTY_CTRL_C}`
|
||||||
|
) {
|
||||||
|
kittySequenceBuffer = '';
|
||||||
|
if (key.sequence === `${ESC}${KITTY_CTRL_C}`) {
|
||||||
|
broadcast({
|
||||||
|
name: 'c',
|
||||||
|
ctrl: true,
|
||||||
|
meta: false,
|
||||||
|
shift: false,
|
||||||
|
paste: false,
|
||||||
|
sequence: key.sequence,
|
||||||
|
kittyProtocol: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
broadcast(key);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kittyProtocolEnabled) {
|
||||||
|
if (
|
||||||
|
kittySequenceBuffer ||
|
||||||
|
(key.sequence.startsWith(`${ESC}[`) &&
|
||||||
|
!key.sequence.startsWith(PASTE_MODE_PREFIX) &&
|
||||||
|
!key.sequence.startsWith(PASTE_MODE_SUFFIX) &&
|
||||||
|
!key.sequence.startsWith(FOCUS_IN) &&
|
||||||
|
!key.sequence.startsWith(FOCUS_OUT))
|
||||||
|
) {
|
||||||
|
kittySequenceBuffer += key.sequence;
|
||||||
|
const kittyKey = parseKittySequence(kittySequenceBuffer);
|
||||||
|
if (kittyKey) {
|
||||||
|
kittySequenceBuffer = '';
|
||||||
|
broadcast(kittyKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config?.getDebugMode()) {
|
||||||
|
const codes = Array.from(kittySequenceBuffer).map((ch) =>
|
||||||
|
ch.charCodeAt(0),
|
||||||
|
);
|
||||||
|
console.warn('Kitty sequence buffer has char codes:', codes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) {
|
||||||
|
if (config) {
|
||||||
|
const event = new KittySequenceOverflowEvent(
|
||||||
|
kittySequenceBuffer.length,
|
||||||
|
kittySequenceBuffer,
|
||||||
|
);
|
||||||
|
logKittySequenceOverflow(config, event);
|
||||||
|
}
|
||||||
|
kittySequenceBuffer = '';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.name === 'paste-start') {
|
||||||
|
isPaste = true;
|
||||||
|
} else if (key.name === 'paste-end') {
|
||||||
|
isPaste = false;
|
||||||
|
broadcast({
|
||||||
|
name: '',
|
||||||
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
shift: false,
|
||||||
|
paste: true,
|
||||||
|
sequence: pasteBuffer.toString(),
|
||||||
|
});
|
||||||
|
pasteBuffer = Buffer.alloc(0);
|
||||||
|
} else {
|
||||||
|
if (isPaste) {
|
||||||
|
pasteBuffer = Buffer.concat([pasteBuffer, Buffer.from(key.sequence)]);
|
||||||
|
} else {
|
||||||
|
if (key.name === 'return' && key.sequence === `${ESC}\r`) {
|
||||||
|
key.meta = true;
|
||||||
|
}
|
||||||
|
broadcast({ ...key, paste: isPaste });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRawKeypress = (data: Buffer) => {
|
||||||
|
const pasteModePrefixBuffer = Buffer.from(PASTE_MODE_PREFIX);
|
||||||
|
const pasteModeSuffixBuffer = Buffer.from(PASTE_MODE_SUFFIX);
|
||||||
|
|
||||||
|
let pos = 0;
|
||||||
|
while (pos < data.length) {
|
||||||
|
const prefixPos = data.indexOf(pasteModePrefixBuffer, pos);
|
||||||
|
const suffixPos = data.indexOf(pasteModeSuffixBuffer, pos);
|
||||||
|
const isPrefixNext =
|
||||||
|
prefixPos !== -1 && (suffixPos === -1 || prefixPos < suffixPos);
|
||||||
|
const isSuffixNext =
|
||||||
|
suffixPos !== -1 && (prefixPos === -1 || suffixPos < prefixPos);
|
||||||
|
|
||||||
|
let nextMarkerPos = -1;
|
||||||
|
let markerLength = 0;
|
||||||
|
|
||||||
|
if (isPrefixNext) {
|
||||||
|
nextMarkerPos = prefixPos;
|
||||||
|
} else if (isSuffixNext) {
|
||||||
|
nextMarkerPos = suffixPos;
|
||||||
|
}
|
||||||
|
markerLength = pasteModeSuffixBuffer.length;
|
||||||
|
|
||||||
|
if (nextMarkerPos === -1) {
|
||||||
|
keypressStream.write(data.slice(pos));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextData = data.slice(pos, nextMarkerPos);
|
||||||
|
if (nextData.length > 0) {
|
||||||
|
keypressStream.write(nextData);
|
||||||
|
}
|
||||||
|
const createPasteKeyEvent = (
|
||||||
|
name: 'paste-start' | 'paste-end',
|
||||||
|
): Key => ({
|
||||||
|
name,
|
||||||
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
shift: false,
|
||||||
|
paste: false,
|
||||||
|
sequence: '',
|
||||||
|
});
|
||||||
|
if (isPrefixNext) {
|
||||||
|
handleKeypress(undefined, createPasteKeyEvent('paste-start'));
|
||||||
|
} else if (isSuffixNext) {
|
||||||
|
handleKeypress(undefined, createPasteKeyEvent('paste-end'));
|
||||||
|
}
|
||||||
|
pos = nextMarkerPos + markerLength;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let rl: readline.Interface;
|
||||||
|
if (usePassthrough) {
|
||||||
|
rl = readline.createInterface({
|
||||||
|
input: keypressStream,
|
||||||
|
escapeCodeTimeout: 0,
|
||||||
|
});
|
||||||
|
readline.emitKeypressEvents(keypressStream, rl);
|
||||||
|
keypressStream.on('keypress', handleKeypress);
|
||||||
|
stdin.on('data', handleRawKeypress);
|
||||||
|
} else {
|
||||||
|
rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 0 });
|
||||||
|
readline.emitKeypressEvents(stdin, rl);
|
||||||
|
stdin.on('keypress', handleKeypress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (usePassthrough) {
|
||||||
|
keypressStream.removeListener('keypress', handleKeypress);
|
||||||
|
stdin.removeListener('data', handleRawKeypress);
|
||||||
|
} else {
|
||||||
|
stdin.removeListener('keypress', handleKeypress);
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
// Restore the terminal to its original state.
|
||||||
|
setRawMode(false);
|
||||||
|
|
||||||
|
if (backslashTimeout) {
|
||||||
|
clearTimeout(backslashTimeout);
|
||||||
|
backslashTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any pending paste data to avoid data loss on exit.
|
||||||
|
if (isPaste) {
|
||||||
|
broadcast({
|
||||||
|
name: '',
|
||||||
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
shift: false,
|
||||||
|
paste: true,
|
||||||
|
sequence: pasteBuffer.toString(),
|
||||||
|
});
|
||||||
|
pasteBuffer = Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [stdin, setRawMode, kittyProtocolEnabled, config, subscribers]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeypressContext.Provider value={{ subscribe, unsubscribe }}>
|
||||||
|
{children}
|
||||||
|
</KeypressContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,8 +4,10 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import { renderHook, act } from '@testing-library/react';
|
import { renderHook, act } from '@testing-library/react';
|
||||||
import { useKeypress, Key } from './useKeypress.js';
|
import { useKeypress, Key } from './useKeypress.js';
|
||||||
|
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||||
import { useStdin } from 'ink';
|
import { useStdin } from 'ink';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { PassThrough } from 'stream';
|
import { PassThrough } from 'stream';
|
||||||
|
@ -102,6 +104,9 @@ describe('useKeypress', () => {
|
||||||
const onKeypress = vi.fn();
|
const onKeypress = vi.fn();
|
||||||
let originalNodeVersion: string;
|
let originalNodeVersion: string;
|
||||||
|
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement(KeypressProvider, null, children);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
stdin = new MockStdin();
|
stdin = new MockStdin();
|
||||||
|
@ -129,7 +134,9 @@ describe('useKeypress', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should not listen if isActive is false', () => {
|
it('should not listen if isActive is false', () => {
|
||||||
renderHook(() => useKeypress(onKeypress, { isActive: false }));
|
renderHook(() => useKeypress(onKeypress, { isActive: false }), {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
act(() => stdin.pressKey({ name: 'a' }));
|
act(() => stdin.pressKey({ name: 'a' }));
|
||||||
expect(onKeypress).not.toHaveBeenCalled();
|
expect(onKeypress).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -141,14 +148,15 @@ describe('useKeypress', () => {
|
||||||
{ key: { name: 'up', sequence: '\x1b[A' } },
|
{ key: { name: 'up', sequence: '\x1b[A' } },
|
||||||
{ key: { name: 'down', sequence: '\x1b[B' } },
|
{ key: { name: 'down', sequence: '\x1b[B' } },
|
||||||
])('should listen for keypress when active for key $key.name', ({ key }) => {
|
])('should listen for keypress when active for key $key.name', ({ key }) => {
|
||||||
renderHook(() => useKeypress(onKeypress, { isActive: true }));
|
renderHook(() => useKeypress(onKeypress, { isActive: true }), { wrapper });
|
||||||
act(() => stdin.pressKey(key));
|
act(() => stdin.pressKey(key));
|
||||||
expect(onKeypress).toHaveBeenCalledWith(expect.objectContaining(key));
|
expect(onKeypress).toHaveBeenCalledWith(expect.objectContaining(key));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set and release raw mode', () => {
|
it('should set and release raw mode', () => {
|
||||||
const { unmount } = renderHook(() =>
|
const { unmount } = renderHook(
|
||||||
useKeypress(onKeypress, { isActive: true }),
|
() => useKeypress(onKeypress, { isActive: true }),
|
||||||
|
{ wrapper },
|
||||||
);
|
);
|
||||||
expect(mockSetRawMode).toHaveBeenCalledWith(true);
|
expect(mockSetRawMode).toHaveBeenCalledWith(true);
|
||||||
unmount();
|
unmount();
|
||||||
|
@ -156,8 +164,9 @@ describe('useKeypress', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should stop listening after being unmounted', () => {
|
it('should stop listening after being unmounted', () => {
|
||||||
const { unmount } = renderHook(() =>
|
const { unmount } = renderHook(
|
||||||
useKeypress(onKeypress, { isActive: true }),
|
() => useKeypress(onKeypress, { isActive: true }),
|
||||||
|
{ wrapper },
|
||||||
);
|
);
|
||||||
unmount();
|
unmount();
|
||||||
act(() => stdin.pressKey({ name: 'a' }));
|
act(() => stdin.pressKey({ name: 'a' }));
|
||||||
|
@ -165,7 +174,7 @@ describe('useKeypress', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly identify alt+enter (meta key)', () => {
|
it('should correctly identify alt+enter (meta key)', () => {
|
||||||
renderHook(() => useKeypress(onKeypress, { isActive: true }));
|
renderHook(() => useKeypress(onKeypress, { isActive: true }), { wrapper });
|
||||||
const key = { name: 'return', sequence: '\x1B\r' };
|
const key = { name: 'return', sequence: '\x1B\r' };
|
||||||
act(() => stdin.pressKey(key));
|
act(() => stdin.pressKey(key));
|
||||||
expect(onKeypress).toHaveBeenCalledWith(
|
expect(onKeypress).toHaveBeenCalledWith(
|
||||||
|
@ -199,7 +208,9 @@ describe('useKeypress', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process a paste as a single event', () => {
|
it('should process a paste as a single event', () => {
|
||||||
renderHook(() => useKeypress(onKeypress, { isActive: true }));
|
renderHook(() => useKeypress(onKeypress, { isActive: true }), {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
const pasteText = 'hello world';
|
const pasteText = 'hello world';
|
||||||
act(() => stdin.paste(pasteText));
|
act(() => stdin.paste(pasteText));
|
||||||
|
|
||||||
|
@ -215,7 +226,9 @@ describe('useKeypress', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle keypress interspersed with pastes', () => {
|
it('should handle keypress interspersed with pastes', () => {
|
||||||
renderHook(() => useKeypress(onKeypress, { isActive: true }));
|
renderHook(() => useKeypress(onKeypress, { isActive: true }), {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
const keyA = { name: 'a', sequence: 'a' };
|
const keyA = { name: 'a', sequence: 'a' };
|
||||||
act(() => stdin.pressKey(keyA));
|
act(() => stdin.pressKey(keyA));
|
||||||
|
@ -239,8 +252,9 @@ describe('useKeypress', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit partial paste content if unmounted mid-paste', () => {
|
it('should emit partial paste content if unmounted mid-paste', () => {
|
||||||
const { unmount } = renderHook(() =>
|
const { unmount } = renderHook(
|
||||||
useKeypress(onKeypress, { isActive: true }),
|
() => useKeypress(onKeypress, { isActive: true }),
|
||||||
|
{ wrapper },
|
||||||
);
|
);
|
||||||
const pasteText = 'incomplete paste';
|
const pasteText = 'incomplete paste';
|
||||||
|
|
||||||
|
|
|
@ -4,414 +4,36 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useStdin } from 'ink';
|
|
||||||
import readline from 'readline';
|
|
||||||
import { PassThrough } from 'stream';
|
|
||||||
import {
|
import {
|
||||||
KITTY_CTRL_C,
|
useKeypressContext,
|
||||||
BACKSLASH_ENTER_DETECTION_WINDOW_MS,
|
KeypressHandler,
|
||||||
MAX_KITTY_SEQUENCE_LENGTH,
|
Key,
|
||||||
} from '../utils/platformConstants.js';
|
} from '../contexts/KeypressContext.js';
|
||||||
import {
|
|
||||||
KittySequenceOverflowEvent,
|
|
||||||
logKittySequenceOverflow,
|
|
||||||
Config,
|
|
||||||
} from '@google/gemini-cli-core';
|
|
||||||
import { FOCUS_IN, FOCUS_OUT } from './useFocus.js';
|
|
||||||
|
|
||||||
const ESC = '\u001B';
|
export { Key };
|
||||||
export const PASTE_MODE_PREFIX = `${ESC}[200~`;
|
|
||||||
export const PASTE_MODE_SUFFIX = `${ESC}[201~`;
|
|
||||||
|
|
||||||
export interface Key {
|
|
||||||
name: string;
|
|
||||||
ctrl: boolean;
|
|
||||||
meta: boolean;
|
|
||||||
shift: boolean;
|
|
||||||
paste: boolean;
|
|
||||||
sequence: string;
|
|
||||||
kittyProtocol?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook that listens for keypress events from stdin, providing a
|
* A hook that listens for keypress events from stdin.
|
||||||
* key object that mirrors the one from Node's `readline` module,
|
|
||||||
* adding a 'paste' flag for characters input as part of a bracketed
|
|
||||||
* paste (when enabled).
|
|
||||||
*
|
|
||||||
* Pastes are currently sent as a single key event where the full paste
|
|
||||||
* is in the sequence field.
|
|
||||||
*
|
*
|
||||||
* @param onKeypress - The callback function to execute on each keypress.
|
* @param onKeypress - The callback function to execute on each keypress.
|
||||||
* @param options - Options to control the hook's behavior.
|
* @param options - Options to control the hook's behavior.
|
||||||
* @param options.isActive - Whether the hook should be actively listening for input.
|
* @param options.isActive - Whether the hook should be actively listening for input.
|
||||||
* @param options.kittyProtocolEnabled - Whether Kitty keyboard protocol is enabled.
|
|
||||||
* @param options.config - Optional config for telemetry logging.
|
|
||||||
*/
|
*/
|
||||||
export function useKeypress(
|
export function useKeypress(
|
||||||
onKeypress: (key: Key) => void,
|
onKeypress: KeypressHandler,
|
||||||
{
|
{ isActive }: { isActive: boolean },
|
||||||
isActive,
|
|
||||||
kittyProtocolEnabled = false,
|
|
||||||
config,
|
|
||||||
}: { isActive: boolean; kittyProtocolEnabled?: boolean; config?: Config },
|
|
||||||
) {
|
) {
|
||||||
const { stdin, setRawMode } = useStdin();
|
const { subscribe, unsubscribe } = useKeypressContext();
|
||||||
const onKeypressRef = useRef(onKeypress);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onKeypressRef.current = onKeypress;
|
if (!isActive) {
|
||||||
}, [onKeypress]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isActive || !stdin.isTTY) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRawMode(true);
|
subscribe(onKeypress);
|
||||||
|
|
||||||
const keypressStream = new PassThrough();
|
|
||||||
let usePassthrough = false;
|
|
||||||
const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10);
|
|
||||||
if (
|
|
||||||
nodeMajorVersion < 20 ||
|
|
||||||
process.env['PASTE_WORKAROUND'] === '1' ||
|
|
||||||
process.env['PASTE_WORKAROUND'] === 'true'
|
|
||||||
) {
|
|
||||||
// Prior to node 20, node's built-in readline does not support bracketed
|
|
||||||
// paste mode. We hack by detecting it with our own handler.
|
|
||||||
usePassthrough = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isPaste = false;
|
|
||||||
let pasteBuffer = Buffer.alloc(0);
|
|
||||||
let kittySequenceBuffer = '';
|
|
||||||
let backslashTimeout: NodeJS.Timeout | null = null;
|
|
||||||
let waitingForEnterAfterBackslash = false;
|
|
||||||
|
|
||||||
// Parse Kitty protocol sequences
|
|
||||||
const parseKittySequence = (sequence: string): Key | null => {
|
|
||||||
// Match CSI <number> ; <modifiers> u or ~
|
|
||||||
// Format: ESC [ <keycode> ; <modifiers> u/~
|
|
||||||
const kittyPattern = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])$`);
|
|
||||||
const match = sequence.match(kittyPattern);
|
|
||||||
if (!match) return null;
|
|
||||||
|
|
||||||
const keyCode = parseInt(match[1], 10);
|
|
||||||
const modifiers = match[3] ? parseInt(match[3], 10) : 1;
|
|
||||||
|
|
||||||
// Decode modifiers (subtract 1 as per Kitty protocol spec)
|
|
||||||
const modifierBits = modifiers - 1;
|
|
||||||
const shift = (modifierBits & 1) === 1;
|
|
||||||
const alt = (modifierBits & 2) === 2;
|
|
||||||
const ctrl = (modifierBits & 4) === 4;
|
|
||||||
|
|
||||||
// Handle Escape key (code 27)
|
|
||||||
if (keyCode === 27) {
|
|
||||||
return {
|
|
||||||
name: 'escape',
|
|
||||||
ctrl,
|
|
||||||
meta: alt,
|
|
||||||
shift,
|
|
||||||
paste: false,
|
|
||||||
sequence,
|
|
||||||
kittyProtocol: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Enter key (code 13)
|
|
||||||
if (keyCode === 13) {
|
|
||||||
return {
|
|
||||||
name: 'return',
|
|
||||||
ctrl,
|
|
||||||
meta: alt,
|
|
||||||
shift,
|
|
||||||
paste: false,
|
|
||||||
sequence,
|
|
||||||
kittyProtocol: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Ctrl+letter combinations (a-z)
|
|
||||||
// ASCII codes: a=97, b=98, c=99, ..., z=122
|
|
||||||
if (keyCode >= 97 && keyCode <= 122 && ctrl) {
|
|
||||||
const letter = String.fromCharCode(keyCode);
|
|
||||||
return {
|
|
||||||
name: letter,
|
|
||||||
ctrl: true,
|
|
||||||
meta: alt,
|
|
||||||
shift,
|
|
||||||
paste: false,
|
|
||||||
sequence,
|
|
||||||
kittyProtocol: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle other keys as needed
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeypress = (_: unknown, key: Key) => {
|
|
||||||
// Handle VS Code's backslash+return pattern (Shift+Enter)
|
|
||||||
if (key.name === 'return' && waitingForEnterAfterBackslash) {
|
|
||||||
// Cancel the timeout since we got the Enter
|
|
||||||
if (backslashTimeout) {
|
|
||||||
clearTimeout(backslashTimeout);
|
|
||||||
backslashTimeout = null;
|
|
||||||
}
|
|
||||||
waitingForEnterAfterBackslash = false;
|
|
||||||
|
|
||||||
// Convert to Shift+Enter
|
|
||||||
onKeypressRef.current({
|
|
||||||
...key,
|
|
||||||
shift: true,
|
|
||||||
sequence: '\\\r', // VS Code's Shift+Enter representation
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle backslash - hold it to see if Enter follows
|
|
||||||
if (key.sequence === '\\' && !key.name) {
|
|
||||||
// Don't pass through the backslash yet - wait to see if Enter follows
|
|
||||||
waitingForEnterAfterBackslash = true;
|
|
||||||
|
|
||||||
// Set up a timeout to pass through the backslash if no Enter follows
|
|
||||||
backslashTimeout = setTimeout(() => {
|
|
||||||
waitingForEnterAfterBackslash = false;
|
|
||||||
backslashTimeout = null;
|
|
||||||
// Pass through the backslash since no Enter followed
|
|
||||||
onKeypressRef.current(key);
|
|
||||||
}, BACKSLASH_ENTER_DETECTION_WINDOW_MS);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're waiting for Enter after backslash but got something else,
|
|
||||||
// pass through the backslash first, then the new key
|
|
||||||
if (waitingForEnterAfterBackslash && key.name !== 'return') {
|
|
||||||
if (backslashTimeout) {
|
|
||||||
clearTimeout(backslashTimeout);
|
|
||||||
backslashTimeout = null;
|
|
||||||
}
|
|
||||||
waitingForEnterAfterBackslash = false;
|
|
||||||
|
|
||||||
// Pass through the backslash that was held
|
|
||||||
onKeypressRef.current({
|
|
||||||
name: '',
|
|
||||||
sequence: '\\',
|
|
||||||
ctrl: false,
|
|
||||||
meta: false,
|
|
||||||
shift: false,
|
|
||||||
paste: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then continue processing the current key normally
|
|
||||||
}
|
|
||||||
|
|
||||||
// If readline has already identified an arrow key, pass it through
|
|
||||||
// immediately, bypassing the Kitty protocol sequence buffering.
|
|
||||||
if (['up', 'down', 'left', 'right'].includes(key.name)) {
|
|
||||||
onKeypressRef.current(key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always pass through Ctrl+C immediately, regardless of protocol state
|
|
||||||
// Check both standard format and Kitty protocol sequence
|
|
||||||
if (
|
|
||||||
(key.ctrl && key.name === 'c') ||
|
|
||||||
key.sequence === `${ESC}${KITTY_CTRL_C}`
|
|
||||||
) {
|
|
||||||
kittySequenceBuffer = '';
|
|
||||||
// If it's the Kitty sequence, create a proper key object
|
|
||||||
if (key.sequence === `${ESC}${KITTY_CTRL_C}`) {
|
|
||||||
onKeypressRef.current({
|
|
||||||
name: 'c',
|
|
||||||
ctrl: true,
|
|
||||||
meta: false,
|
|
||||||
shift: false,
|
|
||||||
paste: false,
|
|
||||||
sequence: key.sequence,
|
|
||||||
kittyProtocol: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
onKeypressRef.current(key);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Kitty protocol is enabled, handle CSI sequences
|
|
||||||
if (kittyProtocolEnabled) {
|
|
||||||
// If we have a buffer or this starts a CSI sequence
|
|
||||||
if (
|
|
||||||
kittySequenceBuffer ||
|
|
||||||
(key.sequence.startsWith(`${ESC}[`) &&
|
|
||||||
!key.sequence.startsWith(PASTE_MODE_PREFIX) &&
|
|
||||||
!key.sequence.startsWith(PASTE_MODE_SUFFIX) &&
|
|
||||||
!key.sequence.startsWith(FOCUS_IN) &&
|
|
||||||
!key.sequence.startsWith(FOCUS_OUT))
|
|
||||||
) {
|
|
||||||
kittySequenceBuffer += key.sequence;
|
|
||||||
|
|
||||||
// Try to parse the buffer as a Kitty sequence
|
|
||||||
const kittyKey = parseKittySequence(kittySequenceBuffer);
|
|
||||||
if (kittyKey) {
|
|
||||||
kittySequenceBuffer = '';
|
|
||||||
onKeypressRef.current(kittyKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config?.getDebugMode()) {
|
|
||||||
const codes = Array.from(kittySequenceBuffer).map((ch) =>
|
|
||||||
ch.charCodeAt(0),
|
|
||||||
);
|
|
||||||
// Unless the user is sshing over a slow connection, this likely
|
|
||||||
// indicates this is not a kitty sequence but we have incorrectly
|
|
||||||
// interpreted it as such. See the examples above for sequences
|
|
||||||
// such as FOCUS_IN that are not Kitty sequences.
|
|
||||||
console.warn('Kitty sequence buffer has char codes:', codes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If buffer doesn't match expected pattern and is getting long, flush it
|
|
||||||
if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) {
|
|
||||||
// Log telemetry for buffer overflow
|
|
||||||
if (config) {
|
|
||||||
const event = new KittySequenceOverflowEvent(
|
|
||||||
kittySequenceBuffer.length,
|
|
||||||
kittySequenceBuffer,
|
|
||||||
);
|
|
||||||
logKittySequenceOverflow(config, event);
|
|
||||||
}
|
|
||||||
// Not a Kitty sequence, treat as regular key
|
|
||||||
kittySequenceBuffer = '';
|
|
||||||
} else {
|
|
||||||
// Wait for more characters
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (key.name === 'paste-start') {
|
|
||||||
isPaste = true;
|
|
||||||
} else if (key.name === 'paste-end') {
|
|
||||||
isPaste = false;
|
|
||||||
onKeypressRef.current({
|
|
||||||
name: '',
|
|
||||||
ctrl: false,
|
|
||||||
meta: false,
|
|
||||||
shift: false,
|
|
||||||
paste: true,
|
|
||||||
sequence: pasteBuffer.toString(),
|
|
||||||
});
|
|
||||||
pasteBuffer = Buffer.alloc(0);
|
|
||||||
} else {
|
|
||||||
if (isPaste) {
|
|
||||||
pasteBuffer = Buffer.concat([pasteBuffer, Buffer.from(key.sequence)]);
|
|
||||||
} else {
|
|
||||||
// Handle special keys
|
|
||||||
if (key.name === 'return' && key.sequence === `${ESC}\r`) {
|
|
||||||
key.meta = true;
|
|
||||||
}
|
|
||||||
onKeypressRef.current({ ...key, paste: isPaste });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRawKeypress = (data: Buffer) => {
|
|
||||||
const pasteModePrefixBuffer = Buffer.from(PASTE_MODE_PREFIX);
|
|
||||||
const pasteModeSuffixBuffer = Buffer.from(PASTE_MODE_SUFFIX);
|
|
||||||
|
|
||||||
let pos = 0;
|
|
||||||
while (pos < data.length) {
|
|
||||||
const prefixPos = data.indexOf(pasteModePrefixBuffer, pos);
|
|
||||||
const suffixPos = data.indexOf(pasteModeSuffixBuffer, pos);
|
|
||||||
|
|
||||||
// Determine which marker comes first, if any.
|
|
||||||
const isPrefixNext =
|
|
||||||
prefixPos !== -1 && (suffixPos === -1 || prefixPos < suffixPos);
|
|
||||||
const isSuffixNext =
|
|
||||||
suffixPos !== -1 && (prefixPos === -1 || suffixPos < prefixPos);
|
|
||||||
|
|
||||||
let nextMarkerPos = -1;
|
|
||||||
let markerLength = 0;
|
|
||||||
|
|
||||||
if (isPrefixNext) {
|
|
||||||
nextMarkerPos = prefixPos;
|
|
||||||
} else if (isSuffixNext) {
|
|
||||||
nextMarkerPos = suffixPos;
|
|
||||||
}
|
|
||||||
markerLength = pasteModeSuffixBuffer.length;
|
|
||||||
|
|
||||||
if (nextMarkerPos === -1) {
|
|
||||||
keypressStream.write(data.slice(pos));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextData = data.slice(pos, nextMarkerPos);
|
|
||||||
if (nextData.length > 0) {
|
|
||||||
keypressStream.write(nextData);
|
|
||||||
}
|
|
||||||
const createPasteKeyEvent = (
|
|
||||||
name: 'paste-start' | 'paste-end',
|
|
||||||
): Key => ({
|
|
||||||
name,
|
|
||||||
ctrl: false,
|
|
||||||
meta: false,
|
|
||||||
shift: false,
|
|
||||||
paste: false,
|
|
||||||
sequence: '',
|
|
||||||
});
|
|
||||||
if (isPrefixNext) {
|
|
||||||
handleKeypress(undefined, createPasteKeyEvent('paste-start'));
|
|
||||||
} else if (isSuffixNext) {
|
|
||||||
handleKeypress(undefined, createPasteKeyEvent('paste-end'));
|
|
||||||
}
|
|
||||||
pos = nextMarkerPos + markerLength;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let rl: readline.Interface;
|
|
||||||
if (usePassthrough) {
|
|
||||||
rl = readline.createInterface({
|
|
||||||
input: keypressStream,
|
|
||||||
escapeCodeTimeout: 0,
|
|
||||||
});
|
|
||||||
readline.emitKeypressEvents(keypressStream, rl);
|
|
||||||
keypressStream.on('keypress', handleKeypress);
|
|
||||||
stdin.on('data', handleRawKeypress);
|
|
||||||
} else {
|
|
||||||
rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 0 });
|
|
||||||
readline.emitKeypressEvents(stdin, rl);
|
|
||||||
stdin.on('keypress', handleKeypress);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (usePassthrough) {
|
unsubscribe(onKeypress);
|
||||||
keypressStream.removeListener('keypress', handleKeypress);
|
|
||||||
stdin.removeListener('data', handleRawKeypress);
|
|
||||||
} else {
|
|
||||||
stdin.removeListener('keypress', handleKeypress);
|
|
||||||
}
|
|
||||||
rl.close();
|
|
||||||
setRawMode(false);
|
|
||||||
|
|
||||||
// Clean up any pending backslash timeout
|
|
||||||
if (backslashTimeout) {
|
|
||||||
clearTimeout(backslashTimeout);
|
|
||||||
backslashTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are in the middle of a paste, send what we have.
|
|
||||||
if (isPaste) {
|
|
||||||
onKeypressRef.current({
|
|
||||||
name: '',
|
|
||||||
ctrl: false,
|
|
||||||
meta: false,
|
|
||||||
shift: false,
|
|
||||||
paste: true,
|
|
||||||
sequence: pasteBuffer.toString(),
|
|
||||||
});
|
|
||||||
pasteBuffer = Buffer.alloc(0);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [isActive, stdin, setRawMode, kittyProtocolEnabled, config]);
|
}, [isActive, onKeypress, subscribe, unsubscribe]);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue