# dsh-conversation-outline — Implementation Spec A DeepSeek Harness (DSH) client plugin that adds a **Codex-style conversation outline**: a floating panel listing every user question in the current conversation, with search and **click-to-jump** (switches to the Chat view, scrolls to the message, flashes a highlight). Bilingual zh-CN / en. This spec is the single source of truth for the team. All paths are relative to the repo root `/Users/liziqing/Programs/agents/dsh-conversation-outline`. --- ## 1. Facts about DSH (verified against installed rc.6 packages) Reference material on disk (READ ONLY): - DSH packages (runtime + types): `/Users/liziqing/.npm/_npx/1e7f6d9597241db0/node_modules/@deepseek-ai/` - Web profile flat node_modules (symlinks to the above, plus react): `/Users/liziqing/.dsh/profiles/node_modules/` - Working third-party plugin example (published, MIT): `@nanmicoder/dsh-agent-teams` installed at `/Users/liziqing/.dsh/profiles/web/node_modules/@nanmicoder/dsh-agent-teams/` — its `lib/client.js` shows the exact loader-wrapper shape, panel CSS patterns, and `apply(ctx)` portal pattern. - Authoritative dev skill: `/tmp/dsh-plugin-skill.md` (fetched from NanmiCoder/dsh-agent-teams `skills/dsh-plugin-development/SKILL.md`) and `/tmp/developing-dsh-plugins.md` (the long-form guide). Read both. ### 1.1 Client plugin contract - A bundle = npm package whose `package.json` has: - `dsh.bundle.patch` → `./cordis.patch.yml` (bundle patch layer) - `dsh.client` = `{ platform: "web", inject: ["@deepseek-ai/dsh-client-runtime", ...] }` (`inject` is informational metadata; the REAL activation deps come from the client bundle's exported `inject` array) - `exports["./client"]` → the built browser bundle - `cordis.patch.yml` is a top-level YAML array: `- insert: [{id, name, config}]`. `name` MUST equal the package name (roster resolves `require.resolve('/package.json')`). - The browser bundle is a CJS closure factory: ```js window.__ModuleLoader__.load({ id: "", factory: (require) => { var module = { exports: {} }; var exports = module.exports; /* ...bundled code... */ return module.exports; } }); ``` Built with tsdown@0.22 (cjs, platform browser), `external` = the platform module table (see 1.4), everything else inlined, sourcemap on, `clean: false` (must not wipe the tsc host output). - Client plugin entry (`src/client/index.tsx`, MUST be `.tsx` to use JSX) exports: ```ts export const inject = ['slots', 'sessions', 'locale'] // cordis services to wait for export function apply(ctx: ClientContext): void { ... } ``` - HMR/dispose: every DOM node, React root, style tag, listener must be owned by `ctx.effect(() => disposer, 'label')`. ### 1.2 The host half Even a client-only plugin needs a host row in the composition (the roster scans Loader entries for `dsh.client` packages). The host module (`src/index.ts`, tsc → `lib/index.js`) must export a plugin body — a function or `{apply}` object (cordis throws otherwise). Use a minimal: ```ts import type { Context } from '@deepseek-ai/cordis' export const name = 'dsh-conversation-outline' export const inject: string[] = [] export function apply(ctx: Context): void { // client-only plugin: nothing to do on the host } ``` (Keep a `Config` schema optional with a `z.object({})` default so the row config is valid; follow the agent-teams shape.) ### 1.3 UI seams (verified in rc.6) - `shell.overlay` — **the** seat for frame-wide floating surfaces: `kind: 'list'`, `scope: 'root'`, declared by `@deepseek-ai/dsh-client-ui-layout`. The layer is click-through by default; entries opt back into pointer events. Register with a fresh `id` (e.g. `dsh-conversation-outline.badge`), no `key`. Types: `@deepseek-ai/dsh-client-ui-layout/client` (module augmentation only). Render site verified in layout `lib/client.js` line ~236. - Conversation scrollport: `[data-conversation-scroll]` (ui-conversation). - Chat flow rows: each row div carries `data-chat-anchor-key=` and `data-chat-flow-kind=` (ui-conversation `ChatNodeSeat`). - Chat view tab id is `"chat"`, registered at `order: 0` (the FIRST tab in `[role="tablist"]`); trajectory is `order: 10`. Tab buttons are `button[role="tab"]` inside `[role="tablist"]`; clicking one calls `setView(id)`. - Layout phases: the active app column has `[data-phase="active"]`; the CSS var `--dsh-sidebar-width` exists. Panel CSS may rely on these (see agent-teams panel CSS for the pattern), but do NOT depend on hashed class names of other packages. ### 1.4 Platform module table (externals for tsdown) Verified `require(...)` calls inside official client bundles (rc.6): `@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-runtime/client`, `@deepseek-ai/dsh-client-ui-slots`, `@deepseek-ai/dsh-client-ui-primitives`, `@deepseek-ai/dsh-client-ui-attachment`, `react`, `react/jsx-runtime`, `react-dom`, `react-dom/client`. Use these as `external` in tsdown. Do NOT value-import any other cross-plugin package (purity gate; the browser module table would reject it). Type-only imports are erased and allowed, e.g. `import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'` and `import type {} from '@deepseek-ai/dsh-client-ui-layout/client'` (declaration merge). ### 1.5 Runtime data (verified) - `ctx.sessions.list` is an `ObservableSnapshot`: `getSnapshot()` → `{ current: SessionId | undefined, byId, ... }`, `subscribe`. (agent-teams passes it into its panel component and subscribes with `useSyncExternalStore`.) - Current session object: `ctx.sessions.binding(sessionId)?.session` → `Session` with `subscribe(listener)` (uSES) and `getSnapshot()` → `ConversationSnapshot` with: - `chat.nodes` — Map-like `ChatNodeStore` (`get(key)`, `values()`) - `chat.order` — `readonly string[]` of node keys in flow order - `chat.legacy.nodes` — array form - `hasMore: boolean`, `loadingOlder: boolean` - `running`, `blank`, `openState`, `pending`, `queue`, ... - `session.loadOlder()` — pages up (returns Promise, guarded by hasMore). - Chat node shape (from `chatNode()` in ui-conversation): `{ key, kind, id, target: 'chat', anchorSeq, location, visibility, data }`. - user-message node: `kind: 'user'` (also `'steering'` for mid-turn steers, `'context'` for injected context). `data` = `{ kind, seq, time (unix ms), content: ContentBlock[], source }` where `ContentBlock` is `{ type: 'text', text } | { type: 'image', ... } | ...`. - `node.key` = `conversationContextKey(defKind, businessId)` with format `:` — e.g. `13:input-message` for user messages. Use `node.key` verbatim for DOM lookup. - `node.location`: `{kind:'turn', turn: TurnLocation} | {kind:'step', turn, step} | {kind:'session'} | {kind:'unresolved'}`; turn number = `location.kind === 'step' ? location.turn.turn : location.kind === 'turn' ? location.turn.turn : undefined`. - Locale: `ctx.locale.register(NS, { zh, en })` then `ctx.locale.bind(NS)` → `t(key)`. - Open a session: `ctx.sessions.open(id)`. ### 1.6 Versions (peerDependencies / devDependencies) - **peerDependencies: `react` `^18.2.0` ONLY.** The `@deepseek-ai/*` packages are deliberately NOT peers (REVISED after the first install report): the registry's `latest` tag for the client sub-packages is stale (`0.0.1-rc.1`) while `0.1.0-rc.6` lives on `next`, so a `^0.1.0-rc.6` peer makes pnpm fail with `ERR_PNPM_NO_MATCHING_VERSION` on a fresh profile. Nothing imports those packages at runtime anyway — the browser module loader resolves platform modules from the profile's own (channel-matched) installation, and the host half imports nothing. `react-dom` is also dropped (unused since the shell.overlay rewrite). - Typechecking still links the local `@deepseek-ai/*` types via `scripts/link-types.mjs` (dev-only symlinks, never shipped — see §1.7). - devDeps from public npm: `typescript@^5.9.3`, `tsdown@0.22.2`, `lightningcss@^1.33.0`, `@types/react@~18.3.1`, `@types/react-dom@^19.2.4`, `react@^18.2.0`, `react-dom@^18.2.0` (react needed as devDep for jsx types). - Node `^22.19.0 || >=24` (engines). ### 1.7 Typecheck strategy (no registry access to @deepseek-ai) `@deepseek-ai/*` packages are NOT re-installable from the public registry here; the profile flat dir already has everything. Symlink (do NOT commit): ```sh mkdir -p node_modules/@deepseek-ai for p in cordis dsh-client-runtime dsh-client-ui-layout dsh-client-ui-slots dsh-client-ui-primitives dsh-client-locale dsh-client-web-react; do ln -sfn /Users/liziqing/.dsh/profiles/node_modules/@deepseek-ai/$p node_modules/@deepseek-ai/$p done ``` Provide a `scripts/link-types.mjs` that does this (idempotent), and a `pnpm dev:types` script. `.gitignore` must exclude `node_modules/` so the symlinks never ship. Two tsc programs (host and client) — see skill §6.1 / guide §3.1. The client program compiles `src/client/**` with `lib: ["ES2022","DOM","DOM.Iterable"]`, `jsx: "react-jsx"`, `types: []`, `allowImportingTsExtensions` + `rewriteRelativeImportExtensions` (TS 5.7+). Output: host → `lib/`, client → `lib/client/`. tsdown then bundles `lib/client/index.js` → `lib/client.js` (with banner/footer wrapper + sourcemap; keep `lib/client/*.js` files — the verify script imports the pure-logic module from there). ### 1.8 CSS Keep it simple and HMR-safe: plain CSS string in a `.ts` module, injected in `apply()` via `ctx.effect` with a `