feat(v0.2): multi-tab terminal + titles + Claude Code shortcut bar

Frontend feature (backend unchanged — manager was already multi-session):
- Multi-tab: each tab = independent WS + Terminal + session (terminal-session.ts,
  tabs.ts); tab bar with +, ×, middle-click close; persisted to localStorage
  (migrates v0.1 single-session key)
- Tab titles: auto from xterm onTitleChange (OSC 0/2), double-click to rename
  inline (manual wins over auto); ellipsis + tooltip
- Shortcut key bar now shown on ALL devices (clickable buttons trigger shortcuts),
  reordered for Claude Code (Esc prominent, ⇧Tab, ↑↓, ⏎, ^C, Tab, ←→, /);
  responsive sizing via @media (pointer:coarse) for touch
- Verified in browser: rename, two independent sessions, keybar on desktop +
  narrow viewport. 191 tests green; both typechecks + esbuild build pass.
This commit is contained in:
Yaojia Wang
2026-06-17 12:11:18 +02:00
parent f3b6ad5a68
commit 4f66016f02
7 changed files with 734 additions and 319 deletions

View File

@@ -11,7 +11,7 @@
*/
import { describe, it, expect } from 'vitest'
import { KEY_MAP } from '../public/keybar.js'
import { KEY_MAP, KEYBAR_BUTTONS } from '../public/keybar.js'
describe('keybar', () => {
describe('KEY_MAP — pure byte mappings', () => {
@@ -73,7 +73,12 @@ describe('keybar', () => {
expect(KEY_MAP.tab.charCodeAt(0)).toBe(0x09)
})
it('all 9 keys are defined', () => {
it('slash maps to / (Claude Code slash-command launcher)', () => {
expect(KEY_MAP.slash).toBe('/')
expect(KEY_MAP.slash).toHaveLength(1)
})
it('all 10 keys are defined', () => {
const keys = Object.keys(KEY_MAP)
expect(keys).toEqual([
'esc',
@@ -85,13 +90,14 @@ describe('keybar', () => {
'enter',
'ctrlC',
'tab',
'slash',
])
})
it('all mapped bytes are unique strings', () => {
const bytes = Object.values(KEY_MAP)
expect(bytes).toHaveLength(9)
expect(new Set(bytes).size).toBe(9) // all unique
expect(bytes).toHaveLength(10)
expect(new Set(bytes).size).toBe(10) // all unique
})
it('ANSI arrow sequences use correct format', () => {
@@ -103,4 +109,24 @@ describe('keybar', () => {
})
})
})
describe('KEYBAR_BUTTONS — layout', () => {
it('every button references a valid KEY_MAP key', () => {
for (const b of KEYBAR_BUTTONS) {
expect(KEY_MAP[b.keyName]).toBeDefined()
}
})
it('Esc is first and marked primary (most-used interrupt)', () => {
expect(KEYBAR_BUTTONS[0]?.keyName).toBe('esc')
expect(KEYBAR_BUTTONS[0]?.primary).toBe(true)
})
it('exposes all Claude Code keys as buttons', () => {
const keys = KEYBAR_BUTTONS.map((b) => b.keyName)
expect(keys).toContain('shiftTab')
expect(keys).toContain('slash')
expect(keys).toContain('arrowUp')
})
})
})