Conversation outline panel for the DSH web surface: user questions as root nodes (index, time, tool count and text volume per turn) with the Markdown headings of assistant replies hanging beneath them, plus level slider, search scope, bookmarks, copy and export as Markdown, section jump, keyboard navigation, a right-edge density rail and per-session reading-position memory. Read-only UI: it never touches prompts, tool results or provider traffic, and writes nothing outside its own localStorage keys.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:huangfuren/dsh-conversation
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
English | 中文
A conversation outline panel for the DSH web surface: it turns the current session into a navigable tree — user questions are the root nodes (index, time, per-turn tool count and text volume), and the Markdown headings of the assistant replies hang under the question they belong to. Clicking any row jumps to the message and flashes it briefly. Pure UI: it never touches prompts, messages, tool results, and makes no provider requests.
Project status: 1.x. Verified on dsh 0.1.5-rc.2 with the
webprofile, and adapted to dsh 0.2.x (>=0.2.0-rc.1 <1.0.0). Older dsh versions and other profiles are not certified — the plugin degrades to "panel absent" rather than throwing, but that path is not part of the tested matrix.
Version history: 1.1.0 merged the whole former dsh-outline plugin and renamed the package from dsh-client-hqst; 1.2.0 added the bulk of the navigation surface (three width tiers, side rail, search scope, export, section jump, keyboard shortcuts, reading-position memory). The side rail was disabled in 1.3.1, and 1.5.5 comments its code out in place with a per-block rationale (no behaviour change).
What it looks like
- Pinned (default) — a floating panel (
dsh-conv-panel) titled "History · Outline".- Three width tiers: compact 320px → wide 480px → full 720px full-height, cycled from the title bar. A tier grows away from whichever edge the panel is anchored to — flush left grows right, flush right grows left, dragged into the middle grows symmetrically; when one side lacks the room, the entire shortfall goes to the other side. The wall-side edge never moves, so widening never pushes the panel out of the window and shrinking returns it to its previous position (a margin ≤ 32px counts as flush, which covers the default 16px dock offset).
- Left/right docking: the title-bar dock button switches between docking to the left and to the right, resetting to the default position.
- The title bar doubles as the drag handle (4px threshold, position persisted).
- Tool row: expand/collapse all, bookmarks-only, copy outline (Markdown to clipboard), export as .md, search box with highlighted hits.
- Search scope: the button left of the search box cycles all / questions / headings.
- Level slider: 0 = questions only, 1–6 = open down to that heading level; dot tooltips show per-level counts.
- Background opacity: a slider at the bottom of the panel adjusts the panel background between 40% and 100% (default 100%, i.e. the old look). Only the background is dimmed — text stays fully opaque — and the backdrop blur is derived from it in reverse: the more transparent, the more the text behind shows through and the larger the blur radius (about 3.6px at 40%, 0 at 100%). That is by design, not a rendering fault. Click the percentage to go back to fully opaque, or double-click the slider. Useful when the panel covers the content you are reading.
- Rows: collapse arrow,
Q1badge, text, time, this turn's "N tools / N chars" stats, bookmark star; the row at the current reading position is softly highlighted and scrolled into view. - Footer: top / bottom.
- Collapsed — the only entry point is the "Outline" button in the session title bar (
order: 100, next to the native buttons). Since 1.3.1 the former right-edge rail (dsh-conv-rail) and the corner capsule (dsh-conv-fab) are disabled: they duplicated the header button, and the rail overlapped plugins that ship their own navigation rail. Since 1.5.5 that dead code is commented out in place — every block states why it is dead (which commit removed the mount point), why it is kept, how to restore it and the retirement date; search the marker停用保留("retired, kept") inclient.jsto find them. Nothing is deleted and none of it is built, so restoring the feature means removing the comment markers. - Jump granularity — single click jumps to the node; double click jumps to the end of that section (next node at the same or higher level, or the very bottom).
- Keyboard — with the panel focused,
Alt+↓/Alt+↑move to the next / previous node; the listener is scoped to the panel and never steals host shortcuts. - Reading position memory — each session remembers the outline node you last looked at, and the panel lands there when you come back.
- Empty state — "No outline content yet" until the session has questions or headings.
- First-paint placeholder — after a reload, while the host's session / chat data is not ready yet, the panel position shows "Reading conversation…" and the panel mounts once the host session binding is ready (if the binding arrives before the chat snapshot, the panel may render the empty state once and fill in on the next snapshot update). If the host still has no session binding after 2 seconds, the panel mounts anyway and shows the empty state.
- Adaptive header button (icon-only) — when the right sidebar opens and the header narrows, the "Outline" button collapses to the bare icon via the same mechanism as the native buttons: the same container query (
@container (width<=540px)) against the same.wSkVaW_titleRowcontainer. The button really shrinks to icon width;aria-label/titlestay, click and keyboard focus keep working; it restores when the space comes back. On older hosts without the container it degrades to the full-width button (safe). - Persisted state:
pinned/ drag position / view tier / panel background opacity / default level / per-session bookmarks / per-session reading position — all inlocalStorageunder thedsh-conversation:prefix. The rail-toggle field disabled in 1.3.1 is no longer read as of 1.5.5 (its normalisation is commented out); the key stays inlocalStorageand is simply ignored, and restoring that code brings the old value back.
How it works
| Half | File | Responsibility |
|---|---|---|
| Node half | index.js |
An empty apply() — it exists only so the plugin is visible to the host cordis.yml / Loader. No host-side behaviour. |
| Client half | client.js |
The actual UI, discovered by the web host through exports["./client"] plus the dsh.client declaration in package.json. |
Client behaviour:
- Takes two required services via
inject: ["slots", "sessions"]; the panel is disabled (with a warning) only whenslots.registeritself is missing.apply()also captures thesessions/uiSessionservices into a plugin-private host bridge. - Registers
dsh-conversation(order: 100) in theconversation.session.header.actionsslot and mergesstore/hostlast into the slot props — empty owner props, or a third-party header passing a different shape, cannot override them. When the slot is declared late, registration goes through three channels (inject / declaration subscription / immediate + 700 ms backoff retry; see "Three-tier registration fallback"). - Two data paths: the slot props
useChat/useSession/useSessionsinjected by older hosts or third-party headers win when present; when they are absent the panel readsuiSession.current→hooks.chat(ChatSnapshot) andhooks.session(hasMore/loadOlder). Both paths normalise throughconversationNodesOfand anArray.isArrayguard; if neither yields data the panel renders its empty state.sessions.binding(id)is still used for the legacy fallback andloadOlder. buildOutlineItemsflattens the snapshot into outline items: user/steering → level 0 (counting that turn's tools and text volume); assistant text blocks become level 1–6 by ATX heading (^#{1,6}\s+,#inside fenced code blocks skipped); streamingpartialheadings appear live with a breathing animation; non-prose nodes (tool-call / context) are counted but never enter the outline.OutlineManagerowns level, search (with scope) and bookmark state, synced tolocalStorage; tree building, collapse restoration, visible-row flattening and reveal-on-locate are pure functions.- Click-to-locate tries the host's internal anchors
[data-chat-anchor-key]→[data-message-key]→[data-node-key], then falls back to matchingdata-chat-flow-kindorder plus a text check; when nothing matches it says "this node cannot be located right now" rather than silently scrolling to the wrong place. - "End of section" is resolved by
nextSectionItemwalking the pre-order flat list for the next node at the same or higher level, falling back to scrolling to the bottom. - Export uses an in-memory Blob +
URL.createObjectURL(no host request, nothing written to the workspace) and shares its content with "copy outline" (outlineToMarkdown). - All styling goes through
dsw-alias-*/dsw-shadow-*theme tokens; no hard-coded colours.
Compatibility
- Both snapshot contracts: the new
snapshot.nodes[]+snapshot.partial, and the legacysnapshot.chat.{order, nodes(Map)}+node.data.content. A shape mismatch yields an empty list, never an exception. - Hardened slot data source: the
conversation.session.header.actionsowner props can be empty (dsh 0.2.0-rc.2 renders the slot withrenderSlot(..., {})), and a third-party header such asdsh-better-sidebarmay pass a different shape entirely. The panel prefers hooks injected through slot props and falls back touiSession.current→hooks.chat/hooks.session; when neither path has data it renders the empty state instead of taking down the slot. - Host API degradation: the shapes of
slots/sessions/uiSessionand every store'sgetSnapshot/subscribeare probed layer by layer with exceptions swallowed; a rename or removal only degrades this panel to its empty state, it never takes down other plugins in the same slot. - One-shot compatibility diagnostics: the first recognisable snapshot (including 0 rows) logs one
console.infoline with the actual structure (legacy.nodes/nodes/chat.order…) and row count; an unrecognised structure logs oneconsole.warnnaming the keys it saw. The first rendered outline probes the host DOM contract (data-conversation-scroll/data-chat-flow-kind/ anchor attributes) and logs oneconsole.warnwhen an attribute is missing or renamed, stating that exact-anchor jumps degraded to text-order matching. Both logs fire at most once per page load and change no fallback behaviour. - Three-tier registration fallback: the slot may be declared after the plugin loads, and the first
slots.registerthrowsslot "…" is not declared(0.1.5-rc.2 / 0.2.0-rc.1 / 0.2.1-rc.5 all throw it). The channel is therefore chosen from what the host actually exposes: ①slots.injectpresent → use the host contract; ② otherwiseslots.subscribeDeclaration→ subscribe to declaration events and register, which also recovers after a slot collapses and is re-declared; ③ neither → try immediately plus 700ms backoff (up to 12 attempts). Losinginjectno longer makes the whole panel disappear. Duplicate registration is gated bydeclarationEpoch, so the same id never triggersalready has an entry. - Style capability probing: background opacity relies on CSS
color-mix(Chromium 111+). Where it is unsupported that row is not rendered and the panel stays fully opaque, instead of leaving a slider that does nothing. - Bilingual copy: the plugin ships its own zh/en dictionary and falls back by
navigator.language; if the host injects a usabletthat wins.
Design notes and provenance
The feature choices here were informed by publicly described functionality and interaction patterns of similar plugins in the DSH ecosystem (conversation outline / question navigation / minimap style). Capabilities of this kind — an outline tree, an edge rail, search, export, reading-position tracking — are generic interaction design and are not protected by copyright. The code is an independent implementation in this repository; no third-party source was copied or adapted, and there is no third-party runtime dependency (the only peer dependency is the host-provided react).
Requirements
| Component | Supported baseline |
|---|---|
| Node.js | >= 22.19 |
| DSH | >= 0.1.5-rc.2 < 1.0.0 (slot name and slots.inject / register contract checked against 0.1.5-rc.2, 0.2.0-rc.1 and 0.2.1-rc.5; on-device verification: 0.2.1-rc.5 desktop) |
| react | ^18.2.0 (provided by the host) |
| Surface | web (dsh.client.platform) |
| Rendering engine | Chromium 111+ (only for "background opacity"; 0.2.1-rc.5 runs Electron 44.7.0 / Chromium 152) |
Anything outside this table is unverified.
Installation
An immutable tag is preferred:
dsh plugin --profile web add github:huangfuren/dsh-conversation#v1.5.6
Tracking the development branch (need a commit that is not released yet? use this):
dsh plugin --profile web add github:huangfuren/dsh-conversation
Or point at a local checkout:
dsh plugin --profile web add file:/absolute/path/to/dsh-conversation
package.json declares dsh.bundle.patch (./cordis.patch.yml), so dsh plugin add wires the bundle
itself. If you prefer to insert it by hand in the profile's cordis.patch.yml, the equivalent is:
- insert:
- id: conversation
name: dsh-conversation
The only peer dependency is react ^18.2.0, supplied by the host.
Development
npm run check # node --check on both entry points
npm test # node --test
npm run verify # check + test (bound to prepack, so a failing test cannot be packaged)
client.js is not an ES module: the dsh client module loader loads it as
window.__ModuleLoader__.load({ id, factory }), and the require inside factory can only resolve
host-provided external modules (this plugin uses react alone). Relative requires would not resolve in
the real loader, which is why the client stays a single self-contained file — the same choice
dsh-grafana makes (its client.js is ~67 KB). test/client.test.js therefore loads the module through
a minimal loader stub and asserts the pure helpers it exports as internals; test/host-service.test.js
drives the slot component registered by apply() and locks in two more contracts: with empty host slot
props the outline is built from the uiSession / sessions services, and missing services or hostile
props only degrade to the empty state. test/diagnostics.test.js locks in the DOM-contract probe matrix,
the snapshot-shape matrix, and the "at most one line per page load" rule for both diagnostics.
Known limitations
- Click-to-locate depends on the host's DOM contract (
data-chat-anchor-key/data-message-key/data-node-keyordata-chat-flow-kind); if those attributes are renamed wholesale, new candidates must be added inlocateByKey/collectUserRows. The plugin probes the contract once after the first rendered outline and logs a singleconsole.warnwhen something is missing (see "One-shot compatibility diagnostics"). - The service path follows the host's "main view" binding (
uiSession.current): the slot's owner props carry nosessionId, so on a subagent session header the panel still shows the main session's outline. AsessionIdsupplied through props, if the host ever adds one, takes precedence for bookmarks/reading position and theloadOldertarget; the outline itself still follows the main-view binding. - The panel is
fixed-positioned, so it can overlap other top-right floating plugins — they have to offset theirtop. - The "N tools / N chars" figures count tool nodes after that turn in the snapshot and the character count of assistant prose; if the host renames node kinds they degrade to not displayed (never throw).
- The export filename is fixed at
dsh-conversation-outline.md; there is no naming option.
Dependencies and license
- peerDependencies:
react ^18.2.0 - license: MIT
Links
More in this category
zhu1090093659/dsh-web#packages/dsh-task-board★ 10124
Task board for the dsh web GUI: a sidebar multi-column kanban whose cards run in real DSH agent sessions and can also be scheduled with cron expressions, executed host-side even with the browser closed.
zhu1090093659/dsh-web#packages/dsh-web-all★ 10124
Plugin and skin collection for the DSH Web UI: task board, Git graph, right-side panel, remote mobile UI, pet, live token stats, and a skin center.
ccch1mneyyy/dsh-TUI★ 7006
Claude Code-style full-screen terminal UI: pixel-whale header, live status line, and streaming thought expansion.
MeteorNOX/DeepSeek-Balance-Whale-Widget★ 6922
A fixed-corner whale widget for the DSH web GUI — balance, today's usage and per-turn cost with peak/off-peak pricing, editable balance-alert and daily-budget bubbles, a module-based custom bubble queue with A/B weighted choices and random lines or images, 30+ vendor templates (OpenAI, OpenRouter, Kimi, SiliconFlow, Ark, Zhipu, MiniMax and more) with per-model balance and subscription quota, plus task-end sound, imported audio, custom roles and a resource manager. Local-only, no telemetry.
omdsh-dev/DSH-better-sidebar★ 4933
Full sidebar workbench with file rendering and editing, terminal, Git, and subagents; third-party plugins can register new tabs.
Devin-AXIS/deepseek-design#deepseek-idesign★ 1723
Visual design studio for websites, app prototypes, posters, cards, reports, and magazines, with templates, direct element editing, selection-aware AI draft handoff, and export.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.