@chestnut23/dsh-conversation-outline 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,455 @@
1
+ # dsh-conversation-outline — Implementation Spec
2
+
3
+ A DeepSeek Harness (DSH) client plugin that adds a **Codex-style conversation outline**:
4
+ a floating panel listing every user question in the current conversation, with
5
+ search and **click-to-jump** (switches to the Chat view, scrolls to the message,
6
+ flashes a highlight). Bilingual zh-CN / en.
7
+
8
+ This spec is the single source of truth for the team. All paths are relative to
9
+ the repo root `/Users/liziqing/Programs/agents/dsh-conversation-outline`.
10
+
11
+ ---
12
+
13
+ ## 1. Facts about DSH (verified against installed rc.6 packages)
14
+
15
+ Reference material on disk (READ ONLY):
16
+ - DSH packages (runtime + types): `/Users/liziqing/.npm/_npx/1e7f6d9597241db0/node_modules/@deepseek-ai/`
17
+ - Web profile flat node_modules (symlinks to the above, plus react):
18
+ `/Users/liziqing/.dsh/profiles/node_modules/`
19
+ - Working third-party plugin example (published, MIT): `@nanmicoder/dsh-agent-teams`
20
+ installed at `/Users/liziqing/.dsh/profiles/web/node_modules/@nanmicoder/dsh-agent-teams/`
21
+ — its `lib/client.js` shows the exact loader-wrapper shape, panel CSS patterns,
22
+ and `apply(ctx)` portal pattern.
23
+ - Authoritative dev skill: `/tmp/dsh-plugin-skill.md` (fetched from
24
+ NanmiCoder/dsh-agent-teams `skills/dsh-plugin-development/SKILL.md`) and
25
+ `/tmp/developing-dsh-plugins.md` (the long-form guide). Read both.
26
+
27
+ ### 1.1 Client plugin contract
28
+
29
+ - A bundle = npm package whose `package.json` has:
30
+ - `dsh.bundle.patch` → `./cordis.patch.yml` (bundle patch layer)
31
+ - `dsh.client` = `{ platform: "web", inject: ["@deepseek-ai/dsh-client-runtime", ...] }`
32
+ (`inject` is informational metadata; the REAL activation deps come from the
33
+ client bundle's exported `inject` array)
34
+ - `exports["./client"]` → the built browser bundle
35
+ - `cordis.patch.yml` is a top-level YAML array: `- insert: [{id, name, config}]`.
36
+ `name` MUST equal the package name (roster resolves `require.resolve('<name>/package.json')`).
37
+ - The browser bundle is a CJS closure factory:
38
+ ```js
39
+ window.__ModuleLoader__.load({ id: "<package-name>", factory: (require) => {
40
+ var module = { exports: {} }; var exports = module.exports;
41
+ /* ...bundled code... */
42
+ return module.exports;
43
+ } });
44
+ ```
45
+ Built with tsdown@0.22 (cjs, platform browser), `external` = the platform
46
+ module table (see 1.4), everything else inlined, sourcemap on, `clean: false`
47
+ (must not wipe the tsc host output).
48
+ - Client plugin entry (`src/client/index.tsx`, MUST be `.tsx` to use JSX) exports:
49
+ ```ts
50
+ export const inject = ['slots', 'sessions', 'locale'] // cordis services to wait for
51
+ export function apply(ctx: ClientContext): void { ... }
52
+ ```
53
+ - HMR/dispose: every DOM node, React root, style tag, listener must be owned by
54
+ `ctx.effect(() => disposer, 'label')`.
55
+
56
+ ### 1.2 The host half
57
+
58
+ Even a client-only plugin needs a host row in the composition (the roster scans
59
+ Loader entries for `dsh.client` packages). The host module (`src/index.ts`, tsc
60
+ → `lib/index.js`) must export a plugin body — a function or `{apply}` object
61
+ (cordis throws otherwise). Use a minimal:
62
+ ```ts
63
+ import type { Context } from '@deepseek-ai/cordis'
64
+ export const name = 'dsh-conversation-outline'
65
+ export const inject: string[] = []
66
+ export function apply(ctx: Context): void {
67
+ // client-only plugin: nothing to do on the host
68
+ }
69
+ ```
70
+ (Keep a `Config` schema optional with a `z.object({})` default so the row config
71
+ is valid; follow the agent-teams shape.)
72
+
73
+ ### 1.3 UI seams (verified in rc.6)
74
+
75
+ - `shell.overlay` — **the** seat for frame-wide floating surfaces:
76
+ `kind: 'list'`, `scope: 'root'`, declared by `@deepseek-ai/dsh-client-ui-layout`.
77
+ The layer is click-through by default; entries opt back into pointer events.
78
+ Register with a fresh `id` (e.g. `dsh-conversation-outline.badge`), no `key`.
79
+ Types: `@deepseek-ai/dsh-client-ui-layout/client` (module augmentation only).
80
+ Render site verified in layout `lib/client.js` line ~236.
81
+ - Conversation scrollport: `[data-conversation-scroll]` (ui-conversation).
82
+ - Chat flow rows: each row div carries `data-chat-anchor-key=<node.key>` and
83
+ `data-chat-flow-kind=<kind>` (ui-conversation `ChatNodeSeat`).
84
+ - Chat view tab id is `"chat"`, registered at `order: 0` (the FIRST tab in
85
+ `[role="tablist"]`); trajectory is `order: 10`. Tab buttons are
86
+ `button[role="tab"]` inside `[role="tablist"]`; clicking one calls
87
+ `setView(id)`.
88
+ - Layout phases: the active app column has `[data-phase="active"]`; the CSS var
89
+ `--dsh-sidebar-width` exists. Panel CSS may rely on these (see agent-teams
90
+ panel CSS for the pattern), but do NOT depend on hashed class names of other
91
+ packages.
92
+
93
+ ### 1.4 Platform module table (externals for tsdown)
94
+
95
+ Verified `require(...)` calls inside official client bundles (rc.6):
96
+ `@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-runtime/client`,
97
+ `@deepseek-ai/dsh-client-ui-slots`, `@deepseek-ai/dsh-client-ui-primitives`,
98
+ `@deepseek-ai/dsh-client-ui-attachment`, `react`, `react/jsx-runtime`,
99
+ `react-dom`, `react-dom/client`.
100
+
101
+ Use these as `external` in tsdown. Do NOT value-import any other cross-plugin
102
+ package (purity gate; the browser module table would reject it). Type-only
103
+ imports are erased and allowed, e.g.
104
+ `import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'` and
105
+ `import type {} from '@deepseek-ai/dsh-client-ui-layout/client'` (declaration
106
+ merge).
107
+
108
+ ### 1.5 Runtime data (verified)
109
+
110
+ - `ctx.sessions.list` is an `ObservableSnapshot<SessionListState>`:
111
+ `getSnapshot()` → `{ current: SessionId | undefined, byId, ... }`, `subscribe`.
112
+ (agent-teams passes it into its panel component and subscribes with
113
+ `useSyncExternalStore`.)
114
+ - Current session object: `ctx.sessions.binding(sessionId)?.session` → `Session`
115
+ with `subscribe(listener)` (uSES) and `getSnapshot()` →
116
+ `ConversationSnapshot` with:
117
+ - `chat.nodes` — Map-like `ChatNodeStore` (`get(key)`, `values()`)
118
+ - `chat.order` — `readonly string[]` of node keys in flow order
119
+ - `chat.legacy.nodes` — array form
120
+ - `hasMore: boolean`, `loadingOlder: boolean`
121
+ - `running`, `blank`, `openState`, `pending`, `queue`, ...
122
+ - `session.loadOlder()` — pages up (returns Promise, guarded by hasMore).
123
+ - Chat node shape (from `chatNode()` in ui-conversation):
124
+ `{ key, kind, id, target: 'chat', anchorSeq, location, visibility, data }`.
125
+ - user-message node: `kind: 'user'` (also `'steering'` for mid-turn steers,
126
+ `'context'` for injected context). `data` =
127
+ `{ kind, seq, time (unix ms), content: ContentBlock[], source }` where
128
+ `ContentBlock` is `{ type: 'text', text } | { type: 'image', ... } | ...`.
129
+ - `node.key` = `conversationContextKey(defKind, businessId)` with format
130
+ `<kindLen>:<kind><id>` — e.g. `13:input-message<uuid>` for user messages.
131
+ Use `node.key` verbatim for DOM lookup.
132
+ - `node.location`: `{kind:'turn', turn: TurnLocation} | {kind:'step', turn, step} |
133
+ {kind:'session'} | {kind:'unresolved'}`; turn number =
134
+ `location.kind === 'step' ? location.turn.turn : location.kind === 'turn' ? location.turn.turn : undefined`.
135
+ - Locale: `ctx.locale.register(NS, { zh, en })` then `ctx.locale.bind(NS)` → `t(key)`.
136
+ - Open a session: `ctx.sessions.open(id)`.
137
+
138
+ ### 1.6 Versions (peerDependencies / devDependencies)
139
+
140
+ - **peerDependencies: `react` `^18.2.0` ONLY.** The `@deepseek-ai/*` packages are
141
+ deliberately NOT peers (REVISED after the first install report): the registry's
142
+ `latest` tag for the client sub-packages is stale (`0.0.1-rc.1`) while `0.1.0-rc.6`
143
+ lives on `next`, so a `^0.1.0-rc.6` peer makes pnpm fail with
144
+ `ERR_PNPM_NO_MATCHING_VERSION` on a fresh profile. Nothing imports those packages at
145
+ runtime anyway — the browser module loader resolves platform modules from the
146
+ profile's own (channel-matched) installation, and the host half imports nothing.
147
+ `react-dom` is also dropped (unused since the shell.overlay rewrite).
148
+ - Typechecking still links the local `@deepseek-ai/*` types via `scripts/link-types.mjs`
149
+ (dev-only symlinks, never shipped — see §1.7).
150
+ - devDeps from public npm: `typescript@^5.9.3`, `tsdown@0.22.2`, `lightningcss@^1.33.0`,
151
+ `@types/react@~18.3.1`, `@types/react-dom@^19.2.4`, `react@^18.2.0`,
152
+ `react-dom@^18.2.0` (react needed as devDep for jsx types).
153
+ - Node `^22.19.0 || >=24` (engines).
154
+
155
+ ### 1.7 Typecheck strategy (no registry access to @deepseek-ai)
156
+
157
+ `@deepseek-ai/*` packages are NOT re-installable from the public registry here;
158
+ the profile flat dir already has everything. Symlink (do NOT commit):
159
+ ```sh
160
+ mkdir -p node_modules/@deepseek-ai
161
+ 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
162
+ ln -sfn /Users/liziqing/.dsh/profiles/node_modules/@deepseek-ai/$p node_modules/@deepseek-ai/$p
163
+ done
164
+ ```
165
+ Provide a `scripts/link-types.mjs` that does this (idempotent), and a
166
+ `pnpm dev:types` script. `.gitignore` must exclude `node_modules/` so the
167
+ symlinks never ship.
168
+
169
+ Two tsc programs (host and client) — see skill §6.1 / guide §3.1. The client
170
+ program compiles `src/client/**` with `lib: ["ES2022","DOM","DOM.Iterable"]`,
171
+ `jsx: "react-jsx"`, `types: []`, `allowImportingTsExtensions` +
172
+ `rewriteRelativeImportExtensions` (TS 5.7+). Output: host → `lib/`, client →
173
+ `lib/client/`. tsdown then bundles `lib/client/index.js` → `lib/client.js`
174
+ (with banner/footer wrapper + sourcemap; keep `lib/client/*.js` files — the
175
+ verify script imports the pure-logic module from there).
176
+
177
+ ### 1.8 CSS
178
+
179
+ Keep it simple and HMR-safe: plain CSS string in a `.ts` module, injected in
180
+ `apply()` via `ctx.effect` with a `<style data-plugin="dsh-conversation-outline"
181
+ data-plugin-css="dsh-conversation-outline/panel.css">` tag (same pattern the
182
+ shipped bundles generate; HMR removes owned `style[data-plugin]` tags).
183
+ Use stable prefixed class names (`dso_*`) and DSH theme vars (`--dsw-alias-*`,
184
+ `--dsw-specific-*`, `--dsh-sidebar-width`) — see agent-teams' injected CSS for
185
+ the exact vars. CSS Modules via lightningcss is allowed but optional; the
186
+ manual style-tag approach is preferred for lower build risk.
187
+
188
+ ---
189
+
190
+ ## 2. Product spec (the panel)
191
+
192
+ ### 2.1 Surfaces (REVISED — rail + hover panel, no badge)
193
+
194
+ Per user feedback the top-right capsule badge was replaced: the AgentTeams
195
+ plugin already occupies that corner. The outline now lives on the right EDGE.
196
+
197
+ 1. **Rail** (always mounted, `shell.overlay` entry, id
198
+ `dsh-conversation-outline.rail`): a thin fixed strip on the right edge
199
+ (`right: 0`, vertically centered `top: 50%`), one small horizontal bar per
200
+ user question in chronological order top→bottom — a conversation minimap.
201
+ Capped at 60 bars; overflow folds into a top `+N` marker. Bars are real
202
+ `<button>`s with localized `aria-label` (`跳到第 {n} 个问题`); clicking a
203
+ bar jumps. The strip itself is `role="group"` with `aria-label`, focusable
204
+ (focus/Enter opens the panel for keyboard users). Hidden (renders nothing)
205
+ when there is no current session or the session is blank.
206
+ 2. **Panel** (rendered only while open): fixed right overlay
207
+ (`right: 16px`, vertically centered), `width: min(340px, calc(100vw - 24px))`,
208
+ `max-height: min(70vh, 600px)`, rounded 16px card, internal scroll, backdrop
209
+ blur, DSH theme vars. Sections:
210
+ - Header: title `会话大纲 / Outline` + count + close button (×).
211
+ - Search input (localized placeholder; case-insensitive filter).
212
+ - Question list, **chronological**: one row per user question — `#<turn>`
213
+ badge, **single-line truncated** text preview (the question's opening
214
+ words; longer text ellipsizes), trailing `HH:MM` time, copy button,
215
+ `追问`/`steer` tag for steering messages. Clicking a row = jump (2.2).
216
+ - Footer: `加载更早 / Load older` when `snapshot.hasMore` (disabled while
217
+ `loadingOlder`); calls `session.loadOlder()`.
218
+ - Empty state when no user messages in the loaded window.
219
+ 3. Behavior rules:
220
+ - **Hover-open**: mouseenter on the rail (or panel) opens the panel;
221
+ mouseleave schedules a collapse after a 240ms grace (canceled on re-enter,
222
+ so the pointer can travel rail→panel). Touch devices: tapping the strip
223
+ (non-bar area) pins/unpins the panel; `Escape` or × closes. The panel is
224
+ a pure OVERLAY — no column yield, no layout shift.
225
+ - Follows `sessions.list.current`; on session change the panel closes and
226
+ pending jump work is canceled (navigate → collapse).
227
+ - While a session is running, new user messages appear live (snapshot
228
+ subscription covers it; the rail grows a bar per question).
229
+
230
+ ### 2.2 Jump-to-message algorithm (core feature)
231
+
232
+ On row click, given the target `node.key`:
233
+
234
+ 1. Ensure the conversation page is mounted: abort when
235
+ `document.querySelector('[data-conversation-scroll]')` is missing. If a
236
+ conversation-root header tablist exists (found by walking up from
237
+ `scrollport.parentElement` — the scrollport itself may contain other
238
+ tablists such as the trajectory event-details tabs), click its first
239
+ `button[role="tab"]` (chat is `order: 0` — always first; `setView("chat")`
240
+ is idempotent). A profile without extra views has no tablist — fine, the
241
+ rows are already visible.
242
+ 2. Wait for the target row to render: poll with `requestAnimationFrame` up to
243
+ ~1500ms for `[data-chat-anchor-key="<node.key>"]` inside the chat list. The
244
+ row is guaranteed to be in the loaded window (it comes from the snapshot),
245
+ so this is a render-timing wait.
246
+ 3. Scroll: `const scrollport = row.closest('[data-conversation-scroll]') ?? document.querySelector('[data-conversation-scroll]')`; compute
247
+ `row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top`
248
+ and set `scrollport.scrollTop += flowTop - 96` (leave ~96px headroom under
249
+ the sticky header/composer area). Use smooth behavior unless
250
+ `prefers-reduced-motion`.
251
+ 4. Flash highlight: set `row.dataset.dshOutlineFlash = 'true'`; CSS
252
+ `[data-dsh-outline-flash]{ animation: dso-flash 1.8s ease-out }` (a
253
+ background/outline pulse using `--dsw-alias-state-business-primary`);
254
+ remove the attribute after ~1.9s (timeout, cleaned on unmount).
255
+ 5. Close the panel.
256
+
257
+ All DOM work must be guarded (elements may be missing) and cleaned up on
258
+ dispose (timeouts/RAF cancelled in the effect disposer).
259
+
260
+ ### 2.3 Pure logic module (`src/client/outline.ts`, NO DOM, NO React)
261
+
262
+ Keep the derivations as importable pure functions so `scripts/verify.mjs` can
263
+ test them in Node by importing `lib/client/outline.js`:
264
+
265
+ - `flattenQuestionText(content: ContentBlock[]): string` — join text blocks,
266
+ image blocks → `[图片]` / `[image]`, trim, collapse whitespace.
267
+ - `collectQuestions(snapshot): OutlineItem[]` — walk `snapshot.chat.order`,
268
+ look up `snapshot.chat.nodes.get(key)`, keep `kind === 'user' | 'steering'`,
269
+ skip empty text; item =
270
+ `{ key, kind, seq, time, turn: number | undefined, text }`.
271
+ - `filterQuestions(items, query): OutlineItem[]` — case-insensitive substring.
272
+ - `formatTime(ms): string` — `HH:MM` local.
273
+ - `isJumpTargetRow(row: Element, key: string): boolean` — pure DOM predicate
274
+ (used by the jump loop; testable with a fake object in node).
275
+
276
+ ### 2.4 Localization
277
+
278
+ Namespace `dsh-conversation-outline`; keys at least: title, count (with
279
+ `{count}`), searchPlaceholder, empty, loadOlder, loadingOlder, copy, copied,
280
+ steerTag, jumpFailed (console only). zh-CN + en dictionaries, registered via
281
+ `ctx.locale.register(NS, { zh, en })`.
282
+
283
+ ---
284
+
285
+ ## 3. Repo layout (create exactly this)
286
+
287
+ ```
288
+ dsh-conversation-outline/
289
+ ├── package.json
290
+ ├── cordis.patch.yml
291
+ ├── tsconfig.json # host program (excludes src/client)
292
+ ├── tsconfig.client.json # client program
293
+ ├── tsdown.config.ts # client bundle (wrapper banner/footer)
294
+ ├── .gitignore # node_modules, lib, *.log, .DS_Store
295
+ ├── LICENSE # MIT (owner placeholder "your name")
296
+ ├── README.md # written by docs member
297
+ ├── docs/
298
+ │ ├── implementation-spec.md # this file
299
+ │ └── publishing-guide.md # written by docs member
300
+ ├── scripts/
301
+ │ ├── link-types.mjs # symlink @deepseek-ai types (dev-only)
302
+ │ └── verify.mjs # offline smoke: manifest/patch/bundle shape + pure logic
303
+ └── src/
304
+ ├── index.ts # minimal host apply
305
+ ├── event-types.ts # NOT needed (no custom events) — skip
306
+ └── client/
307
+ ├── index.tsx # apply(): locale, style tag, shell.overlay registration, badge+panel
308
+ ├── OutlinePanel.tsx # badge + panel components
309
+ ├── outline.ts # pure logic (2.3)
310
+ ├── locales.ts # zh/en dictionaries
311
+ └── styles.ts # CSS string + injectStyle(ctx) helper
312
+ ```
313
+
314
+ ### 3.1 package.json essentials
315
+
316
+ ```jsonc
317
+ {
318
+ "name": "dsh-conversation-outline",
319
+ "version": "0.1.0",
320
+ "type": "module",
321
+ "main": "lib/index.js",
322
+ "types": "lib/types/index.d.ts",
323
+ "exports": {
324
+ ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
325
+ "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" },
326
+ "./cordis.patch.yml": "./cordis.patch.yml",
327
+ "./package.json": "./package.json"
328
+ },
329
+ "files": ["lib", "cordis.patch.yml", "README.md", "LICENSE"],
330
+ "dsh": {
331
+ "bundle": { "patch": "./cordis.patch.yml" },
332
+ "client": { "inject": ["@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-layout"], "platform": "web" }
333
+ },
334
+ "scripts": {
335
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && tsdown",
336
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
337
+ "verify": "node scripts/verify.mjs",
338
+ "dev:types": "node scripts/link-types.mjs"
339
+ },
340
+ "peerDependencies": {
341
+ "@deepseek-ai/cordis": "^4.0.1",
342
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
343
+ "@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.6",
344
+ "react": "^18.2.0",
345
+ "react-dom": "^18.2.0"
346
+ },
347
+ "devDependencies": {
348
+ "@types/react": "~18.3.1",
349
+ "@types/react-dom": "^19.2.4",
350
+ "lightningcss": "^1.33.0",
351
+ "react": "^18.2.0",
352
+ "react-dom": "^18.2.0",
353
+ "tsdown": "0.22.2",
354
+ "typescript": "^5.9.3"
355
+ },
356
+ "engines": { "node": "^22.19.0 || >=24" },
357
+ "license": "MIT",
358
+ "keywords": ["dsh", "dsh-plugin", "deepseek-harness", "conversation", "outline", "navigation"],
359
+ "repository": { "type": "git", "url": "git+https://github.com/<owner>/dsh-conversation-outline.git" }
360
+ }
361
+ ```
362
+
363
+ ### 3.2 cordis.patch.yml
364
+
365
+ ```yaml
366
+ - insert:
367
+ - id: dsh-conversation-outline
368
+ name: dsh-conversation-outline
369
+ config: {}
370
+ ```
371
+
372
+ ### 3.3 tsdown.config.ts
373
+
374
+ Entry `{ client: 'lib/client/index.js' }`, outDir `lib`, `format: 'cjs'`,
375
+ `platform: 'browser'`, `dts: false`, `sourcemap: true`, `clean: false`,
376
+ `external` = the platform table (1.4), `define` NODE_ENV production,
377
+ `outputOptions`: `entryFileNames: 'client.js'`, `banner: 'window.__ModuleLoader__.load({ id: "dsh-conversation-outline", factory: (require) => {'`,
378
+ `footer: 'return module.exports; } });'`,
379
+ `intro: 'var module = { exports: {} }; var exports = module.exports;'`.
380
+ No CSS-modules plugin needed (manual style tag); keep a simple purity guard
381
+ plugin that throws on value-imports of `@deepseek-ai/*` packages outside the
382
+ external table.
383
+
384
+ ---
385
+
386
+ ## 4. Verification plan
387
+
388
+ ### 4.1 Offline (engineer, then researcher re-runs)
389
+
390
+ 1. `pnpm dev:types` (symlink), `pnpm install`, `pnpm typecheck`, `pnpm build`.
391
+ 2. `node scripts/verify.mjs`:
392
+ - manifest/exports/files consistency (every exported path exists);
393
+ - `cordis.patch.yml` parses, first insert id/name match package name;
394
+ - `lib/client.js` starts with the `window.__ModuleLoader__.load` wrapper;
395
+ - no absolute machine paths inside `lib/` (grep `/Users/`);
396
+ - pure-logic assertions from `lib/client/outline.js`: flatten text incl.
397
+ image blocks, collect/filter/sort questions from a fixture snapshot,
398
+ formatTime, turn extraction.
399
+
400
+ ### 4.2 Integration (researcher)
401
+
402
+ Use a SCRATCH profile + temporary `DSH_HOME` + different port — NEVER touch the
403
+ running instance (the user's GUI at 127.0.0.1:3080 serves the current session;
404
+ restarting it would kill the session. Also do not start a replacement server on
405
+ 3080).
406
+
407
+ 1. `pnpm build` in the repo.
408
+ 2. `tmp=$(mktemp -d)`; `DSH_HOME=$tmp npx -p @deepseek-ai/dsh dsh plugin --profile scratch add /Users/liziqing/Programs/agents/dsh-conversation-outline`
409
+ (network: npm registry reachable for the dsh CLI; if the npx download fails,
410
+ fall back to the already-installed CLI:
411
+ `DSH_HOME=$tmp node /Users/liziqing/.npm/_npx/1e7f6d9597241db0/node_modules/@deepseek-ai/dsh/lib/bin.js plugin --profile scratch add <path>`).
412
+ 3. `DSH_HOME=$tmp <dsh> --profile scratch --dump-config` — assert the
413
+ `dsh-conversation-outline` row appears with id/name/config.
414
+ 4. Boot the scratch web instance on a free port (e.g. 3199) in the background:
415
+ `DSH_HOME=$tmp <dsh> --profile scratch --port 3199` (check `--help` flags if
416
+ needed), then `curl http://127.0.0.1:3199/` and assert
417
+ `window.__DSH_BOOT__` contains an entry
418
+ `{"id":"dsh-conversation-outline","url":"/plugins/dsh-conversation-outline/client.js"...}`,
419
+ and `curl http://127.0.0.1:3199/plugins/dsh-conversation-outline/client.js`
420
+ returns 200 with the loader wrapper. Kill the instance afterwards.
421
+ 5. Report exact commands + outputs in the task output.
422
+
423
+ ### 4.3 Review (reviewer)
424
+
425
+ Check against the skill checklist (§9 完成标准): minimal surface; manifest/
426
+ exports/patch/products consistent; inject boundaries; effect ownership
427
+ (dispose of root/DOM/style/listeners/timeouts/RAF); purity of client imports;
428
+ no host/client type pollution (two programs); a11y (aria, focus-visible,
429
+ Escape, reduced motion); the jump algorithm's failure modes (missing row,
430
+ missing tablist, session switched mid-jump, blank session); live updates;
431
+ load-older; i18n coverage. Mark findings as must-fix / nice-to-have and hand
432
+ back to the engineer for fixes.
433
+
434
+ ### 4.4 Final (captain)
435
+
436
+ Install into the user's real web profile (`dsh plugin --profile web add <path>`)
437
+ — this only edits profile files; the plugin takes effect after the user
438
+ restarts their GUI (document this). Keep the scratch instance killed and temp
439
+ dirs cleaned.
440
+
441
+ ---
442
+
443
+ ## 5. Order of work
444
+
445
+ 1. engineer: scaffold (3.x) → implement (2.x) → build green (4.1).
446
+ 2. researcher: 4.2 integration.
447
+ 3. reviewer: 4.3; engineer fixes must-fix findings.
448
+ 4. docs: README (install via `dsh plugin --profile web add <pkg|github:...>`,
449
+ usage, dev loop with HMR: `tsdown --watch` + `pnpm run dev:web` note,
450
+ screenshots placeholder) + `docs/publishing-guide.md` (GitHub publish:
451
+ create repo, license choice — MIT recommended, .gitignore, README, release
452
+ workflow: npm publish with `prepublishOnly: pnpm build && pnpm verify`,
453
+ GitHub-only install without npm, versioning, CHANGELOG, issues/PR templates,
454
+ publishing scoped vs unscoped, npm name collision fallback to a scope).
455
+ 5. captain: final assembly, install into web profile, wrap-up report.