Make Option/Alt session shortcuts work on macOS (physical key codes)

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.
This commit is contained in:
Aamer Akhter
2026-06-14 15:54:50 -04:00
parent e742d00c98
commit 60dab7ce3f
4 changed files with 56 additions and 8 deletions
+2 -1
View File
@@ -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+W` | Kill active session |
| `Ctrl/Cmd+Tab` | Next 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+Shift+{` / `Ctrl+Shift+}` | Move active tab left / right |
| `Ctrl/Cmd+L` | Clear terminal | | `Ctrl/Cmd+L` | Clear terminal |
| `Ctrl+Shift+R` | Restore terminal size | | `Ctrl+Shift+R` | Restore terminal size |
+23 -6
View File
@@ -770,14 +770,31 @@ class CodemanApp {
if (this.attachmentHistoryDrawerOpen) this.closeAttachmentHistory(); if (this.attachmentHistoryDrawerOpen) this.closeAttachmentHistory();
} }
// Alt+1-9: switch to Codeman session by index // Option/Alt session navigation uses physical key CODES, not e.key, so macOS
if (e.altKey && !e.ctrlKey && !e.shiftKey && e.key >= '1' && e.key <= '9') { // keyboard layouts that emit special characters under Option (Option+1 -> ¡,
const idx = parseInt(e.key) - 1; // Option+[ -> "“") still switch sessions. e.code is the physical key regardless
if (idx < this.sessionOrder.length) { // 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(); e.preventDefault();
this.selectSession(this.sessionOrder[idx]); this.prevSession();
return;
}
if (e.code === 'BracketRight') {
e.preventDefault();
this.nextSession();
return;
} }
return;
} }
// Match against shortcut table // Match against shortcut table
+2 -1
View File
@@ -505,7 +505,8 @@
<div class="shortcuts-grid"> <div class="shortcuts-grid">
<div><kbd>Ctrl</kbd>+<kbd>W</kbd></div><div>Close Session</div> <div><kbd>Ctrl</kbd>+<kbd>W</kbd></div><div>Close Session</div>
<div><kbd>Ctrl</kbd>+<kbd>Tab</kbd></div><div>Next Session</div> <div><kbd>Ctrl</kbd>+<kbd>Tab</kbd></div><div>Next Session</div>
<div><kbd>Alt</kbd>+<kbd>1-9</kbd></div><div>Switch to Tab N</div> <div><kbd>Option</kbd>+<kbd>[</kbd> / <kbd>Option</kbd>+<kbd>]</kbd></div><div>Previous / Next Session</div>
<div><kbd>Option</kbd>+<kbd>1-9</kbd></div><div>Switch to Tab N</div>
<div><kbd>Ctrl</kbd>+<kbd>L</kbd></div><div>Clear Terminal</div> <div><kbd>Ctrl</kbd>+<kbd>L</kbd></div><div>Clear Terminal</div>
<div><kbd>Ctrl</kbd>+<kbd>+</kbd></div><div>Increase Font</div> <div><kbd>Ctrl</kbd>+<kbd>+</kbd></div><div>Increase Font</div>
<div><kbd>Ctrl</kbd>+<kbd>-</kbd></div><div>Decrease Font</div> <div><kbd>Ctrl</kbd>+<kbd>-</kbd></div><div>Decrease Font</div>
+29
View File
@@ -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('<kbd>Option</kbd>+<kbd>[</kbd>');
expect(helpHtml).toContain('<kbd>Option</kbd>+<kbd>]</kbd>');
expect(helpHtml).toContain('<kbd>Option</kbd>+<kbd>1-9</kbd>');
expect(readme).toContain('`Option+[` / `Option+]`');
expect(readme).toContain('`Option+1`-`Option+9`');
});
});