- Terminals were never destroyed when their tab closed (socket drop only
detached listeners), so the per-session quota (3) filled up and new
terminals failed to mount with 'terminal limit reached'. Now the client
sends a {type:'close'} control frame on tab teardown and the host releases
the pty immediately; a bare socket drop (refresh/switch) keeps the process
for a 30s grace period, and a reconnect within it cancels the pending
close. open() also respawns a process whose shell already exited instead
of reusing a dead handle (input sink).
- The sidebar portal renders inside an error boundary: a render failure now
shows a dismissible error strip with the message (console.error with the
component stack) instead of a silent blank panel.
- Tests: +2 real-pty smoke specs (close releases quota + respawn after
exit; reconnect cancels pending close).
Host half (src/index.ts): /sidebar/api JSON API (session cwd, fs tree/read/
write, git status/diff/stage/commit/branch/log/show), /sidebar/file media
route, /sidebar/ws/terminal WebSocket over node-pty; routes share the /api
trust fence (Host-header loopback or connection.trustedHosts).
Client half (src/client): portal-mounted right panel with a per-session
split-pane workbench (recursive row/col splits, draggable dividers, tab
drag-move), tab bar with max-width 160px + plus-menu (explorer/git/terminal
multi-open rules), lazy file explorer rooted at the session cwd, editable
text editor (dirty dot, Ctrl/Cmd+S), image + Markdown preview, xterm
terminal with reconnect, and a VSCode-style git panel. Per-session state
persists to localStorage (dsh-sidebar:v1:<sessionId>). Intercepts the chat
turn-tail produced-files row (priority -1 chain entry) to open files in the
sidebar. UI styled with the dsh --dsw-* theme tokens and ui-primitives.
Build: tsdown emits the host lib plus the __ModuleLoader__ client bundle
(externals = module table words; xterm/css inlined). Tests: 19 unit + smoke
specs (pure state/tree/git parsers + real git/fs integration).