fix(cli): reserve Ctrl+Shift+C for terminal copy, not quit/clear (#8011)

* fix(cli): reserve Ctrl+Shift+C for terminal copy, not quit/clear

Ctrl+Shift+C is the standard terminal copy shortcut. The QUIT and
CLEAR_INPUT bindings matched it because they only constrained ctrl
and left shift unspecified, which the matcher ignores. In Kitty-
protocol terminals the escape-hatch check also caught Ctrl+Shift+C
(ESC[99;6u) since it tested ctrl+name without excluding shift.

Add shift: false to both bindings and !key.shift to the escape-hatch
condition so Ctrl+Shift+C passes through to the terminal for copy.
Plain Ctrl+C (shift undefined or false) is unaffected.

Fixes #8006

* test(cli): guard Ctrl+Shift+C paste escape-hatch against !key.shift mutation

The existing Kitty Ctrl+Shift+C test passed identically with or without
the `!key.shift` condition in the Ctrl+C escape hatch, so a regression
making Ctrl+Shift+C clear a stuck paste would ship undetected. Add a
test that enters a stuck keypress-level paste and asserts Ctrl+Shift+C
is buffered as paste content rather than dispatched, which fails when
`!key.shift` is removed.

* fix(cli): remove dead !key.shift escape-hatch guard (#8011)

The KeypressContext.tsx escape-hatch guard added in bb86d57 is
unreachable: Node readline never produces a Key with ctrl=true,
name='c', and shift=true for any terminal encoding (kitty,
modifyOtherKeys, or legacy 0x03). Mutation test M2 confirmed
reverting this hunk has zero effect on the test suite.

Remove the guard and the two tests that exercised it. The
keyBindings.ts shift:false fix (the load-bearing change proven by
M1) and its keyMatchers tests are untouched.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: 秦奇 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Code Autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-07-29 12:52:53 +00:00
1 parent ea4c25e25f
commit bfd64114bf
3 files changed
+37 -6

No files matched your search

+2 -2
View File
@@ -136,7 +136,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
// Text deletion
[Command.KILL_LINE_RIGHT]: [{ key: 'k', ctrl: true }],
[Command.KILL_LINE_LEFT]: [{ key: 'u', ctrl: true }],
[Command.CLEAR_INPUT]: [{ key: 'c', ctrl: true }],
[Command.CLEAR_INPUT]: [{ key: 'c', ctrl: true, shift: false }],
// Added command (meta/alt/option) for mac compatibility
[Command.DELETE_WORD_BACKWARD]: [
{ key: 'backspace', ctrl: true },
@@ -243,7 +243,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
// App level bindings
[Command.TOGGLE_TOOL_DESCRIPTIONS]: [{ key: 't', ctrl: true }],
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }],
[Command.QUIT]: [{ key: 'c', ctrl: true }],
[Command.QUIT]: [{ key: 'c', ctrl: true, shift: false }],
[Command.EXIT]: [{ key: 'd', ctrl: true }],
[Command.SHOW_MORE_LINES]: [{ key: 's', ctrl: true }],
[Command.RETRY_LAST]: [{ key: 'y', ctrl: true }],
@@ -908,6 +908,26 @@ describe('KeypressContext - Kitty Protocol', () => {
}),
);
});
it('should still treat Kitty Ctrl+C as the escape hatch', async () => {
const keyHandler = vi.fn();
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => result.current.subscribe(keyHandler));
// Modifier 5 is Ctrl
act(() => {
stdin.sendKittySequence(`\x1b[99;5u`);
});
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'c',
ctrl: true,
shift: false,
kittyProtocol: true,
}),
);
});
});
describe('paste mode', () => {
+15 -4
View File
@@ -29,7 +29,8 @@ describe('keyMatchers', () => {
[Command.END]: (key: Key) => key.ctrl && key.name === 'e',
[Command.KILL_LINE_RIGHT]: (key: Key) => key.ctrl && key.name === 'k',
[Command.KILL_LINE_LEFT]: (key: Key) => key.ctrl && key.name === 'u',
[Command.CLEAR_INPUT]: (key: Key) => key.ctrl && key.name === 'c',
[Command.CLEAR_INPUT]: (key: Key) =>
key.ctrl && key.name === 'c' && !key.shift,
[Command.DELETE_WORD_BACKWARD]: (key: Key) =>
((key.ctrl || key.meta) && key.name === 'backspace') ||
key.sequence === '\x1f',
@@ -66,7 +67,7 @@ describe('keyMatchers', () => {
key.ctrl && key.name === 't',
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: (key: Key) =>
key.ctrl && key.name === 'g',
[Command.QUIT]: (key: Key) => key.ctrl && key.name === 'c',
[Command.QUIT]: (key: Key) => key.ctrl && key.name === 'c' && !key.shift,
[Command.EXIT]: (key: Key) => key.ctrl && key.name === 'd',
[Command.SHOW_MORE_LINES]: (key: Key) => key.ctrl && key.name === 's',
[Command.RETRY_LAST]: (key: Key) => key.ctrl && key.name === 'y',
@@ -150,7 +151,12 @@ describe('keyMatchers', () => {
{
command: Command.CLEAR_INPUT,
positive: [createKey('c', { ctrl: true })],
negative: [createKey('c'), createKey('k', { ctrl: true })],
negative: [
createKey('c'),
createKey('k', { ctrl: true }),
// Ctrl+Shift+C is the terminal copy shortcut — must not clear input
createKey('c', { ctrl: true, shift: true }),
],
},
{
command: Command.DELETE_WORD_BACKWARD,
@@ -321,7 +327,12 @@ describe('keyMatchers', () => {
{
command: Command.QUIT,
positive: [createKey('c', { ctrl: true })],
negative: [createKey('c'), createKey('d', { ctrl: true })],
negative: [
createKey('c'),
createKey('d', { ctrl: true }),
// Ctrl+Shift+C is the terminal copy shortcut — must not quit
createKey('c', { ctrl: true, shift: true }),
],
},
{
command: Command.EXIT,