@treeseed/dsh-chapters 0.1.13 → 0.1.14
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.
- package/docs/backlog.md +97 -0
- package/docs/development.md +1 -1
- package/docs/plans-carriage.md +9 -1
- package/docs/verify.md +1 -1
- package/lib/client.js +121 -91
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
package/docs/backlog.md
ADDED
|
@@ -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.
|
package/docs/development.md
CHANGED
|
@@ -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
|
|
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 |
|
package/docs/plans-carriage.md
CHANGED
|
@@ -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.
|
|
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/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
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
|
-
|
|
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
|
|
62
|
-
* first, so the first match is the fresh
|
|
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
|
|
92
|
-
return async function
|
|
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,
|
|
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 ??
|
|
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 ||
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
alive
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
220
|
-
|
|
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:
|
|
223
|
-
order:
|
|
224
|
-
inject: (sessionId) => ({
|
|
225
|
-
},
|
|
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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"}
|