Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion packages/cli/src/ui/components/Composer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { describe, it, expect, vi } from 'vitest';
import { render } from '../../test-utils/render.js';
import { Text } from 'ink';
import { Box, Text } from 'ink';
import { Composer } from './Composer.js';
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
import {
Expand Down Expand Up @@ -589,4 +589,29 @@ describe('Composer', () => {
);
});
});

describe('Shortcuts Hint', () => {
it('hides shortcuts hint when a action is required (e.g. dialog is open)', () => {
const uiState = createMockUIState({
customDialog: (
<Box>
<Text>Test Dialog</Text>
<Text>Test Content</Text>
</Box>
),
});

const { lastFrame } = renderComposer(uiState);

expect(lastFrame()).not.toContain('ShortcutsHint');
});

it('keeps shortcuts hint visible when no action is required', () => {
const uiState = createMockUIState();

const { lastFrame } = renderComposer(uiState);

expect(lastFrame()).toContain('ShortcutsHint');
});
});
});
4 changes: 2 additions & 2 deletions packages/cli/src/ui/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
flexDirection="column"
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
>
<ShortcutsHint />
{!hasPendingActionRequired && <ShortcutsHint />}
</Box>
</Box>
{uiState.shortcutsHelpVisible && <ShortcutsHelp />}
<HorizontalLine width={uiState.terminalWidth} />
<HorizontalLine />
<Box
justifyContent={
settings.merged.ui.hideContextSummary
Expand Down
49 changes: 49 additions & 0 deletions packages/cli/src/ui/components/InputPrompt.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4028,6 +4028,55 @@ describe('InputPrompt', () => {
});
});
});

describe('shortcuts help visibility', () => {
it.each([
{
name: 'terminal paste event occurs',
input: '\x1b[200~pasted text\x1b[201~',
},
{
name: 'Ctrl+V (PASTE_CLIPBOARD) is pressed',
input: '\x16',
setupMocks: () => {
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
vi.mocked(clipboardy.read).mockResolvedValue('clipboard text');
},
},
{
name: 'mouse right-click paste occurs',
input: '\x1b[<2;1;1m',
mouseEventsEnabled: true,
setupMocks: () => {
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
vi.mocked(clipboardy.read).mockResolvedValue('clipboard text');
},
},
])(
'should close shortcuts help when a $name',
async ({ input, setupMocks, mouseEventsEnabled }) => {
setupMocks?.();
const setShortcutsHelpVisible = vi.fn();
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
{
uiState: { shortcutsHelpVisible: true },
uiActions: { setShortcutsHelpVisible },
mouseEventsEnabled,
},
);

await act(async () => {
stdin.write(input);
});

await waitFor(() => {
expect(setShortcutsHelpVisible).toHaveBeenCalledWith(false);
});
unmount();
},
);
});
});

function clean(str: string | undefined): string {
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/ui/components/InputPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({

// Handle clipboard image pasting with Ctrl+V
const handleClipboardPaste = useCallback(async () => {
if (shortcutsHelpVisible) {
setShortcutsHelpVisible(false);
}
try {
if (await clipboardHasImage()) {
const imagePath = await saveClipboardImage(config.getTargetDir());
Expand Down Expand Up @@ -403,7 +406,14 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
} catch (error) {
debugLogger.error('Error handling paste:', error);
}
}, [buffer, config, stdout, settings]);
}, [
buffer,
config,
stdout,
settings,
shortcutsHelpVisible,
setShortcutsHelpVisible,
]);

useMouseClick(
innerBoxRef,
Expand Down Expand Up @@ -553,6 +563,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
}

if (key.name === 'paste') {
if (shortcutsHelpVisible) {
setShortcutsHelpVisible(false);
}
// Record paste time to prevent accidental auto-submission
if (!isTerminalPasteTrusted(kittyProtocol.enabled)) {
setRecentUnsafePasteTime(Date.now());
Expand Down
49 changes: 49 additions & 0 deletions packages/cli/src/ui/components/ShortcutsHelp.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, afterEach, vi } from 'vitest';
import { renderWithProviders } from '../../test-utils/render.js';
import { ShortcutsHelp } from './ShortcutsHelp.js';

describe('ShortcutsHelp', () => {
const originalPlatform = process.platform;

afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
vi.restoreAllMocks();
});

const testCases = [
{ name: 'wide', width: 100 },
{ name: 'narrow', width: 40 },
];

const platforms = [
{ name: 'mac', value: 'darwin' },
{ name: 'linux', value: 'linux' },
] as const;

it.each(
platforms.flatMap((platform) =>
testCases.map((testCase) => ({ ...testCase, platform })),
),
)(
'renders correctly in $name mode on $platform.name',
({ width, platform }) => {
Object.defineProperty(process, 'platform', {
value: platform.value,
});

const { lastFrame } = renderWithProviders(<ShortcutsHelp />, {
width,
});
expect(lastFrame()).toContain('shell mode');
expect(lastFrame()).toMatchSnapshot();
},
);
});
Loading
Loading