Two failure modes of a sidebar render crash, fixed together:
1. Layout-push leak (root cause): the layout-push effect wrote
--dsh-sidebar-width/--dsh-sidebar-height on document.documentElement
without a cleanup, so ANY unmount of the Sidebar (error-boundary swap,
plugin disable, HMR) left the variables behind and layout.css kept
squeezing #root with a stale margin — "the sidebar cannot be hidden"
until a full reload. The effect now removes both properties on cleanup
(the CSS fallback 0px restores the layout).
2. Crash containment (UX): the root error boundary previously replaced the
WHOLE sidebar — toggle cluster included — with an error strip on any tab
crash. A new shared RenderBoundary (src/client/RenderBoundary.tsx) now
scopes containment per tab: a crashing viewer/editor shows a dismissible
strip inside its own pane while the toggle cluster, the other tabs, and
the panel stay alive. The descriptor is rendered as a real element
(createElement) so even top-level component throws land in the boundary,
not in TabContent's own render. The root boundary remains as the last
resort for Workbench/shell errors.
Tests (tests/sidebar-crash.spec.tsx, jsdom + real Sidebar/store harness):
- unmount clears both CSS variables (red before the fix: 307px leaked)
- a crashing tab is contained: strip in-pane, collapse button alive,
layout push still live
- the retry button recovers a tab whose crash has since been fixed
Full suite: 425 tests pass; tsc --noEmit clean.
- Remove every box-shadow; panel uses the native sidebar fill surface
(--dsw-specific-sidebar-fill) with a hairline border, the collapse
control becomes the native 36px circular rail button, the active tab
uses the native interactive-bg-active fill, and rows/typography follow
the native 34px/14px session-row recipe.
- New 'Side card' settings section in the DSH Settings shell: open-by-
default for new conversations (boolean) and default width share as a
percent of the window (20-60). New sessions seed from these prefs;
existing per-session layouts stay untouched.
- The settings namespace is read/written through the plugin's own fenced
/sidebar routes calling the settings seam in-process: the DSH settings
RPC domain only serves allowlisted namespaces to configuration clients,
so third-party namespaces cannot use connection.api.settings (reads are
silently filtered, writes refused with 'not exposed'). The seam still
persists to the user settings document with revision-guarded writes.
- state: side-store per conversation, injected as a prop instead of a module singleton
- host: schemastery Config (read/media/list/terminal limits) resolved from cordis config
- shape: types/exports/peerDependencies for the web profile + plugin-shape spec
- fix: split divider emits incremental deltas so drags track the cursor 1:1
- docs: README refresh
- cwd resolution falls back (attached header → client summary cwd → process
cwd) across the JSON API, media route and terminal upgrade, so explorer/
git/terminal work from first paint instead of 'session ... has no working
directory' while the web client hydrates the session (refresh-only fix).
- terminal client threads the summary cwd into the WS URL and stops the
reconnect loop after three unreasoned failures, showing the close code
with a manual retry; a 1011+reason refusal already showed the reason.
- white screen: persisted state is structurally sanitized on load (malformed
shapes fall back to default) and the restored width is clamped to the
viewport; the layout margin is capped at the viewport; the client entry
wraps every lifecycle step in try/catch and pins a visible diagnostic bar
on load/mount/interception failure instead of failing silently.
- editor: textarea replaced with CodeMirror 6 — line wrapping, per-extension
syntax highlighting (js/ts/jsx/tsx, json, python, html, css, xml, yaml,
sql, java, c/c++, rust, go, php, shell, toml, nginx, dockerfile,
properties), one-dark theme, dirty dot + Ctrl/Cmd+S save kept.
- tests: +9 (cwd fallback chain over the API route, state sanitization,
language mapping); 36 pass, typecheck + build + vm materialize clean.
The open panel now occupies layout space: #root (the AppFrame grid) gives
up the panel width on the right via margin-right driven by a
--dsh-sidebar-width CSS variable, and only the center column is flexible,
so the squeeze lands exactly on the agent output and the input bar (VSCode
sidebar behavior). The fixed panel covers the vacated strip; expand/
collapse animates the margin and the slide on the same theme duration,
width drags disable the transition, and prefers-reduced-motion is honored.
- 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).