@treeseed/dsh-chapters 0.1.13 → 0.1.15

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,97 @@
1
+ # Open items — 2026-10-11
2
+
3
+ Handoff for a fresh session. Everything below is *scoped and reachable*; none of it is a unknown
4
+ defect. Shipped state: **0.1.13** (tag = npm `latest` = the user's installed profile), hosted CI green
5
+ (deterministic gate + eight-project tape chain), and the untagged commit after it (`5bdc11b`) changes
6
+ only the e2e stub and this record.
7
+
8
+ Order matters: 1 and 2 are the ones that change what we are allowed to *claim*.
9
+
10
+ ## 1. Live-model proof of the plans journey
11
+
12
+ **What is missing.** Every acceptance claim for plan carriage runs off a tape distilled from the
13
+ scripted upstream (`tests/e2e/stub-model.ts`), because this box's model server was in a CUDA-OOM
14
+ restart loop. The behaviour at our seam is proven; "a real model plans, a human approves, the
15
+ continuation carries it" is not.
16
+
17
+ **What attempt 5 already bought** (see `tests/fixtures/model-tape/plans/PROVENANCE.md`): a real
18
+ `exit_plan_mode` submission, the host review answered by a real click, `plan/mode` flipping
19
+ active→inactive on approval, the archived chapter carrying the plan, and the **branch** child receiving
20
+ zero directive bytes — all with a live model. What it did *not* buy: the continuation leg, because the
21
+ model called `chapters_segment` and then answered in prose.
22
+
23
+ **How to close it.** The container is now `RestartCount=0` with `N_PARALLEL` removed.
24
+
25
+ 1. Prove a **completion**, not a health check: `curl` a real ~14K-token chat request and confirm it
26
+ finishes. `/v1/models` answering proves nothing (measured repeatedly).
27
+ 2. Run the journey live: `E2E_MODEL=live E2E_PORT=41850 E2E_THRESHOLD=0.99 E2E_NO_AUX_MODEL=1
28
+ npx playwright test -c tests/e2e/playwright.config.ts --project=plans`
29
+ 3. Expect cost: ~15 tok/s decode means several minutes per turn, and with one slot this session's own
30
+ turns queue behind it. Do it when the box is otherwise free.
31
+ 4. Record the outcome in `PROVENANCE.md` either way — including a failure and its cause. Do **not**
32
+ promote the scripted tape's status based on a live run that only half-completed.
33
+
34
+ **Honest limit.** A live journey is inherently non-deterministic (attempt 5 proved the model may chain
35
+ two calls or may not). It is evidence, never a CI gate. Do not try to make it one.
36
+
37
+ ## 2. The seeded-gate row in a browser
38
+
39
+ **What is missing.** The seeded `plan/mode` into a continuation child (§ 10 of
40
+ `docs/plans-carriage.md`) is proven by a kernel probe and by deterministic rows, but never *observed*
41
+ in a real session.
42
+
43
+ **Already done, and it is the hard part:** the session shape was reached live-in-the-harness and
44
+ recorded — `plan/mode {active:true}` at seq 4, `turn/end` at 21, **no** tool call. So a session can be
45
+ in plan mode with a completed turn, which gives the deterministic door a real ceiling to archive
46
+ against. The attempt was reverted unshipped for two harness reasons, both now fixed on `main`:
47
+
48
+ - the stub had to learn to answer **plainly** in plan mode (`kind: 'plain'`), and its "is the human
49
+ asking for a plan?" test first read the **last** user message — but the harness appends its own
50
+ injected user messages (workspace instructions, runtime snapshot, skill catalog) *after* the human's,
51
+ so the ask was invisible and the next step hung 15 minutes on a review card that could not appear.
52
+ Now it scans every user-role message, the trigger is a whole sentence so the plan-mode *policy* text
53
+ cannot set it off, and both properties are pinned by rows in `tests/unit/stub-model.test.ts`.
54
+ - a **stale stub process** was serving pre-edit logic, which made a harness bug look like a product
55
+ one. Restart the upstream before trusting any distillation.
56
+
57
+ **Remaining work.** Re-add the leg (draft is in this file's history of the repo: new session → `/plan`
58
+ with a plain ask → wait for `plan/mode` + `turn/end` → `/chapters-continue` → poll `ch-` children for a
59
+ `plan/mode` event at seq 1 with `active:true`, assert exactly one `dsh-chapters` user/message, and
60
+ assert the notice carries **no** plan section since no plan was submitted). Then re-distill the plans
61
+ tape with a **fresh** stub and replay it. Roughly 15–20 minutes.
62
+
63
+ ## 3. Per-message anchoring (needs a product decision, not just code)
64
+
65
+ `deriveRanges` already accepts any anchor, and the fork/continue doors archive from the watermark. What
66
+ does not exist is a way for a human to say *"branch from this message"*, because the button acts at
67
+ conversation end. Two open questions before code:
68
+
69
+ - where the affordance lives (message action row vs selection in the composer), and
70
+ - **how the child is told what it did not inherit** — an anchored branch silently drops the tail, which
71
+ is exactly the kind of unstated loss this project refuses. It needs a line in the notice.
72
+
73
+ Scope it as its own plan; do not fold it into a release.
74
+
75
+ ## 4. Default-preset shipping policy (the user's call)
76
+
77
+ Today `chapters` is opt-in: the profile must name it, and the README/site tell users how. The
78
+ alternative — make it the default agent preset — changes every new session's header. That is a
79
+ distribution decision with a cache consequence (a recomposed header is measured at ~12K tokens on a
80
+ 32K model) and it belongs to the human, not to an agent. Ask before touching it.
81
+
82
+ ## 5. Human-judgment verification rows
83
+
84
+ Still open and still human-only: readability of a real checkpoint in a real browser, and whether the
85
+ notice/preamble *feels* right to work in. These are not automatable and should not be faked with
86
+ assertions. `docs/verify.md` lists them; run them after installing, in the project you actually care
87
+ about.
88
+
89
+ ## Also worth knowing
90
+
91
+ - **Durability caveat for a new project:** with no pool linked, the archive lives only in the
92
+ workspace. `/chapters-link <path-or-url>` is the difference between "this machine remembers" and
93
+ "the project remembers".
94
+ - **Search now answers unlinked** (0.1.12) and says which door it used. If a result ever looks
95
+ machine-only when you expected the pool, that note is the first thing to read.
96
+ - **`/chapters-continue` vs `/chapters-fork`** is the intent line: continue carries the operative plan
97
+ and may seed plan mode; fork carries nothing.
@@ -362,7 +362,7 @@ happened in these notes before it was caught.
362
362
  | `scripts/dsh-scratch.sh` | **done** — refuses any DSH_HOME under the live `~/.dsh`; the near-miss is in FINDINGS |
363
363
  | `chapters_segment` / `chapters_continue` / `chapters_fork` tools (`src/tools.ts` → `continue-core.ts`) | **done & boot-proven** (r22 9/9 full loop incl. child read-back; r24 fork siblings reserve nothing; r25 durable title; r26/26b checklist pass) |
364
364
  | E3: header cache across a compaction | **measured, per-machine (r23 cloud, r28 local)**: cloud held 7,424 cached across the replacement (refill 13,658 → ~7,000); local llama.cpp reuses NOTHING across a head replacement (0 cached) but steady-state turns cost only the new content. Never quote one machine's number for the other |
365
- | Fork button | **shipped** (client bundle + slots; `remote.commands` trigger — `connection.rpc.handle` from a plugin is broken on this host, r26a; the button rides the command path instead) |
365
+ | Fork + Continue buttons | **shipped** (client bundle + slots; `remote.commands` trigger — `connection.rpc.handle` from a plugin is broken on this host, r26a; the buttons ride the command path instead). Both deterministic doors are rows over ONE client runner (`src/client/doors.ts` + the `DOOR_ACTIONS` table): `/chapters-continue` shipped as a composer command only, so its child was created correctly with no visible affordance |
366
366
  | Long-context mechanics (architecture amendments) | **built + verified**: arrival-time artifacting (`src/arrival.ts`, `chapters_artifact`; e2e artifact-arrival green — 373KB book never prefilled, cache intact) and plot carriage (persona PLOT: + elicited fallback); e2e = 7 specs / 2 projects on a throwaway per-boot home (188 unit/integration) |
367
367
  | Knowledge layer P1 (record §13) | **complete**: project identity, redaction, live turn signatures, topic-sequential composition (fork proven live, r36; engine wired, r29), sharded index + incremental manifest, `chapters_search`, `/chapters-link`+`/chapters-status`+notice v2 (Project line, `(N msgs)`), the sync loop with §5.3 local-only degradation + diverged-rebuild, both schedulers and all §5.1 triggers wired. 9/9 live (r35j) + the two-machine exit criterion PROVEN as a browser journey (e2e suite green 2026-09-18) (composer→real remote→machine B searches) |
368
368
  | Topic mapping (composer) | **live-verified at the fork (r36)**: same-topic turns merge into one chapter on real signatures; topic changes split; pressure fragments stay legacy (measured, documented §4.2/4.3 + FINDINGS r28-36) — engine composition (r29) waits for deep selections |
@@ -425,7 +425,15 @@ containing a submission does not end until the review is answered. So:
425
425
  plan marked PENDING/un-stored (`continue-command.test.ts`).
426
426
 
427
427
  So the honest statement is: the seeded continuation is covered deterministically and structurally, and
428
- NOT yet observed in a real browser. Building that row needs new stub rules and a fresh distillation; it is
428
+ NOT yet observed in a real browser. An attempt at that browser row on 2026-10-11 was built and then
429
+ REVERTED, unshipped, for two reasons worth recording so the next person does not re-derive them:
430
+ (a) the scripted upstream had to learn to answer PLAINLY in plan mode, and its decision rule first keyed
431
+ on the LAST user message — but the harness appends its own injected user messages (instructions, runtime
432
+ snapshot, skill catalog) AFTER the human's, so the ask was invisible and the journey hung on a review card
433
+ that could never appear; that ordering trap is now pinned by a unit row; and (b) a stale stub process
434
+ serving pre-edit logic made the failure look like a product problem. Neither is a product defect. The
435
+ shape is sound — `plan/mode {active:true}` logged, turn completed at seq 21, no tool call — so a future
436
+ leg needs the plain branch plus a fresh distillation, and nothing else. Building that row needs new stub rules and a fresh distillation; it is
429
437
  recorded as the remaining piece rather than quietly claimed.
430
438
 
431
439
  ## Deferred (human decisions, not ours)
package/docs/verify.md CHANGED
@@ -152,7 +152,7 @@ renderer, the segmentation tool, and the child's notice.
152
152
  | P10 | Artifact citations resolve wherever they were written | a chapter's `artifacts/aa/<sha>.txt` reference resolves whether the blob sits at the STORE ROOT (arrival stubs) or inside the ANCESTRY TREE (`<store>/<root>/artifacts/…`, where `writeArchive` puts deferred results and every plan) — before this fix the second form was unreadable through `chapters_artifact`, which is what "every byte remains retrievable" actually promised | `artifact-query.test.ts` rows + integration `plans-carriage.test.ts` (`resolveArtifactWithMirror` over the real tree) |
153
153
  | P11 | The browser plane runs in the chain | `/plan <message>` → a real `exit_plan_mode` call → the host's review card opened and answered by a real CLICK → fork; asserts the submission's bytes are the artifact's bytes (or provably only-redacted), `planMode: inactive` from the fold, a `kind: plan` document on disk, and that the BRANCH child carries **no** `## Active plan` and no call-out while still citing the archive; plus a guard that no `compaction/*` entered the journey | `tests/e2e/plans.spec.ts` = chain project `plans` (13 s replay, no LLM; 3-entry tape). **Scope, learned at cost:** the continuation leg was REMOVED from this journey because a tape freezes the model's tool ARGUMENTS — my scripted upstream derived `chapters_continue` ranges from the ceiling the live segment tool reported while distilling (30 here), and the runner's session lands at 29, so `validateRanges` refused the stale input (correctly). A journey whose recorded request embeds a machine-specific seq cannot be hardware-neutral. Continuation semantics — `## Active plan`, `## Previous plans`, the seeded `plan/mode`, the checkpoint carry — are covered by the deterministic plane (`continue-plans`, `plans-engine`, `plan-documents`, the § 10 kernel probe) and the in-place path by the heavy/arrival/enrich/fanout journeys. Tape provenance: scripted upstream, see `tests/fixtures/model-tape/plans/PROVENANCE.md` |
154
154
 
155
- | P21 | The continuation has a **human-pulled door**, and the two doors cannot drift | `/chapters-continue` = `deterministicCommand({intent:'continue'})`, the same builder as `/chapters-fork`; only name/verbs/intent/no-directive prose differ; archive semantics identical (everything since the watermark) | unit `continue-command.test.ts` (7 rows) incl. the **no-drift row**: same events in ⇒ same ranges, titles and plan facts out of both doors; the no-plan case, the refusal naming its own door, and approved ⇒ NOT seeded vs still-planning ⇒ seeded `['user/message','plan/mode']` |
155
+ | P21 | The continuation has a **human-pulled door**, and the two doors cannot drift | `/chapters-continue` = `deterministicCommand({intent:'continue'})`, the same builder as `/chapters-fork`; only name/verbs/intent/no-directive prose differ; archive semantics identical (everything since the watermark). CLIENT SIDE, same discipline: the assistant row registers both doors from ONE runner (`src/client/doors.ts` `makeDoorRunner` + a two-row `DOOR_ACTIONS` table) — a composer-only continue left the user with a created child and no affordance | unit `continue-command.test.ts` (7 rows) incl. the **no-drift row**: same events in ⇒ same ranges, titles and plan facts out of both doors; the no-plan case, the refusal naming its own door, and approved ⇒ NOT seeded vs still-planning ⇒ seeded `['user/message','plan/mode']`. Unit `client-doors.test.ts` (7 rows) pins the two client rows (distinct id/order/label/command, the fork label frozen for the specs), that ONE child-id regex parses BOTH door wordings, that both rows fire their own command through the same body, and — by reading the server's own templates — that the reply wording the client parses is still what `deterministicCommand` emits. Browser: project `fork` asserts BOTH buttons render |
156
156
  | P22 | The continuation shape is covered **in the browser**, on every push | the leg that a tape could not carry (§ 15) is back, driven by the command — no model request, no frozen `endSeq`; asserted from the child's durable log without switching the UI: `## Active plan`, the operative plan by title with its real verdict, `plan document:` and `chapters_artifact` citations, `## Finding things`, no seeded gate after approval, exactly one synthetic notice (invariant 2) | `tests/e2e/plans.spec.ts` project `plans` — replays in ~14 s with no LLM/GPU, eight-project chain green |
157
157
  | P18 | Search answers **without a pool linked** — the findability audit | `buildLocalCorpus` walks the workspace store (chapters, `kind: plan` documents, rules) and answers through the SAME scorer as the mirror door (`rankEntries`), so ranking cannot drift between doors; artifacts are deliberately not indexed (they answer to `chapters_artifact`); the answer names its door ("searched this machine's store, N docs, no pool mirror") so a session never reads *this machine only* as *the whole project*; "nothing matched" (`documents>0`) is distinguished from "nothing here" (`empty`); a pool miss falls back to the local store before answering empty | unit `local-corpus.test.ts` (5) + integration `find-without-a-pool.test.ts` through the real tool wrapper — the empty answer used to be born in the wrapper, not the pure function |
158
158
  | P19 | A child is TOLD the lookup verbs | the notice gains a three-line `## Finding things` naming `read` (chapters), `chapters_artifact` (deferred blobs, path or bare sha) and `chapters_search` (everything else), plus the one fact a model cannot infer — that plans are searchable **as plans** (`kind: plan`) and rules arrive from other machines; `retrievalHints` (default on) off ⇒ the notice is byte-identical to the previous shape | unit rows in `find-without-a-pool.test.ts` (on/off goldens); measured that NO committed tape prefix contains a seeded notice, so the change costs no re-record, and the eight-project chain replays green |
package/lib/client.js CHANGED
@@ -6,39 +6,28 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
- //#region src/client/index.ts
10
- /**
11
- * dsh-chapters client entry: the per-message fork action.
12
- *
13
- * Registers one button into the assistant action row
14
- * (`conversation.chat.assistant-actions` — the slot the official feedback
15
- * package populates beside copy/branch). Clicking it executes `/chapters-fork`
16
- * through `remote.commands` — the exact wire the composer's input line rides —
17
- * then SWITCHES the app to the fresh branch:
18
- *
19
- * 1. `sessions.open(child)` — the client sessions service carries open() on
20
- * its prototype (Playwright-verified against this host; a keys-dump alone
21
- * hid it, which is why an earlier round wrongly gave up on this path);
22
- * 2. fallback: click the branch's sidebar row after expanding collapsed
23
- * groups (plugin children can render under collapsed 'Ungrouped' heads);
24
- * 3. last resort: an inline note — the branch is durable either way.
25
- *
26
- * There is deliberately NO second implementation of fork logic here: the
27
- * button is a mouth for the host command (watermark, segmentation, budget,
28
- * titles, refusal-with-numbers all server-side, all tested). The purity gate
29
- * in tsdown.client.config.ts keeps this file cross-plugin-free.
30
- *
31
- * @module dsh-chapters/src/client
32
- */
33
- const inject = [
34
- "slots",
35
- "remote",
36
- "remote.commands",
37
- "sessions"
38
- ];
39
- /** Pull the child id and branch title out of the command's durable texts. */
9
+ //#region src/client/doors.ts
10
+ const DOOR_ACTIONS = [{
11
+ id: "chapters-fork",
12
+ order: 30,
13
+ label: "Fork with chapters",
14
+ command: "/chapters-fork",
15
+ noun: "branch",
16
+ iconPath: "M5 3.25a2.25 2.25 0 1 0-1.5 2.12v5.26a2.25 2.25 0 1 0 1.5 0V9h5a2.25 2.25 0 0 0 2.25-2.25v-1.4a2.25 2.25 0 1 0-1.5 0v1.4A.75.75 0 0 1 10 7.5H5V5.37A2.25 2.25 0 0 0 5 3.25Z"
17
+ }, {
18
+ id: "chapters-continue",
19
+ order: 31,
20
+ label: "Continue with chapters",
21
+ command: "/chapters-continue",
22
+ noun: "continuation",
23
+ iconPath: "M1 8a.75.75 0 0 1 0-1.5h9.44L7.72 3.28a.75.75 0 1 1 1.06-1.06l3.5 3.5.53.53-.53.53-3.5 3.5a.75.75 0 1 1-1.06-1.06L10.44 9.5H1Zm13-5.25v10.5a.75.75 0 0 1-1.5 0v-10.5a.75.75 0 0 1 1.5 0Z"
24
+ }];
25
+ /** Pull the child id out of either door's durable success text — both end in
26
+ * `… is session ch-…` (src/tools.ts), so this regex is door-agnostic. */
40
27
  const CHILD_ID = /is session (ch-[\w-]+)/;
41
- const BRANCH_TITLE = /branch \u201C([^\u201D]+)\u201D is session/;
28
+ /** The quoted title the door gave the child, matched by its OWN noun so a
29
+ * refusal or an unrelated quote cannot pass for it. */
30
+ const doorTitle = (noun) => new RegExp(`${noun} \\u201C([^\\u201D]+)\\u201D is session`);
42
31
  function openByService(sessions, sessionId) {
43
32
  return (async () => {
44
33
  const svc = sessions;
@@ -58,8 +47,8 @@ window.__ModuleLoader__.load({
58
47
  }
59
48
  /**
60
49
  * Fallback path: expand collapsed groups / show-more buttons, then click the
61
- * newest treeitem whose label contains the branch title (newest-updated sorts
62
- * first, so the first match is the fresh branch).
50
+ * newest treeitem whose label contains the child title (newest-updated sorts
51
+ * first, so the first match is the fresh child).
63
52
  */
64
53
  async function openBranchRow(title, budgetMs = 4e3) {
65
54
  if (typeof document === "undefined") return false;
@@ -88,41 +77,41 @@ window.__ModuleLoader__.load({
88
77
  }
89
78
  return tryClick();
90
79
  }
91
- function makeForker(remote, sessions) {
92
- return async function fork(sessionId) {
80
+ function makeDoorRunner(remote, sessions, door) {
81
+ return async function run(sessionId) {
93
82
  try {
94
83
  if (remote.commands?.execute === void 0) return {
95
84
  ok: false,
96
85
  message: "commands remote unavailable"
97
86
  };
98
- const res = await remote.commands.execute(sessionId, "/chapters-fork", []);
87
+ const res = await remote.commands.execute(sessionId, door.command, []);
99
88
  if (res.ok !== true) return {
100
89
  ok: false,
101
- message: (res.error?.message ?? "the fork call failed").slice(0, 90)
90
+ message: (res.error?.message ?? `the ${door.noun} call failed`).slice(0, 90)
102
91
  };
103
92
  const result = res.value?.result;
104
93
  const text = typeof result?.text === "string" ? result.text : "";
105
94
  if (result?.kind !== "success") return {
106
95
  ok: false,
107
- message: (text || "the fork was refused — see the command result").slice(0, 90)
96
+ message: (text || `the ${door.noun} was refused — see the command result`).slice(0, 90)
108
97
  };
109
98
  const child = CHILD_ID.exec(text)?.[1];
110
99
  if (child === void 0) return {
111
100
  ok: true,
112
- message: "forked — the branch is appearing in the sidebar"
101
+ message: `${door.noun} created — appearing in the sidebar`
113
102
  };
114
103
  if (await openByService(sessions, child)) return {
115
104
  ok: true,
116
105
  message: ""
117
106
  };
118
- const title = BRANCH_TITLE.exec(text)?.[1];
107
+ const title = doorTitle(door.noun).exec(text)?.[1];
119
108
  if (title !== void 0 && await openBranchRow(title)) return {
120
109
  ok: true,
121
110
  message: ""
122
111
  };
123
112
  return {
124
113
  ok: true,
125
- message: "forked — open the branch in the sidebar"
114
+ message: `${door.noun} created — open it in the sidebar`
126
115
  };
127
116
  } catch (error) {
128
117
  return {
@@ -132,11 +121,45 @@ window.__ModuleLoader__.load({
132
121
  }
133
122
  };
134
123
  }
124
+ //#endregion
125
+ //#region src/client/index.ts
126
+ /**
127
+ * dsh-chapters client entry: the per-message door buttons.
128
+ *
129
+ * Registers the two deterministic archive doors into the assistant action row
130
+ * (`conversation.chat.assistant-actions` — the slot the official feedback
131
+ * package populates beside copy/branch). Clicking one executes its host
132
+ * command — `/chapters-fork`, `/chapters-continue` — through `remote.commands`,
133
+ * the exact wire the composer's input line rides, then SWITCHES the app to the
134
+ * fresh child:
135
+ *
136
+ * 1. `sessions.open(child)` — the client sessions service carries open() on
137
+ * its prototype (Playwright-verified against this host; a keys-dump alone
138
+ * hid it, which is why an earlier round wrongly gave up on this path);
139
+ * 2. fallback: click the child's sidebar row after expanding collapsed
140
+ * groups (plugin children can render under collapsed 'Ungrouped' heads);
141
+ * 3. last resort: an inline note — the child is durable either way.
142
+ *
143
+ * There is deliberately NO second implementation of door logic here: the
144
+ * buttons are mouths for the host commands (watermark, segmentation, budget,
145
+ * titles, refusal-with-numbers all server-side, all tested), and the runner
146
+ * they share is `src/client/doors.ts` — one body, one row per door. The purity
147
+ * gate in tsdown.client.config.ts keeps this file cross-plugin-free.
148
+ *
149
+ * @module dsh-chapters/src/client
150
+ */
151
+ const inject = [
152
+ "slots",
153
+ "remote",
154
+ "remote.commands",
155
+ "sessions"
156
+ ];
135
157
  /**
136
158
  * Style parity with the native IconActions row: rules copied verbatim from the
137
159
  * host's MessageIconActions module (`._xzv4MW_action` in dsh-client-ui-chat's
138
160
  * compiled client + the feedback package's :disabled) — same metrics, tokens,
139
- * hover. Update this string if the host restyles its action row.
161
+ * hover. Update this string if the host restyles its action row. Both doors
162
+ * share these metrics.
140
163
  *
141
164
  * The final rule hides the native branch action IN THE MESSAGE ROW only
142
165
  * (user-approved cosmetic decision, docs/architecture.md): built-in chrome, so
@@ -163,48 +186,51 @@ window.__ModuleLoader__.load({
163
186
  document.head.appendChild(tag);
164
187
  } catch {}
165
188
  }
166
- function ChaptersForkAction({ fork }) {
167
- const [busy, setBusy] = (0, react.useState)(false);
168
- const [note, setNote] = (0, react.useState)(null);
169
- const alive = (0, react.useRef)(true);
170
- (0, react.useEffect)(() => () => {
171
- alive.current = false;
172
- }, []);
173
- const onClick = (0, react.useCallback)(() => {
174
- if (busy) return;
175
- setBusy(true);
176
- setNote(null);
177
- fork().then((result) => {
178
- if (!alive.current) return;
179
- setBusy(false);
180
- setNote(result.message === "" ? null : result.message);
181
- setTimeout(() => {
182
- if (alive.current) setNote(null);
183
- }, 8e3);
184
- });
185
- }, [busy, fork]);
186
- const button = (0, react.createElement)("button", {
187
- type: "button",
188
- "aria-label": "Fork with chapters",
189
- disabled: busy,
190
- onClick,
191
- className: "dsh-chapters_forkAction"
192
- }, (0, react.createElement)("svg", {
193
- viewBox: "0 0 16 16",
194
- width: "15",
195
- height: "15",
196
- fill: "currentColor",
197
- "aria-hidden": "true"
198
- }, (0, react.createElement)("path", { d: "M5 3.25a2.25 2.25 0 1 0-1.5 2.12v5.26a2.25 2.25 0 1 0 1.5 0V9h5a2.25 2.25 0 0 0 2.25-2.25v-1.4a2.25 2.25 0 1 0-1.5 0v1.4A.75.75 0 0 1 10 7.5H5V5.37A2.25 2.25 0 0 0 5 3.25Z" })));
199
- return (0, react.createElement)("span", { style: {
200
- display: "inline-flex",
201
- alignItems: "center",
202
- gap: "4px"
203
- } }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
204
- label: "Fork with chapters",
205
- side: "bottom",
206
- children: button
207
- }), note !== null ? (0, react.createElement)("span", { className: "dsh-chapters_forkNote" }, note) : null);
189
+ /** One component body for every door row; only the label and glyph differ. */
190
+ function makeDoorAction(door) {
191
+ return function ChaptersDoorAction({ run }) {
192
+ const [busy, setBusy] = (0, react.useState)(false);
193
+ const [note, setNote] = (0, react.useState)(null);
194
+ const alive = (0, react.useRef)(true);
195
+ (0, react.useEffect)(() => () => {
196
+ alive.current = false;
197
+ }, []);
198
+ const onClick = (0, react.useCallback)(() => {
199
+ if (busy) return;
200
+ setBusy(true);
201
+ setNote(null);
202
+ run().then((result) => {
203
+ if (!alive.current) return;
204
+ setBusy(false);
205
+ setNote(result.message === "" ? null : result.message);
206
+ setTimeout(() => {
207
+ if (alive.current) setNote(null);
208
+ }, 8e3);
209
+ });
210
+ }, [busy, run]);
211
+ const button = (0, react.createElement)("button", {
212
+ type: "button",
213
+ "aria-label": door.label,
214
+ disabled: busy,
215
+ onClick,
216
+ className: "dsh-chapters_forkAction"
217
+ }, (0, react.createElement)("svg", {
218
+ viewBox: "0 0 16 16",
219
+ width: "15",
220
+ height: "15",
221
+ fill: "currentColor",
222
+ "aria-hidden": "true"
223
+ }, (0, react.createElement)("path", { d: door.iconPath })));
224
+ return (0, react.createElement)("span", { style: {
225
+ display: "inline-flex",
226
+ alignItems: "center",
227
+ gap: "4px"
228
+ } }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
229
+ label: door.label,
230
+ side: "bottom",
231
+ children: button
232
+ }), note !== null ? (0, react.createElement)("span", { className: "dsh-chapters_forkNote" }, note) : null);
233
+ };
208
234
  }
209
235
  function apply(ctx) {
210
236
  injectStyles();
@@ -216,13 +242,17 @@ window.__ModuleLoader__.load({
216
242
  } catch {
217
243
  sessions = void 0;
218
244
  }
219
- const fork = makeForker(remote, sessions);
220
- slots.inject("conversation.chat.assistant-actions", () => slots.register({
245
+ const rows = DOOR_ACTIONS.map((door) => ({
246
+ door,
247
+ run: makeDoorRunner(remote, sessions, door),
248
+ component: makeDoorAction(door)
249
+ }));
250
+ slots.inject("conversation.chat.assistant-actions", () => rows.map(({ door, run, component }) => slots.register({
221
251
  name: "conversation.chat.assistant-actions",
222
- id: "chapters-fork",
223
- order: 30,
224
- inject: (sessionId) => ({ fork: () => fork(sessionId) })
225
- }, ChaptersForkAction));
252
+ id: door.id,
253
+ order: door.order,
254
+ inject: (sessionId) => ({ run: () => run(sessionId) })
255
+ }, component)));
226
256
  }
227
257
  //#endregion
228
258
  exports.apply = apply;
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useState","useRef","useCallback","h","Tooltip"],"sources":["../src/client/index.ts"],"sourcesContent":["/**\n * dsh-chapters client entry: the per-message fork action.\n *\n * Registers one button into the assistant action row\n * (`conversation.chat.assistant-actions` — the slot the official feedback\n * package populates beside copy/branch). Clicking it executes `/chapters-fork`\n * through `remote.commands` — the exact wire the composer's input line rides —\n * then SWITCHES the app to the fresh branch:\n *\n * 1. `sessions.open(child)` — the client sessions service carries open() on\n * its prototype (Playwright-verified against this host; a keys-dump alone\n * hid it, which is why an earlier round wrongly gave up on this path);\n * 2. fallback: click the branch's sidebar row after expanding collapsed\n * groups (plugin children can render under collapsed 'Ungrouped' heads);\n * 3. last resort: an inline note — the branch is durable either way.\n *\n * There is deliberately NO second implementation of fork logic here: the\n * button is a mouth for the host command (watermark, segmentation, budget,\n * titles, refusal-with-numbers all server-side, all tested). The purity gate\n * in tsdown.client.config.ts keeps this file cross-plugin-free.\n *\n * @module dsh-chapters/src/client\n */\n\nimport { createElement as h, useCallback, useEffect, useRef, useState, type ReactElement } from 'react'\n// The host's own tooltip bubble — a platform module served by the loader table,\n// external like react. Every action in this row labels through it.\nimport { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'\n\n/** Structural slice of the aggregated remote service (@deepseek-ai/dsh-api-remotes).\n * `remote.commands` must ALSO appear in inject — the federation gates each\n * sub-service individually (the live feedback package declares\n * remote.messageFeedback beside remote for exactly this reason). */\ninterface ClientRemote {\n commands?: {\n execute(sessionId: string, line: string, attachments: readonly unknown[]): Promise<{\n ok?: boolean\n value?: { commandId?: string; result?: { kind?: string; text?: string } }\n error?: { code?: string; message?: string }\n }>\n }\n}\n\ninterface ClientSlots {\n inject(name: string, factory: () => unknown): void\n register(registration: Record<string, unknown>, component: unknown): unknown\n}\n\n/** The client sessions service (dsh-client-runtime): open() lives on the\n * prototype; everything else on the surface stays untrusted and unused. */\ninterface ClientSessions {\n open(sessionId: string): unknown\n}\n\ninterface Ctx {\n slots: ClientSlots\n get(name: string): unknown\n}\n\nexport const inject = ['slots', 'remote', 'remote.commands', 'sessions'] as const\n\n/** Pull the child id and branch title out of the command's durable texts. */\nconst CHILD_ID = /is session (ch-[\\w-]+)/\nconst BRANCH_TITLE = /branch \\u201C([^\\u201D]+)\\u201D is session/\n\nfunction openByService(sessions: ClientSessions | undefined, sessionId: string): Promise<boolean> {\n return (async () => {\n const svc = sessions as unknown as { open?: (id: string) => unknown; refresh?: () => unknown } | undefined\n if (typeof svc?.open !== 'function') return false\n try { await svc.refresh?.() } catch { /* refresh best-effort */ }\n // The store must know the session before open() resolves it; a child\n // created seconds ago is only server-known until refresh lands. Retry\n // open briefly across that window.\n const started = Date.now()\n for (;;) {\n try { svc.open(sessionId); return true } catch {\n if (Date.now() - started > 4_000) return false\n await new Promise((r) => setTimeout(r, 250))\n }\n }\n })()\n}\n\n/**\n * Fallback path: expand collapsed groups / show-more buttons, then click the\n * newest treeitem whose label contains the branch title (newest-updated sorts\n * first, so the first match is the fresh branch).\n */\nasync function openBranchRow(title: string, budgetMs = 4_000): Promise<boolean> {\n if (typeof document === 'undefined') return false\n const needle = title.toLowerCase()\n const tryClick = (): boolean => {\n const rows = Array.from(document.querySelectorAll<HTMLElement>('[role=\"treeitem\"]'))\n const hit = rows.find((row) => (row.textContent ?? '').toLowerCase().includes(needle))\n if (hit === undefined) return false\n hit.click()\n return true\n }\n const clickMore = (): number => {\n const buttons = Array.from(document.querySelectorAll<HTMLElement>('button'))\n .filter((b) => /more sessions|ungrouped/i.test(b.textContent ?? '') && b.getAttribute('aria-expanded') !== 'true')\n for (const b of buttons) b.click()\n return buttons.length\n }\n const started = Date.now()\n let expansions = 0\n while (Date.now() - started < budgetMs) {\n if (tryClick()) return true\n if (expansions < 4 && clickMore() > 0) { expansions += 1; await new Promise((r) => setTimeout(r, 250)); continue }\n await new Promise((resolve) => setTimeout(resolve, 150))\n }\n return tryClick()\n}\n\nfunction makeForker(remote: ClientRemote, sessions: ClientSessions | undefined) {\n return async function fork(sessionId: string): Promise<{ ok: boolean; message: string }> {\n try {\n if (remote.commands?.execute === undefined) return { ok: false, message: 'commands remote unavailable' }\n const res = await remote.commands.execute(sessionId, '/chapters-fork', [])\n if (res.ok !== true) return { ok: false, message: (res.error?.message ?? 'the fork call failed').slice(0, 90) }\n const result = res.value?.result\n const text = typeof result?.text === 'string' ? result.text : ''\n if (result?.kind !== 'success') return { ok: false, message: (text || 'the fork was refused — see the command result').slice(0, 90) }\n const child = CHILD_ID.exec(text)?.[1]\n if (child === undefined) return { ok: true, message: 'forked — the branch is appearing in the sidebar' }\n const opened = await openByService(sessions, child)\n if (opened) return { ok: true, message: '' }\n const title = BRANCH_TITLE.exec(text)?.[1]\n if (title !== undefined && await openBranchRow(title)) return { ok: true, message: '' }\n return { ok: true, message: 'forked — open the branch in the sidebar' }\n } catch (error) {\n return { ok: false, message: String((error as Error)?.message ?? error).slice(0, 90) }\n }\n }\n}\n\n/** Props composed by the slot framework: owner (`messageId`) + our injected verb. */\ninterface ForkActionProps {\n messageId: string\n fork: () => Promise<{ ok: boolean; message: string }>\n}\n\n/**\n * Style parity with the native IconActions row: rules copied verbatim from the\n * host's MessageIconActions module (`._xzv4MW_action` in dsh-client-ui-chat's\n * compiled client + the feedback package's :disabled) — same metrics, tokens,\n * hover. Update this string if the host restyles its action row.\n *\n * The final rule hides the native branch action IN THE MESSAGE ROW only\n * (user-approved cosmetic decision, docs/architecture.md): built-in chrome, so\n * the selector is its accessible label scoped to action-row buttons; both\n * locale spellings covered. If the host rewords the label the button simply\n * reappears — fails open, visibly.\n */\nconst CSS = [\n '.dsh-chapters_forkAction{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}',\n '.dsh-chapters_forkAction svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px))}',\n '.dsh-chapters_forkAction:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}',\n '.dsh-chapters_forkAction:disabled{cursor:default;opacity:.4}',\n '.dsh-chapters_forkNote{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:13px;line-height:20px}',\n 'button[class*=\"_action\"][aria-label=\"Branch into a new conversation\"],button[class*=\"_action\"][aria-label=\"\\u5728\\u65b0\\u5bf9\\u8bdd\\u4e2d\\u5206\\u652f\"]{display:none !important}',\n].join('\\n')\nconst CSS_TAG_ID = 'dsh-chapters/assistant-action.css'\n\nfunction injectStyles(): void {\n try {\n if (typeof document === 'undefined' || document.querySelector(`style[data-plugin-css=${JSON.stringify(CSS_TAG_ID)}]`) !== null) return\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-chapters'\n tag.dataset.pluginCss = CSS_TAG_ID\n tag.textContent = CSS\n document.head.appendChild(tag)\n } catch { /* styling is cosmetic; the action still works without it */ }\n}\n\nfunction ChaptersForkAction({ fork }: ForkActionProps) {\n const [busy, setBusy] = useState(false)\n const [note, setNote] = useState<string | null>(null)\n const alive = useRef(true)\n useEffect(() => () => { alive.current = false }, [])\n const onClick = useCallback(() => {\n if (busy) return\n setBusy(true)\n setNote(null)\n void fork().then((result) => {\n if (!alive.current) return\n setBusy(false)\n setNote(result.message === '' ? null : result.message)\n setTimeout(() => { if (alive.current) setNote(null) }, 8000)\n })\n }, [busy, fork])\n const button = h('button', {\n type: 'button',\n 'aria-label': 'Fork with chapters',\n disabled: busy,\n onClick,\n className: 'dsh-chapters_forkAction',\n },\n h('svg', { viewBox: '0 0 16 16', width: '15', height: '15', fill: 'currentColor', 'aria-hidden': 'true' },\n h('path', { d: 'M5 3.25a2.25 2.25 0 1 0-1.5 2.12v5.26a2.25 2.25 0 1 0 1.5 0V9h5a2.25 2.25 0 0 0 2.25-2.25v-1.4a2.25 2.25 0 1 0-1.5 0v1.4A.75.75 0 0 1 10 7.5H5V5.37A2.25 2.25 0 0 0 5 3.25Z' })))\n return h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '4px' } },\n h(Tooltip, { label: 'Fork with chapters', side: 'bottom' as const, children: button as ReactElement<Record<string, unknown>> }),\n note !== null\n ? h('span', { className: 'dsh-chapters_forkNote' }, note)\n : null)\n}\n\nexport function apply(ctx: Ctx): void {\n injectStyles()\n const slots = ctx.slots\n const remote = (ctx.get('remote') ?? {}) as ClientRemote\n let sessions: ClientSessions | undefined\n try { sessions = ctx.get('sessions') as ClientSessions | undefined } catch { sessions = undefined }\n // No early return on missing descriptors: the button registers regardless\n // and the click reports the gap. A missing button is worse than a button\n // that explains itself.\n const fork = makeForker(remote, sessions)\n slots.inject('conversation.chat.assistant-actions', () => slots.register({\n name: 'conversation.chat.assistant-actions',\n id: 'chapters-fork',\n order: 30,\n inject: (sessionId: string) => ({ fork: () => fork(sessionId) }),\n }, ChaptersForkAction))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useState","useRef","useCallback","h","Tooltip"],"sources":["../src/client/doors.ts","../src/client/index.ts"],"sourcesContent":["/**\n * The client door runner: the two deterministic archive doors, executed over\n * `remote.commands` and followed by a SWITCH to the fresh child.\n *\n * ONE runner, two rows. The server side keeps the same discipline — both doors\n * come from `deterministicCommand(door)` in src/tools.ts, and a unit row pins\n * that they archive identically. Copy-pasting this body is how the two would\n * drift apart, so there is exactly one implementation and a table of rows.\n *\n * `chapters-continue` ships only as a composer command; without a button here\n * the child is created correctly and the UI shows nothing, which reads as\n * \"nothing happened\". The row is the missing affordance, not new logic.\n *\n * React-free on purpose: the primitives package pulls CSS modules the node\n * test runner cannot load, so the testable half lives here and index.ts is the\n * thin chrome that renders these rows as buttons (tests/unit/client-doors.test.ts).\n *\n * @module dsh-chapters/src/client/doors\n */\n\n/** Structural slice of the aggregated remote service (@deepseek-ai/dsh-api-remotes).\n * `remote.commands` must ALSO appear in inject — the federation gates each\n * sub-service individually (the live feedback package declares\n * remote.messageFeedback beside remote for exactly this reason). */\nexport interface ClientRemote {\n commands?: {\n execute(sessionId: string, line: string, attachments: readonly unknown[]): Promise<{\n ok?: boolean\n value?: { commandId?: string; result?: { kind?: string; text?: string } }\n error?: { code?: string; message?: string }\n }>\n }\n}\n\n/** The client sessions service (dsh-client-runtime): open() lives on the\n * prototype; everything else on the surface stays untrusted and unused. */\nexport interface ClientSessions {\n open(sessionId: string): unknown\n}\n\nexport interface DoorResult {\n ok: boolean\n message: string\n}\n\n/**\n * One assistant-action row. `noun` is the server's own word for the child in\n * its success text (`branch` / `continuation`), so the fallback wording and the\n * title regex both track the door without a second implementation.\n */\nexport interface DoorAction {\n /** The slot registration id. */\n id: string\n /** Position in the assistant action row. */\n order: number\n /** Button + tooltip label, and the accessible name the e2e specs select on. */\n label: string\n /** The host command this row executes — the ONLY semantic difference at runtime. */\n command: string\n /** The noun in `${noun} “title” is session ch-…`. */\n noun: string\n /** The row's glyph (16×16 viewBox, single path). */\n iconPath: string\n}\n\nexport const DOOR_ACTIONS: readonly DoorAction[] = [\n {\n id: 'chapters-fork',\n order: 30,\n label: 'Fork with chapters',\n command: '/chapters-fork',\n noun: 'branch',\n iconPath: 'M5 3.25a2.25 2.25 0 1 0-1.5 2.12v5.26a2.25 2.25 0 1 0 1.5 0V9h5a2.25 2.25 0 0 0 2.25-2.25v-1.4a2.25 2.25 0 1 0-1.5 0v1.4A.75.75 0 0 1 10 7.5H5V5.37A2.25 2.25 0 0 0 5 3.25Z',\n },\n {\n id: 'chapters-continue',\n order: 31,\n label: 'Continue with chapters',\n command: '/chapters-continue',\n noun: 'continuation',\n iconPath: 'M1 8a.75.75 0 0 1 0-1.5h9.44L7.72 3.28a.75.75 0 1 1 1.06-1.06l3.5 3.5.53.53-.53.53-3.5 3.5a.75.75 0 1 1-1.06-1.06L10.44 9.5H1Zm13-5.25v10.5a.75.75 0 0 1-1.5 0v-10.5a.75.75 0 0 1 1.5 0Z',\n },\n]\n\n/** Pull the child id out of either door's durable success text — both end in\n * `… is session ch-…` (src/tools.ts), so this regex is door-agnostic. */\nexport const CHILD_ID = /is session (ch-[\\w-]+)/\n\n/** The quoted title the door gave the child, matched by its OWN noun so a\n * refusal or an unrelated quote cannot pass for it. */\nexport const doorTitle = (noun: string): RegExp => new RegExp(`${noun} \\\\u201C([^\\\\u201D]+)\\\\u201D is session`)\n\nfunction openByService(sessions: ClientSessions | undefined, sessionId: string): Promise<boolean> {\n return (async () => {\n const svc = sessions as unknown as { open?: (id: string) => unknown; refresh?: () => unknown } | undefined\n if (typeof svc?.open !== 'function') return false\n try { await svc.refresh?.() } catch { /* refresh best-effort */ }\n // The store must know the session before open() resolves it; a child\n // created seconds ago is only server-known until refresh lands. Retry\n // open briefly across that window.\n const started = Date.now()\n for (;;) {\n try { svc.open(sessionId); return true } catch {\n if (Date.now() - started > 4_000) return false\n await new Promise((r) => setTimeout(r, 250))\n }\n }\n })()\n}\n\n/**\n * Fallback path: expand collapsed groups / show-more buttons, then click the\n * newest treeitem whose label contains the child title (newest-updated sorts\n * first, so the first match is the fresh child).\n */\nasync function openBranchRow(title: string, budgetMs = 4_000): Promise<boolean> {\n if (typeof document === 'undefined') return false\n const needle = title.toLowerCase()\n const tryClick = (): boolean => {\n const rows = Array.from(document.querySelectorAll<HTMLElement>('[role=\"treeitem\"]'))\n const hit = rows.find((row) => (row.textContent ?? '').toLowerCase().includes(needle))\n if (hit === undefined) return false\n hit.click()\n return true\n }\n const clickMore = (): number => {\n const buttons = Array.from(document.querySelectorAll<HTMLElement>('button'))\n .filter((b) => /more sessions|ungrouped/i.test(b.textContent ?? '') && b.getAttribute('aria-expanded') !== 'true')\n for (const b of buttons) b.click()\n return buttons.length\n }\n const started = Date.now()\n let expansions = 0\n while (Date.now() - started < budgetMs) {\n if (tryClick()) return true\n if (expansions < 4 && clickMore() > 0) { expansions += 1; await new Promise((r) => setTimeout(r, 250)); continue }\n await new Promise((resolve) => setTimeout(resolve, 150))\n }\n return tryClick()\n}\n\nexport function makeDoorRunner(\n remote: ClientRemote,\n sessions: ClientSessions | undefined,\n door: DoorAction,\n): (sessionId: string) => Promise<DoorResult> {\n return async function run(sessionId: string): Promise<DoorResult> {\n try {\n if (remote.commands?.execute === undefined) return { ok: false, message: 'commands remote unavailable' }\n const res = await remote.commands.execute(sessionId, door.command, [])\n if (res.ok !== true) return { ok: false, message: (res.error?.message ?? `the ${door.noun} call failed`).slice(0, 90) }\n const result = res.value?.result\n const text = typeof result?.text === 'string' ? result.text : ''\n if (result?.kind !== 'success') return { ok: false, message: (text || `the ${door.noun} was refused — see the command result`).slice(0, 90) }\n const child = CHILD_ID.exec(text)?.[1]\n if (child === undefined) return { ok: true, message: `${door.noun} created — appearing in the sidebar` }\n const opened = await openByService(sessions, child)\n if (opened) return { ok: true, message: '' }\n const title = doorTitle(door.noun).exec(text)?.[1]\n if (title !== undefined && await openBranchRow(title)) return { ok: true, message: '' }\n return { ok: true, message: `${door.noun} created — open it in the sidebar` }\n } catch (error) {\n return { ok: false, message: String((error as Error)?.message ?? error).slice(0, 90) }\n }\n }\n}\n","/**\n * dsh-chapters client entry: the per-message door buttons.\n *\n * Registers the two deterministic archive doors into the assistant action row\n * (`conversation.chat.assistant-actions` — the slot the official feedback\n * package populates beside copy/branch). Clicking one executes its host\n * command — `/chapters-fork`, `/chapters-continue` — through `remote.commands`,\n * the exact wire the composer's input line rides, then SWITCHES the app to the\n * fresh child:\n *\n * 1. `sessions.open(child)` — the client sessions service carries open() on\n * its prototype (Playwright-verified against this host; a keys-dump alone\n * hid it, which is why an earlier round wrongly gave up on this path);\n * 2. fallback: click the child's sidebar row after expanding collapsed\n * groups (plugin children can render under collapsed 'Ungrouped' heads);\n * 3. last resort: an inline note — the child is durable either way.\n *\n * There is deliberately NO second implementation of door logic here: the\n * buttons are mouths for the host commands (watermark, segmentation, budget,\n * titles, refusal-with-numbers all server-side, all tested), and the runner\n * they share is `src/client/doors.ts` — one body, one row per door. The purity\n * gate in tsdown.client.config.ts keeps this file cross-plugin-free.\n *\n * @module dsh-chapters/src/client\n */\n\nimport { createElement as h, useCallback, useEffect, useRef, useState, type ReactElement } from 'react'\n// The host's own tooltip bubble — a platform module served by the loader table,\n// external like react. Every action in this row labels through it.\nimport { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'\nimport { DOOR_ACTIONS, makeDoorRunner, type ClientRemote, type ClientSessions, type DoorAction, type DoorResult } from './doors.ts'\n\ninterface ClientSlots {\n inject(name: string, factory: () => unknown): void\n register(registration: Record<string, unknown>, component: unknown): unknown\n}\n\ninterface Ctx {\n slots: ClientSlots\n get(name: string): unknown\n}\n\nexport const inject = ['slots', 'remote', 'remote.commands', 'sessions'] as const\n\n/** Props composed by the slot framework: owner (`messageId`) + our injected verb. */\ninterface DoorActionProps {\n messageId: string\n run: () => Promise<DoorResult>\n}\n\n/**\n * Style parity with the native IconActions row: rules copied verbatim from the\n * host's MessageIconActions module (`._xzv4MW_action` in dsh-client-ui-chat's\n * compiled client + the feedback package's :disabled) — same metrics, tokens,\n * hover. Update this string if the host restyles its action row. Both doors\n * share these metrics.\n *\n * The final rule hides the native branch action IN THE MESSAGE ROW only\n * (user-approved cosmetic decision, docs/architecture.md): built-in chrome, so\n * the selector is its accessible label scoped to action-row buttons; both\n * locale spellings covered. If the host rewords the label the button simply\n * reappears — fails open, visibly.\n */\nconst CSS = [\n '.dsh-chapters_forkAction{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}',\n '.dsh-chapters_forkAction svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px))}',\n '.dsh-chapters_forkAction:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}',\n '.dsh-chapters_forkAction:disabled{cursor:default;opacity:.4}',\n '.dsh-chapters_forkNote{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:13px;line-height:20px}',\n 'button[class*=\"_action\"][aria-label=\"Branch into a new conversation\"],button[class*=\"_action\"][aria-label=\"\\u5728\\u65b0\\u5bf9\\u8bdd\\u4e2d\\u5206\\u652f\"]{display:none !important}',\n].join('\\n')\nconst CSS_TAG_ID = 'dsh-chapters/assistant-action.css'\n\nfunction injectStyles(): void {\n try {\n if (typeof document === 'undefined' || document.querySelector(`style[data-plugin-css=${JSON.stringify(CSS_TAG_ID)}]`) !== null) return\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-chapters'\n tag.dataset.pluginCss = CSS_TAG_ID\n tag.textContent = CSS\n document.head.appendChild(tag)\n } catch { /* styling is cosmetic; the action still works without it */ }\n}\n\n/** One component body for every door row; only the label and glyph differ. */\nfunction makeDoorAction(door: DoorAction) {\n return function ChaptersDoorAction({ run }: DoorActionProps) {\n const [busy, setBusy] = useState(false)\n const [note, setNote] = useState<string | null>(null)\n const alive = useRef(true)\n useEffect(() => () => { alive.current = false }, [])\n const onClick = useCallback(() => {\n if (busy) return\n setBusy(true)\n setNote(null)\n void run().then((result) => {\n if (!alive.current) return\n setBusy(false)\n setNote(result.message === '' ? null : result.message)\n setTimeout(() => { if (alive.current) setNote(null) }, 8000)\n })\n }, [busy, run])\n const button = h('button', {\n type: 'button',\n 'aria-label': door.label,\n disabled: busy,\n onClick,\n className: 'dsh-chapters_forkAction',\n },\n h('svg', { viewBox: '0 0 16 16', width: '15', height: '15', fill: 'currentColor', 'aria-hidden': 'true' },\n h('path', { d: door.iconPath })))\n return h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '4px' } },\n h(Tooltip, { label: door.label, side: 'bottom' as const, children: button as ReactElement<Record<string, unknown>> }),\n note !== null\n ? h('span', { className: 'dsh-chapters_forkNote' }, note)\n : null)\n }\n}\n\nexport function apply(ctx: Ctx): void {\n injectStyles()\n const slots = ctx.slots\n const remote = (ctx.get('remote') ?? {}) as ClientRemote\n let sessions: ClientSessions | undefined\n try { sessions = ctx.get('sessions') as ClientSessions | undefined } catch { sessions = undefined }\n // No early return on missing descriptors: the buttons register regardless\n // and the click reports the gap. A missing button is worse than a button\n // that explains itself. Components and runners are built ONCE here — slot\n // factories may re-run, and a fresh component identity per call would\n // remount the row.\n const rows = DOOR_ACTIONS.map((door) => ({ door, run: makeDoorRunner(remote, sessions, door), component: makeDoorAction(door) }))\n slots.inject('conversation.chat.assistant-actions', () => rows.map(({ door, run, component }) => slots.register({\n name: 'conversation.chat.assistant-actions',\n id: door.id,\n order: door.order,\n inject: (sessionId: string) => ({ run: () => run(sessionId) }),\n }, component)))\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/dsh-chapters",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "dsh plugin: verbatim chapter archive + zero-token index for small-context models",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",