* catalog: add dsh-video-preview to the file-previewer recommended plugins
Adds the video preview plugin (dsh-video-preview) to the "add preview plugin" modal catalog, with zh/en descriptions. Data integrity is guarded by tests/plugin-list.spec.ts.
* test: make add-plugin-modal window.open count catalog-driven
The test pinned the viewer catalog to a single entry (2 "Open:" buttons).
Adding a second recommended plugin (dsh-video-preview) renders 4 buttons
and broke the hardcoded assertion. Derive the expected count from
builtinViewerPlugins so adding/removing plugins keeps the test honest.
- Sidebar.tsx: useLayoutEffect for the layout-push variables so cleanup+setup
land in the same pre-paint frame (no full-width flash on drag release);
applyDrag writes the width only while the panel is open (a closed right
panel no longer squeezes the conversation when dragging the bottom height);
flush the final drag frame before stopDragScheduling on all three handles;
sync centerRect.right on width/corner release so the bottom panel does not
jump.
- index.ts/pty-manager.ts: close a session's UI terminals immediately on
session/disposed (they used to linger until reconnectGraceMs expired).
- sidebar.module.css: no highlight on the resize handles while dragging
(local preference; the corner hover hint stays).
Map the .cs extension to the built-in csharp StreamLanguage from
@codemirror/legacy-modes/mode/clike (already a dependency, no new packages),
so .cs files open with syntax highlighting in the sidebar editor.
The open-path interception reroutes EVERY chat-side file open into the
sidebar editor, including the stock "Show in folder" gesture: the
ui-deliverables row passes "." (resolved to "<cwd>/." by the chat view),
so clicking it opened a bogus editor tab for the workspace directory
(host error: '"..." is a directory').
- openpath-intercept: detect the folder-reveal gesture ("." / "./" /
"<cwd>/.") and route it to a new revealInExplorer dep instead of the editor.
- intercept: revealInExplorer switches to the explorer tab (expanding the
panel), expands the produced files' parent directories, and highlights /
scrolls to them; the intercepted produced-files row now also renders its
own "Show in folder" button when files are hidden.
- produced-files: selectProducedFiles reads the engine Turn data
(owner.turn.data.get("deliverables")) — the same source ui-deliverables
uses — with the node replica as fallback. The old code read owner.nodes,
which the real TurnTailOwnerProps ({turn, seq, openFile}) does not carry,
so the takeover never claimed the chain.
- state: transient `revealed` set (never persisted) plus a revealPaths
reducer that expands ancestor directories.
- ExplorerView: highlight + scroll revealed rows; Sidebar/tabs/service
thread `revealed` through TabComponentProps.
- tests: folder-gesture routing, engine-data selector, wiring seat, zh/en copy.
Add a side-card preference 'browserAllowedLoopback' (comma-separated
host or host:port entries) that lets the sidebar browser navigate to
explicitly trusted loopback addresses. Bare hosts (localhost,
127.0.0.1) match every port; host:port entries match exactly.
Allowlisted local addresses additionally get the 'allow-same-origin'
sandbox token in the iframe: local dev servers (Vite etc.) need a real
origin for their module/HMR/fetch pipeline and would otherwise render
blank in the opaque-origin sandbox. The page stays cross-origin to the
GUI and to every other site.
The server-side browser.probe route mirrors the same allowlist so
probed local servers are not rejected as 'local addresses are not
probed'.
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.
The panel width lived in the per-session state (dsh-sidebar:v1:<id>), so a
drag in one conversation never carried to another: switching conversations
snapped the panel back to that session's own width. The width is a layout
preference, not session content, so it now persists under a cross-session
key (dsh-sidebar:v1:width):
- every persist writes the current width to the global key;
- loadState prefers the global width over a session's persisted one;
- a cache-hit session switch adopts the global width.
Backward compatible: with no global key present (first run / older data)
the behavior is unchanged. Adds a regression test covering drag-in-A ->
switch-to-B and drag-in-B -> switch-back-to-A.
The "add to conversation" selection popup was registered only for the
catch-all code viewer (viewerId === 'code'), so selecting text in the
CodeMirror editor of a markdown file produced no popup; insertion only
worked in markdown preview mode via the separate DOM-selection handler.
Extend the CodeMirror selection popup to the markdown editor
(viewerId === 'markdown') so line-exact selections can be injected into
the composer draft (\`\`\`rel/path:start-end\`\`\` payload), matching
preview-mode behavior and the code viewer.