diff --git a/README.md b/README.md index 63f1a542..ef36d07b 100644 --- a/README.md +++ b/README.md @@ -482,7 +482,7 @@ Single-digit selection (1-9), color-coded status, token counts, auto-refresh. De | Shortcut | Action | |----------|--------| | `Ctrl/Cmd+W` | Kill active session | -| `Ctrl/Cmd+K` | Find open session or start a new one | +| `Ctrl/Cmd/Option+K` | Find open session or start a new one | | `Ctrl/Cmd+Tab` | Next session | | `Alt/Option+[` / `Alt/Option+]` | Previous / next session | | `Alt/Option+1`-`Alt/Option+9` | Switch to tab N (physical keys, so macOS Option layouts work) | diff --git a/src/web/public/index.html b/src/web/public/index.html index de3daa80..cf45bcc9 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -573,7 +573,7 @@

Session

Ctrl+W
Close Session
-
Ctrl/Cmd+K
Find Open Session
+
Ctrl/Cmd/Option+K
Find Open Session
Ctrl+Tab
Next Session
Alt/Option+[ / Alt/Option+]
Previous / Next Session
Alt/Option+1-9
Switch to Tab N
diff --git a/src/web/public/panels-ui.js b/src/web/public/panels-ui.js index 2ce47739..81cccfe0 100644 --- a/src/web/public/panels-ui.js +++ b/src/web/public/panels-ui.js @@ -257,12 +257,17 @@ Object.assign(CodemanApp.prototype, { shouldOpenCommandPaletteFromShortcut(e) { if (!e) return false; - if ((e.key || '').toLowerCase() !== 'k') return false; - if (!(e.metaKey || e.ctrlKey) || e.altKey) return false; + const key = (e.key || '').toLowerCase(); + if (key !== 'k' && e.code !== 'KeyK') return false; + if (!(e.metaKey || e.ctrlKey || e.altKey)) return false; const target = e.target; if (!target) return true; const tagName = (target.tagName || '').toUpperCase(); + const className = typeof target.className === 'string' ? target.className : ''; + const isXtermHelper = + target.classList?.contains?.('xterm-helper-textarea') || className.includes('xterm-helper-textarea'); + if (isXtermHelper) return true; if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') return false; if (target.isContentEditable) return false; if (typeof target.closest === 'function' && target.closest('[contenteditable="true"]')) return false; diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index d44e5dbf..7b287297 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -120,12 +120,17 @@ Object.assign(CodemanApp.prototype, { this.terminal.attachCustomKeyEventHandler((ev) => { if (ev.isComposing || ev.keyCode === 229) return false; - // Let the app's Alt/Option session-nav shortcuts reach the document keydown handler + // Let the app's Alt/Option session-nav and Command Palette shortcuts reach the document keydown handler // (app.js switches tabs by PHYSICAL e.code) instead of xterm injecting ESC into // the PTY. Mirror app.js's gate exactly — same physical codes + modifier guard — so - // macOS Option layouts (Option+1 -> "¡", Option+[ -> "“") are suppressed here too and + // macOS Option layouts (Option+1 -> "¡", Option+[ -> "“", Option+K -> "˚") are suppressed here too and // don't leak an escape sequence into the focused terminal on every tab switch. - if (ev.altKey && !ev.ctrlKey && !ev.shiftKey && /^(Digit[1-9]|BracketLeft|BracketRight)$/.test(ev.code || '')) { + if ( + ev.altKey && + !ev.ctrlKey && + !ev.shiftKey && + /^(Digit[1-9]|BracketLeft|BracketRight|KeyK)$/.test(ev.code || '') + ) { return false; } diff --git a/test/command-palette-ui.test.ts b/test/command-palette-ui.test.ts index 1a3dc6ec..35bf10e8 100644 --- a/test/command-palette-ui.test.ts +++ b/test/command-palette-ui.test.ts @@ -122,6 +122,40 @@ describe('Command-K session palette', () => { ).toBe(false); }); + it('recognizes Ctrl-K from the focused xterm helper textarea', () => { + const { app } = loadPaletteHarness(); + + expect( + app.shouldOpenCommandPaletteFromShortcut({ + key: 'k', + code: 'KeyK', + metaKey: false, + ctrlKey: true, + altKey: false, + target: { + tagName: 'TEXTAREA', + isContentEditable: false, + classList: { contains: (name: string) => name === 'xterm-helper-textarea' }, + }, + }) + ).toBe(true); + }); + + it('recognizes macOS Option-K by physical key code', () => { + const { app } = loadPaletteHarness(); + + expect( + app.shouldOpenCommandPaletteFromShortcut({ + key: '˚', + code: 'KeyK', + metaKey: false, + ctrlKey: false, + altKey: true, + target: null, + }) + ).toBe(true); + }); + it('opens and focuses the palette search box', () => { const { app, elements } = loadPaletteHarness(); diff --git a/test/keyboard-shortcuts.test.ts b/test/keyboard-shortcuts.test.ts index 1e3d8b8c..e8c6ed49 100644 --- a/test/keyboard-shortcuts.test.ts +++ b/test/keyboard-shortcuts.test.ts @@ -24,7 +24,7 @@ describe('keyboard shortcuts', () => { // terminal-ui.js must gate its xterm pass-through on the SAME physical e.code set the // app.js handler consumes; otherwise Alt+[ / Alt+] (and Option+digit on remapped macOS // layouts) switch tabs AND inject ESC into the focused terminal. Keep in sync. - expect(terminalUiSource).toContain('/^(Digit[1-9]|BracketLeft|BracketRight)$/.test(ev.code'); + expect(terminalUiSource).toContain('/^(Digit[1-9]|BracketLeft|BracketRight|KeyK)$/.test(ev.code'); }); it('documents the Alt/Option shortcuts in help and README', () => { @@ -37,7 +37,7 @@ describe('keyboard shortcuts', () => { it('documents the Command-K open-session palette in help and README', () => { expect(appSource).toContain('this.openCommandPalette()'); - expect(helpHtml).toContain('Ctrl/Cmd+K'); - expect(readme).toMatch(/\| `Ctrl\/Cmd\+K`\s+\| Find open session or start a new one\s+\|/); + expect(helpHtml).toContain('Ctrl/Cmd/Option+K'); + expect(readme).toMatch(/\| `Ctrl\/Cmd\/Option\+K`\s+\| Find open session or start a new one\s+\|/); }); });