- types/protocol: ClientMessage approve/reject; ServerMessage status.pending
- server: POST /hook/permission HELD until the client decides; approve/reject
ws messages resolve it with {hookSpecificOutput.decision.behavior allow|deny};
5-min timeout + release-on-disconnect fall back to Claude's own prompt
- manager.handleHookEvent threads pending flag
- setup-hooks: PermissionRequest uses the held curl (writes decision to stdout);
status events stay fire-and-forget
- FE: terminal-session approve()/reject() + pending state; tabs approval banner
(Approve/Reject) for the active tab's held request
- Tests: protocol approve/reject; integration ⑧ held→approve→allow. 207 green.
- Browser-verified: banner 'Claude wants to use Bash' → Approve → resolves allow.
Root cause: whole tab was draggable=true, so a click with any micro-movement
became a (no-op) drag instead of a click → switch failed. Plus every event
(activity/title/status) rebuilt the entire tab bar via replaceChildren, which
could destroy the element mid-click.
Fix:
- switch on pointerdown (fires before drag arms; also works on touch)
- build tab DOM once; update classes/dot/label IN PLACE (refreshTab); full
rebuild only on structural change (add/close/reorder/rename)
- close/rename-input stopPropagation on pointerdown so they don't re-activate
Verified: single pointerdown/click switches reliably across tabs; rename + drag
still work. 198 tests green.
- terminal-session.ts: track SessionStatus (connecting/connected/reconnecting/
exited) + onStatus callback; FIX: wire onTitleChange (onTitle was in opts but
never bound — auto-title was dead)
- tabs.ts: per-tab status dot (color = connection state), unread flag set on
background output + cleared on activate, HTML5 drag-to-reorder (preserves
active tab, persisted)
- style.css: .tab-dot colors, .unread ring, .dragging/.drag-over feedback
- Browser-verified: green dot connected, unread ring after bg output, reorder
AAA|BBB→BBB|AAA persists across reload. 191 tests green, typechecks + build ok.