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.
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
/**
|
|
* public/keybar.ts — Mobile touch key bar, tuned for Claude Code.
|
|
*
|
|
* Exports pure key-to-byte mappings and a mount function that binds DOM buttons
|
|
* to onSend on touchstart (bypassing xterm so the soft keyboard doesn't pop up).
|
|
*
|
|
* Order/selection follows Claude Code mobile habits — the keys a phone soft
|
|
* keyboard can't easily produce, most-used first (leftmost = no scroll needed):
|
|
* Esc \x1b interrupt Claude / dismiss (primary — most used)
|
|
* Shift+Tab \x1b[Z cycle plan / auto-accept mode
|
|
* ↑ ↓ \x1b[A/B select menu option / history
|
|
* Enter \r confirm
|
|
* Ctrl+C \x03 cancel / quit
|
|
* Tab \t complete / toggle
|
|
* ← → \x1b[D/C cursor
|
|
* / / slash-command launcher
|
|
*/
|
|
|
|
import type { MountKeybar } from '../src/types.js'
|
|
|
|
/** Key name → byte string mapping (pure data, for testing). */
|
|
export const KEY_MAP = {
|
|
esc: '\x1b',
|
|
shiftTab: '\x1b[Z',
|
|
arrowUp: '\x1b[A',
|
|
arrowDown: '\x1b[B',
|
|
arrowLeft: '\x1b[D',
|
|
arrowRight: '\x1b[C',
|
|
enter: '\r',
|
|
ctrlC: '\x03',
|
|
tab: '\t',
|
|
slash: '/',
|
|
} as const
|
|
|
|
export type KeyName = keyof typeof KEY_MAP
|
|
|
|
/** Button layout: most-used Claude Code keys first. `primary` styles a key as prominent. */
|
|
export const KEYBAR_BUTTONS: Array<{ label: string; keyName: KeyName; primary?: boolean }> = [
|
|
{ label: 'Esc', keyName: 'esc', primary: true },
|
|
{ label: '⇧Tab', keyName: 'shiftTab' },
|
|
{ label: '↑', keyName: 'arrowUp' },
|
|
{ label: '↓', keyName: 'arrowDown' },
|
|
{ label: '⏎', keyName: 'enter' },
|
|
{ label: '^C', keyName: 'ctrlC' },
|
|
{ label: 'Tab', keyName: 'tab' },
|
|
{ label: '←', keyName: 'arrowLeft' },
|
|
{ label: '→', keyName: 'arrowRight' },
|
|
{ label: '/', keyName: 'slash' },
|
|
]
|
|
|
|
/** Mount the key bar: create buttons in #keybar, bind touchstart → onSend(bytes) + preventDefault. */
|
|
export const mountKeybar: MountKeybar = (onSend) => {
|
|
const keybarEl = document.getElementById('keybar')
|
|
if (!keybarEl) return
|
|
|
|
for (const { label, keyName, primary } of KEYBAR_BUTTONS) {
|
|
const btn = document.createElement('button')
|
|
btn.textContent = label
|
|
btn.classList.add('keybar-btn')
|
|
if (primary) btn.classList.add('keybar-btn-primary')
|
|
btn.dataset.key = keyName
|
|
btn.setAttribute('aria-label', label)
|
|
|
|
const bytes = KEY_MAP[keyName]
|
|
|
|
// touchstart: send immediately, prevent default so the soft keyboard stays hidden.
|
|
btn.addEventListener('touchstart', (e) => {
|
|
e.preventDefault()
|
|
onSend(bytes)
|
|
})
|
|
|
|
// Desktop fallback: click also sends.
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault()
|
|
onSend(bytes)
|
|
})
|
|
|
|
keybarEl.appendChild(btn)
|
|
}
|
|
}
|