dsh-plugin-session-outline
August 14, 2026 · View on GitHub
A DeepSeek Harness cordis plugin: a floating session outline navigator on the right side of the conversation, listing every user turn and jumping to it.
- The entry point is the round button (☰) at the middle-right of the conversation area; click to expand/collapse (Esc or × to close).
- The outline keeps only user messages (
user/steeringrows = the start of each turn, numbered), and clicking an entry smoothly scrolls to that turn. - The panel is draggable (grab the header); its position is remembered in
localStorage.
Interaction details
- Current position indicator: the topmost visible user message is highlighted
in the outline; the header shows a
Session outline · Nentry count. - Previous/next turn:
↑ Previous turn / Next turn ↓buttons at the panel footer step through turns from the current position (without a current position, Previous = last turn, Next = first turn); while focus is inside the panel, ↑/↓ arrow keys do the same (input fields are never hijacked). - Jump feedback: the target entry flashes briefly after a hit.
- Lazy-load fallback: when reopening DSH, older turns may not be rendered
yet (or may have been unloaded by the virtualized list). Clicking such an
entry shows
⏳ Loading…, scrolls to the top/bottom of the conversation to trigger loading, and polls until the target appears (up to ~10s). - Debounced refresh: MutationObserver bursts are coalesced to 150 ms plus a 2 s safety interval; only structural changes and text changes inside user rows trigger a rescan, so assistant streaming does not cause repeated full scans.
- No dead button: when the conversation has no user messages yet, even the ☰ entry button is hidden.
- Bilingual copy: uses the DSH locale system (
session.outlinenamespace, zh/en) and follows the UI language. - Entry text is automatically stripped of trailing timestamps (
HH:mm, relative times, dates in both zh/en formats).
How it works
- Slot:
shell.overlay(list/root) — the layout's reserved floating layer, independent of the right details column;order: 100places it after the shipped entries. - Data source: the stable DOM anchors
data-chat-anchor-key/data-chat-flow-kind(user/steering) on conversation rows — the same anchors the conversation view itself uses. - Jumping: manual
scrollTopcomputation +scrollTo({ behavior: 'smooth' })with a 450 ms position check (notscrollIntoView, which the page's scroll guards / virtualized list may swallow). - Performance: collapsed → only a cheap presence probe; expanded → full scan
- IntersectionObserver for the current position; the plugin's own DOM mutations are ignored; unchanged scans skip re-renders.
- Styling: DSH design tokens (
--dsw-alias-*) only, dark/light aware; the injected<style>carries adata-plugindedupe guard so HMR reloads never duplicate it.
Compatibility
- Platform: DSH web (
dsh web),dsh.client.platform = "web". - peerDependencies:
@deepseek-ai/cordis^4.0.1@deepseek-ai/dsh-client-ui-slots^0.1.0-rc.6
- Client declaration:
inject: ['slots', 'locale'](service-level); the package-leveldsh.client.injectpoints at@deepseek-ai/dsh-client-ui-slots(a static module in the platform seed table). - The only reliance on DSH web internals is the DOM anchors above and the
shell.overlayslot — both are public upstream contracts.
Install
From npm (after publishing)
npm install -g dsh-plugin-session-outline # or install into the profile's node_modules
Manual
After building, copy the plugin directory into the profile's node_modules:
cp -r dsh-plugin-session-outline ~/.dsh/profiles/node_modules/
Then append to cordis.patch.yml:
- insert:
- id: session-outline
name: dsh-plugin-session-outline
Restart DSH and open a conversation — the ☰ button should appear mid-right.
Development
The client half must be bundled into the window.__ModuleLoader__.load({ id, factory })
form to be loaded by the web frontend (dsh-client-modules serves
exports["./client"] into the /plugins boot graph).
npm install # devDependency: esbuild only
npm run bundle # scripts/bundle.mjs: esbuild → lib/client.js
npm test # node --test: bundle contract + internals unit tests
npm run preflight # publish/deploy preflight (stale-bundle guard + contract checks)
npm run check # node --check lib/client.js
# npm publish runs prepublishOnly → npm run preflight automatically
Tests exercise the pure functions through the
internalsexport (the same convention as dsh-web-app) and black-box theapply/injectcontract in a vm sandbox.
Local verification
Deploy to a running DSH profile with the atomic sync script (temporary files + sequential renames, bundle last, so the server never sees a half-updated state):
npm run bundle && npm run sync # default: ~/.dsh/profiles/node_modules/<name>
# optional: DSH_WEB_URL=http://127.0.0.1:PORT npm run sync # also prints the new boot rev
Refresh the browser — a changed boot rev means the new bundle is served.
Publishing
Current status: private testing.
package.jsonis marked"private": true, so it must NOT be published to the public npm registry. Install from a local path or a git dependency during testing:
dsh plugin --profile web add file:<this-repo-path> # local path install
# or as a git dependency (private repos need npm credentials):
npm install git+https://<private-repo-url>.git
Once stable: remove "private": true, then npm publish
(prepublishOnly runs preflight + full tests and aborts on failure).
What ships is controlled by the files field: index.js, lib/client.js,
README.md, README.zh.md, LICENSE (source and build scripts are not
shipped).
Files
index.js host half (minimal loader entry)
src/client.jsx client source (React component + shell.overlay registration)
lib/client.js client bundle (build artifact — do not hand-edit)
scripts/bundle.mjs local build script (esbuild)
scripts/sync-profile.mjs atomic profile sync script
test/ contract + internals tests (node --test)
LICENSE MIT