From 60dab7ce3f6799bcbf9c2d492260611b74bca978 Mon Sep 17 00:00:00 2001 From: Aamer Akhter Date: Sun, 14 Jun 2026 15:54:50 -0400 Subject: [PATCH 1/2] Make Option/Alt session shortcuts work on macOS (physical key codes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tab-switch shortcuts matched e.key, so on macOS Option+1 emits a special character ('¡', not '1') and the shortcut silently failed. Switch to physical e.code (Digit1-9), which is layout-independent. Also adds Option+[ / Option+] for previous / next session. Help modal + README updated. Test: test/keyboard-shortcuts.test.ts. --- README.md | 3 ++- src/web/public/app.js | 29 +++++++++++++++++++++++------ src/web/public/index.html | 3 ++- test/keyboard-shortcuts.test.ts | 29 +++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 test/keyboard-shortcuts.test.ts diff --git a/README.md b/README.md index 7d821538..c7527928 100644 --- a/README.md +++ b/README.md @@ -483,7 +483,8 @@ Single-digit selection (1-9), color-coded status, token counts, auto-refresh. De |----------|--------| | `Ctrl/Cmd+W` | Kill active session | | `Ctrl/Cmd+Tab` | Next session | -| `Alt+1`–`Alt+9` | Switch to tab N | +| `Option+[` / `Option+]` | Previous / next session | +| `Option+1`-`Option+9` | Switch to tab N (physical keys, so macOS Option layouts work) | | `Ctrl+Shift+{` / `Ctrl+Shift+}` | Move active tab left / right | | `Ctrl/Cmd+L` | Clear terminal | | `Ctrl+Shift+R` | Restore terminal size | diff --git a/src/web/public/app.js b/src/web/public/app.js index e5843288..1dd10b6d 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -770,14 +770,31 @@ class CodemanApp { if (this.attachmentHistoryDrawerOpen) this.closeAttachmentHistory(); } - // Alt+1-9: switch to Codeman session by index - if (e.altKey && !e.ctrlKey && !e.shiftKey && e.key >= '1' && e.key <= '9') { - const idx = parseInt(e.key) - 1; - if (idx < this.sessionOrder.length) { + // Option/Alt session navigation uses physical key CODES, not e.key, so macOS + // keyboard layouts that emit special characters under Option (Option+1 -> ¡, + // Option+[ -> "“") still switch sessions. e.code is the physical key regardless + // of layout. Option+1-9 = switch by index; Option+[ / Option+] = prev / next. + if (e.altKey && !e.ctrlKey && !e.shiftKey) { + const code = e.code || ''; + const digitMatch = code.match(/^Digit([1-9])$/); + if (digitMatch) { + const idx = parseInt(digitMatch[1], 10) - 1; + if (idx < this.sessionOrder.length) { + e.preventDefault(); + this.selectSession(this.sessionOrder[idx]); + } + return; + } + if (e.code === 'BracketLeft') { e.preventDefault(); - this.selectSession(this.sessionOrder[idx]); + this.prevSession(); + return; + } + if (e.code === 'BracketRight') { + e.preventDefault(); + this.nextSession(); + return; } - return; } // Match against shortcut table diff --git a/src/web/public/index.html b/src/web/public/index.html index f0987d35..38194fb8 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -505,7 +505,8 @@
Ctrl+W
Close Session
Ctrl+Tab
Next Session
-
Alt+1-9
Switch to Tab N
+
Option+[ / Option+]
Previous / Next Session
+
Option+1-9
Switch to Tab N
Ctrl+L
Clear Terminal
Ctrl++
Increase Font
Ctrl+-
Decrease Font
diff --git a/test/keyboard-shortcuts.test.ts b/test/keyboard-shortcuts.test.ts new file mode 100644 index 00000000..b02f43d6 --- /dev/null +++ b/test/keyboard-shortcuts.test.ts @@ -0,0 +1,29 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const appSource = readFileSync('src/web/public/app.js', 'utf8'); +const helpHtml = readFileSync('src/web/public/index.html', 'utf8'); +const readme = readFileSync('README.md', 'utf8'); + +describe('keyboard shortcuts', () => { + it('uses physical Option+number keys so macOS special characters do not break tab switching', () => { + expect(appSource).toContain('e.code ||'); + expect(appSource).toContain('Digit([1-9])'); + expect(appSource).toContain('parseInt(digitMatch[1], 10) - 1'); + }); + + it('provides Option+bracket shortcuts for previous and next session', () => { + expect(appSource).toContain("e.code === 'BracketLeft'"); + expect(appSource).toContain("e.code === 'BracketRight'"); + expect(appSource).toContain('this.prevSession()'); + expect(appSource).toContain('this.nextSession()'); + }); + + it('documents mac-friendly Option shortcuts in help and README', () => { + expect(helpHtml).toContain('Option+['); + expect(helpHtml).toContain('Option+]'); + expect(helpHtml).toContain('Option+1-9'); + expect(readme).toContain('`Option+[` / `Option+]`'); + expect(readme).toContain('`Option+1`-`Option+9`'); + }); +}); From 80ebf8b5494459b2aed6016e15e0e8c123540753 Mon Sep 17 00:00:00 2001 From: "Claude (Codeman maintainer)" Date: Sun, 14 Jun 2026 22:27:36 +0200 Subject: [PATCH 2/2] fix(shortcuts): stop Alt/Option nav keys leaking ESC sequences into the terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR migrated the app.js tab-nav handler to physical e.code but left xterm's pass-through gate (terminal-ui.js) matching ev.key digits. Consequences: - Alt+[ / Alt+] (the new bindings) were never in the gate, so xterm sent ESC[ / ESC] to the PTY on every platform AS WELL AS switching the session. - Alt+digit on a remapped macOS Option layout (Option+1 -> "¡") didn't match the ev.key '0'-'9' gate either, so xterm injected ESC — on exactly the layouts this PR exists to fix. Update the xterm gate to mirror app.js exactly: suppress when `ev.altKey && !ctrl && !shift && /^(Digit[1-9]|BracketLeft|BracketRight)$/.test(ev.code)`. Returning false there tells xterm not to write to the PTY, so the shortcut switches the tab with no stray escape sequence. Also: relabel the docs Alt/Option (the mechanism is layout/OS-independent, so the shortcut works for Linux/Windows Alt users too — "Option" alone was Mac-only wording), and add a keyboard-shortcuts test asserting terminal-ui.js gates on the same physical codes so this desync can't regress (a grep the original test missed). Verified: keyboard-shortcuts test 4/4, check:frontend-syntax, check:public-assets, format:check all clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 ++-- src/web/public/index.html | 4 ++-- src/web/public/terminal-ui.js | 10 ++++++++-- test/keyboard-shortcuts.test.ts | 20 ++++++++++++++------ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c7527928..6bcfbaab 100644 --- a/README.md +++ b/README.md @@ -483,8 +483,8 @@ Single-digit selection (1-9), color-coded status, token counts, auto-refresh. De |----------|--------| | `Ctrl/Cmd+W` | Kill active session | | `Ctrl/Cmd+Tab` | Next session | -| `Option+[` / `Option+]` | Previous / next session | -| `Option+1`-`Option+9` | Switch to tab N (physical keys, so macOS Option layouts work) | +| `Alt/Option+[` / `Alt/Option+]` | Previous / next session | +| `Alt/Option+1`-`Alt/Option+9` | Switch to tab N (physical keys, so macOS Option layouts work) | | `Ctrl+Shift+{` / `Ctrl+Shift+}` | Move active tab left / right | | `Ctrl/Cmd+L` | Clear terminal | | `Ctrl+Shift+R` | Restore terminal size | diff --git a/src/web/public/index.html b/src/web/public/index.html index 38194fb8..09588109 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -505,8 +505,8 @@
Ctrl+W
Close Session
Ctrl+Tab
Next Session
-
Option+[ / Option+]
Previous / Next Session
-
Option+1-9
Switch to Tab N
+
Alt/Option+[ / Alt/Option+]
Previous / Next Session
+
Alt/Option+1-9
Switch to Tab N
Ctrl+L
Clear Terminal
Ctrl++
Increase Font
Ctrl+-
Decrease Font
diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index c6edc476..c39f91a2 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -114,8 +114,14 @@ Object.assign(CodemanApp.prototype, { this.terminal.attachCustomKeyEventHandler((ev) => { if (ev.isComposing || ev.keyCode === 229) return false; - // Let Alt+digit pass through to browser (tab switching) - if (ev.altKey && ev.key >= '0' && ev.key <= '9') return false; + // Let the app's Alt/Option session-nav 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 + // 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 || '')) { + return false; + } // Ctrl+V / Cmd+V: intercept before xterm sends ^V to PTY. // Route through our paste trap which handles both images and text. diff --git a/test/keyboard-shortcuts.test.ts b/test/keyboard-shortcuts.test.ts index b02f43d6..7b93e541 100644 --- a/test/keyboard-shortcuts.test.ts +++ b/test/keyboard-shortcuts.test.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; const appSource = readFileSync('src/web/public/app.js', 'utf8'); +const terminalUiSource = readFileSync('src/web/public/terminal-ui.js', 'utf8'); const helpHtml = readFileSync('src/web/public/index.html', 'utf8'); const readme = readFileSync('README.md', 'utf8'); @@ -19,11 +20,18 @@ describe('keyboard shortcuts', () => { expect(appSource).toContain('this.nextSession()'); }); - it('documents mac-friendly Option shortcuts in help and README', () => { - expect(helpHtml).toContain('Option+['); - expect(helpHtml).toContain('Option+]'); - expect(helpHtml).toContain('Option+1-9'); - expect(readme).toContain('`Option+[` / `Option+]`'); - expect(readme).toContain('`Option+1`-`Option+9`'); + it('suppresses xterm PTY injection for the same physical Alt nav codes (no ESC leak)', () => { + // 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'); + }); + + it('documents the Alt/Option shortcuts in help and README', () => { + expect(helpHtml).toContain('Alt/Option+['); + expect(helpHtml).toContain('Alt/Option+]'); + expect(helpHtml).toContain('Alt/Option+1-9'); + expect(readme).toContain('`Alt/Option+[` / `Alt/Option+]`'); + expect(readme).toContain('`Alt/Option+1`-`Alt/Option+9`'); }); });