Explorer rows become the app's tree-row cell — 34px, radius 8, inset by
the scroll padding, 22px indent step, and the session-row mount fade —
and git rows get the same cell plus the proper selected fill
(interactive-bg-active). Icon buttons switch to the app's 28px circle
pattern (ui-workspace), headers settle at the 36px section rhythm, and
keyboard focus gets a visible ring everywhere per the styling policy,
with a prefers-reduced-motion block turning off the slides and fades.
The app resolves light/dark from prefers-color-scheme and the user's
choice (body[data-ds-dark-theme]), but xterm and CodeMirror were pinned to
hardcoded dark themes — a light-mode user got a dark card inside a light
panel. New theme.ts reads the resolved scheme + token values and notifies
on flips (the presenter toggles the body attribute); the terminal now takes
its surface colors from the tokens and a curated one-dark/one-light ANSI
palette, and the editor gets a token-driven surface with the same curated
syntax palettes. Both re-theme in place on a scheme flip: the terminal
re-applies its theme, the editor reconfigures a Compartment holding only
the scheme-dependent extensions, so the document, undo history and scroll
survive. one-dark is dropped (superseded by the curated highlight style).
The close frame was sent on every unmount, so switching conversations (or
refreshing) killed the shell even though the tab was still open; coming
back spawned a fresh terminal. Now the unmount close frame only fires when
the tab is truly gone (checked against the per-session store state), so a
bare unmount hands the process to the host's reconnect grace and the next
connect reattaches the SAME shell with its transcript. Closing a tab
releases the quota immediately: the socket-open path keeps the close
frame, and a new pty.close API covers the socket-down (reconnect loop)
case that previously left a 30s zombie holding the quota.
The host also records the spawn cwd and respawns when a reconnect carries
a different authoritative cwd — the page-load hydrate race can attach the
real session cwd after the first connect fell back to the process cwd, and
a shell in the wrong directory must not linger.
Restructured per the user's request: a full feature walkthrough (explorer,
CodeMirror editor with preview/edit toggle, terminal, git, split workbench,
tab bar, session isolation, interception) followed by step-by-step source
installation (clone → pnpm install/build → web profile registration →
restart). Corrected stale limitations (syntax highlighting now shipped;
the sidebar squeezes the layout instead of floating).
Switching tabs used to unmount the active view; a terminal's unmount sent
the tab-close frame, killing its pty, and switching back respawned a fresh
shell. Every tab now stays mounted and inactive ones are display:none: a
terminal keeps its WebSocket + pty + scrollback, an editor keeps its
CodeMirror view and unsaved draft, explorer/git keep their data. The close
frame now fires only when a tab is truly closed (close button, middle
click, pane removal). Re-shown terminals re-fit via the existing
ResizeObserver; editors re-measure via CodeMirror's own observer.
The served bundle's wsUrl is provably correct, yet one report showed
ws://host/ws//host/sidebar/ws/terminal (a /ws/ prefix no version ever
emitted). Mirror the app's own downlink construction (new URL(path,
location.origin) + protocol swap — the construction the chat's working
event streams use), and surface the exact attempted URL in the failure
banner plus a console.error, so a recurrence identifies the source
(stale page vs environment rewrite) immediately.
- markdown tabs open in rendered preview with a 预览/编辑 segmented toggle;
edit mode uses the CodeMirror source editor (markdown highlighting), the
preview renders the live unsaved draft, and the editor stays mounted while
previewing so edits survive the toggle.
- an empty pane shows openable-type cards (explorer / git / terminal, the
same options as the + menu, terminal dimmed at the cap) instead of the
bare 'this pane is empty' message; clicking a card opens the tab.
- the + button sits immediately right of the last tab (sticky at the
scrollport's right edge when the tab row overflows, so it stays
reachable).
- 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 per-session terminal quota counted exited zombie handles (shells
closed or dropped on hosts without the close-frame protocol), so enough
abandoned terminals permanently blocked new ones — the 'disconnected,
reconnecting' loop. open() now sweeps the session's exited handles before
the quota check, and the running instance's leftovers disappear on the
next restart (disposeAll).
- Dragging a tab onto the strip's background fired the drop TWICE: the
TabBar handled it (merge into this pane) and the event bubbled to the
pane root, which ran its edge-zone handler again — producing two splits /
a duplicated instance (up+down). The strip now stops propagation for
drag-over and drop.
- Tests: +1 real-pty smoke (exited handle swept, new tab spawns under a
quota of 1).
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).
- An empty pane (fresh split, or last tab dragged out) now keeps the tab
strip with its + menu mounted, so a new tab can always be created or
dragged in; the placeholder body still explains the state.
- Default width unified: new sessions open at half the CONTENT area
(viewport minus the left sidebar), so the panel never swallows half the
whole window and old/new sessions no longer disagree (previously old
persisted widths ~1/4 vs fresh sessions 1/2). Fullscreen restore targets
the same content-half width.
- Expand/collapse now animates: the panel stays mounted and slides
off-screen (translateX + visibility after the slide); fullscreen width
changes animate too, and drag-resizing disables the transition.
- Root cause of the endless 'disconnected, reconnecting' banner: pnpm
strips the executable bit from node-pty's prebuilt spawn-helper, so every
spawn failed with posix_spawnp failed and the client retried forever.
ensureSpawnHelper() restores 0755 at plugin activation (mirrors
dsh-pty-local's postinstall; works for link installs too). Verified
against the live GUI: terminal now connects and echoes.
- TerminalView stops auto-reconnecting on a server refusal (close 1011 with
a reason): shows the reason plus a manual retry instead of spinning.
- defaultShell() also guards an empty SHELL.
- New sessions open at half the viewport width; width clamping now allows
filling the window, and a header fullscreen button expands the sidebar to
the full viewport (toggle restores half).
- Middle-click on a tab closes it (auxclick button 1, autoscroll
suppressed).
- Tests: +2 (shell fallback, spawn-helper exec bit idempotent).
- Split panes by dragging a tab to a pane edge (left/right/up/down zones
with a drop overlay) or into the center to merge; the toolbar split
buttons are gone (split-pane.tsx, state.ts moveTabToEdge/insertLeafAt).
- The header close button is now a collapse button; the expand button on
the right edge stays visible whenever the panel is collapsed, so the
sidebar can always be reopened (Sidebar.tsx, locales collapse/expand).
- Styles now follow the DSH web-styling rules exactly: --dsw-alias-*
semantic colors (state-error/warn tokens), --dsw-font-* typography roles
(size + line-height paired), --dsw-shadow-lv* elevation, no color
literals or static palette values (sidebar.module.css).
- Tests: +3 for moveTabToEdge (edge split, center merge, self reorder).
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).