- 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.
- explorer row menu gains a download action (files only); the host route
serves raw bytes with Content-Disposition: attachment, binary-safe
- editor enables line numbers
- Tabs, the + menu, and empty-pane cards carry per-type icons (folder /
branch / terminal / code file); a terminal glyph joins the icon set
- Hovering an explorer row reveals an @文件 button at the far right that
appends @<relative path> to the composer draft (session-scoped input
service, space-separated append)
- Right-clicking a row opens a context menu: copy relative / absolute
path (writeClipboard + transient in-row "copied" label)
- Rows become div role=button (Enter/Space) to host the inner button;
relativeTo helper unit-tested
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.
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.
- 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).