mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-31 03:44:33 +00:00
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
This commit is contained in:
parent
7836c919bf
commit
bb86d57ff2
4 changed files with 58 additions and 7 deletions
|
|
@ -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 }],
|
||||
|
|
|
|||
|
|
@ -830,6 +830,46 @@ describe('KeypressContext - Kitty Protocol', () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not treat Kitty Ctrl+Shift+C as the Ctrl+C escape hatch', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Modifier 6 is Ctrl+Shift
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[99;6u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'c',
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
kittyProtocol: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -843,7 +843,7 @@ export function KeypressProvider({
|
|||
// including Ctrl+C itself — and the user has no way to recover
|
||||
// without killing the terminal.
|
||||
const isCtrlCKey =
|
||||
(key.ctrl && key.name === 'c') ||
|
||||
(key.ctrl && key.name === 'c' && !key.shift) ||
|
||||
key.sequence === `${ESC}${KITTY_CTRL_C}`;
|
||||
if (isCtrlCKey) {
|
||||
if (isPaste || pasteChunks.length > 0) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue