@treeseed/dsh-chapters 0.2.0 → 0.2.2-dev.2

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.
Files changed (70) hide show
  1. package/cordis.patch.yml +7 -0
  2. package/docs/backlog.md +1566 -0
  3. package/docs/contract.md +281 -0
  4. package/docs/development.md +122 -0
  5. package/docs/host-protected-head-ask.md +80 -0
  6. package/docs/project-state.md +266 -0
  7. package/docs/verify.md +61 -0
  8. package/lib/arrival.js +24 -0
  9. package/lib/arrival.js.map +1 -1
  10. package/lib/auto-save.d.ts +1 -1
  11. package/lib/auto-save.js +15 -2
  12. package/lib/auto-save.js.map +1 -1
  13. package/lib/aux-message.d.ts +17 -0
  14. package/lib/aux-message.js +20 -0
  15. package/lib/aux-message.js.map +1 -0
  16. package/lib/client.js +536 -227
  17. package/lib/client.js.map +1 -1
  18. package/lib/commands.js +107 -1
  19. package/lib/commands.js.map +1 -1
  20. package/lib/continue-core.d.ts +6 -0
  21. package/lib/continue-core.js +18 -13
  22. package/lib/continue-core.js.map +1 -1
  23. package/lib/diag.d.ts +16 -8
  24. package/lib/diag.js +46 -1
  25. package/lib/diag.js.map +1 -1
  26. package/lib/engine-core.d.ts +5 -0
  27. package/lib/engine-core.js +5 -3
  28. package/lib/engine-core.js.map +1 -1
  29. package/lib/engine.d.ts +15 -0
  30. package/lib/engine.js +79 -6
  31. package/lib/engine.js.map +1 -1
  32. package/lib/index.d.ts +6 -0
  33. package/lib/index.js +202 -9
  34. package/lib/index.js.map +1 -1
  35. package/lib/injections.js +10 -0
  36. package/lib/injections.js.map +1 -1
  37. package/lib/prompt-loop.d.ts +73 -0
  38. package/lib/prompt-loop.js +141 -0
  39. package/lib/prompt-loop.js.map +1 -0
  40. package/lib/prompt-runner.d.ts +44 -0
  41. package/lib/prompt-runner.js +85 -0
  42. package/lib/prompt-runner.js.map +1 -0
  43. package/lib/prompt-state.d.ts +124 -0
  44. package/lib/prompt-state.js +96 -0
  45. package/lib/prompt-state.js.map +1 -0
  46. package/lib/prompt.d.ts +32 -2
  47. package/lib/prompt.js +31 -9
  48. package/lib/prompt.js.map +1 -1
  49. package/lib/registry.d.ts +7 -0
  50. package/lib/registry.js +2 -0
  51. package/lib/registry.js.map +1 -1
  52. package/lib/render.d.ts +22 -1
  53. package/lib/render.js +40 -7
  54. package/lib/render.js.map +1 -1
  55. package/lib/repair-run.d.ts +27 -0
  56. package/lib/repair-run.js +85 -2
  57. package/lib/repair-run.js.map +1 -1
  58. package/lib/rules.js +8 -0
  59. package/lib/rules.js.map +1 -1
  60. package/lib/save-core.js +4 -4
  61. package/lib/save-core.js.map +1 -1
  62. package/lib/store.js +11 -0
  63. package/lib/store.js.map +1 -1
  64. package/lib/tools.d.ts +3 -0
  65. package/lib/tools.js +29 -6
  66. package/lib/tools.js.map +1 -1
  67. package/lib/version.d.ts +1 -1
  68. package/lib/version.js +1 -1
  69. package/lib/version.js.map +1 -1
  70. package/package.json +20 -16
package/lib/client.js CHANGED
@@ -6,7 +6,524 @@ 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/doors.ts
9
+ // src/client/prompt-bar.ts
10
+ function initialBarState(autorun = "disarmed") {
11
+ return {
12
+ status: "off",
13
+ text: null,
14
+ turnSeq: null,
15
+ error: null,
16
+ note: "",
17
+ intent: "worker-step",
18
+ goalPhase: "none",
19
+ goal: null,
20
+ autorun,
21
+ expanded: false,
22
+ running: false,
23
+ hiddenTurnSeq: null
24
+ };
25
+ }
26
+ function promptBarReducer(state, ev) {
27
+ if (ev.t === "note") return {
28
+ ...state,
29
+ note: ev.note
30
+ };
31
+ const next = baseBarReducer(state, ev);
32
+ return ev.t === "polled" && (next.turnSeq !== state.turnSeq || next.text !== state.text) ? {
33
+ ...next,
34
+ note: ""
35
+ } : next;
36
+ }
37
+ function baseBarReducer(state, ev) {
38
+ switch (ev.t) {
39
+ case "polled":
40
+ if (ev.status === "off") return {
41
+ ...state,
42
+ status: "off",
43
+ text: null,
44
+ turnSeq: null,
45
+ error: null,
46
+ expanded: false,
47
+ autorun: "disarmed"
48
+ };
49
+ if (ev.status === "analyzing") return {
50
+ ...state,
51
+ status: "analyzing",
52
+ text: null,
53
+ error: null
54
+ };
55
+ return {
56
+ ...state,
57
+ status: ev.status,
58
+ text: ev.status === "ready" ? ev.text ?? null : null,
59
+ intent: ev.status === "ready" ? ev.intent ?? "worker-step" : "worker-step",
60
+ turnSeq: ev.turnSeq ?? state.turnSeq,
61
+ error: ev.status === "failed" ? ev.error ?? "the suggestion call failed" : null
62
+ };
63
+ case "expand": return {
64
+ ...state,
65
+ expanded: true
66
+ };
67
+ case "collapse": return {
68
+ ...state,
69
+ expanded: false
70
+ };
71
+ case "autorun": return {
72
+ ...state,
73
+ autorun: ev.on ? "armed" : "disarmed"
74
+ };
75
+ case "running": return {
76
+ ...state,
77
+ running: ev.running
78
+ };
79
+ case "human/prompt": return {
80
+ ...state,
81
+ autorun: "disarmed",
82
+ hiddenTurnSeq: state.turnSeq,
83
+ expanded: false
84
+ };
85
+ case "off": return {
86
+ ...state,
87
+ status: "off",
88
+ text: null,
89
+ turnSeq: null,
90
+ error: null,
91
+ expanded: false,
92
+ autorun: "disarmed"
93
+ };
94
+ case "goal": return {
95
+ ...state,
96
+ goalPhase: ev.phase,
97
+ autorun: ev.phase === "complete" ? "disarmed" : state.autorun
98
+ };
99
+ }
100
+ return state;
101
+ }
102
+ /** The status label. `failed` keeps the ready label so the error span is the delta, not a mystery row. */
103
+ function barLabel(s) {
104
+ if (s.status === "analyzing") return "Analyzing situation…";
105
+ if (s.status === "ready" || s.status === "failed") return "Recommended Next Step";
106
+ return null;
107
+ }
108
+ function barVisible(s) {
109
+ if (s.status === "off") return false;
110
+ if (s.status === "ready" && (s.text === null || s.text === "")) return false;
111
+ if (s.hiddenTurnSeq !== null && s.hiddenTurnSeq === s.turnSeq) return false;
112
+ return true;
113
+ }
114
+ /** May the bar submit its own offer? Armed, present, idle, not dismissed, and still serving a purpose. */
115
+ function shouldAutoFire(s) {
116
+ if (s.status !== "ready") return false;
117
+ if (s.text === null || s.text === "") return false;
118
+ if (s.intent === "goal-edit") return false;
119
+ if (s.goal !== null && s.goal.phase === "active" && s.goal.activation === "armed") return false;
120
+ if (s.autorun !== "armed") return false;
121
+ if (s.running) return false;
122
+ if (s.goalPhase === "complete") return false;
123
+ if (s.hiddenTurnSeq !== null && s.hiddenTurnSeq === s.turnSeq) return false;
124
+ return true;
125
+ }
126
+ /** The command the bar pulls. One string, so the client and the command plane cannot drift silently. */
127
+ const BAR_STATE_COMMAND = "/chapters-prompt state";
128
+ /** The bar's Run button: the server submits the on-screen offer as the human's own turn. */
129
+ const BAR_RUN_COMMAND = "/chapters-prompt run";
130
+ /**
131
+ * Parse the `state` verb's `k: v` lines into a pull event.
132
+ *
133
+ * One output serving the human and the component is the design (see `docs/contract.md`); this function is
134
+ * the cost of that choice, and it is kept honest: an unrecognised line is IGNORED, not guessed at, and a
135
+ * payload that yields no `status` is `null` so the caller keeps its last state rather than inventing `off`
136
+ * (which would hide a bar that is merely mid-reply). Indentation and the prompt's own lines are the reason
137
+ * the prompt is collected separately from the scalar fields — it is the only multi-line value.
138
+ */
139
+ function parseBarState(text) {
140
+ const lines = text.split("\n");
141
+ const head = lines[0] ?? "";
142
+ const statusWord = head.startsWith("suggested next step:") ? head.slice(20).trim() : "";
143
+ const status = statusWord === "off" ? "off" : statusWord === "idle" ? "idle" : statusWord === "analyzing" ? "analyzing" : statusWord === "ready" ? "ready" : statusWord === "failed" ? "failed" : null;
144
+ if (status === null) return null;
145
+ const scalars = {};
146
+ const prompt = [];
147
+ let inPrompt = false;
148
+ for (const line of lines.slice(1)) {
149
+ if (inPrompt) {
150
+ prompt.push(line.replace(/^ {4}/, ""));
151
+ continue;
152
+ }
153
+ const m = /^ {2}([a-z]+): ?(.*)$/.exec(line);
154
+ if (m === null) continue;
155
+ const key = m[1], value = m[2].trim();
156
+ if (key === "prompt") {
157
+ inPrompt = true;
158
+ continue;
159
+ }
160
+ scalars[key] !== void 0 ? scalars[key] += ` ${value}` : scalars[key] = value;
161
+ }
162
+ const turnRaw = scalars["turn"] ?? "";
163
+ const turnSeq = /^\d+$/.test(turnRaw) ? Number(turnRaw) : null;
164
+ const errorRaw = scalars["error"] ?? "";
165
+ return {
166
+ t: "polled",
167
+ intent: ((scalars["intent"] ?? "").split(" ")[0] ?? "") === "goal-edit" ? "goal-edit" : "worker-step",
168
+ status,
169
+ ...status === "ready" ? {
170
+ text: prompt.join("\n"),
171
+ turnSeq
172
+ } : {},
173
+ ...status === "failed" ? {
174
+ error: errorRaw,
175
+ turnSeq
176
+ } : {}
177
+ };
178
+ }
179
+ /** The autorun flag, read from the same payload — the row is durable, the flag is not. */
180
+ function parseBarAutorun(text) {
181
+ const m = /^ {2}autorun: (armed|disarmed)\b/m.exec(text);
182
+ return m === null ? null : m[1];
183
+ }
184
+ /**
185
+ * Does the host say this session is busy right now?
186
+ *
187
+ * The dock's props are HOOKS (`useSessionStatus`, `useChat`, `useInput`, …) — documented in
188
+ * docs/contract.md § "What a dock component is handed" — so a component that waits for a scalar
189
+ * `props.running` waits forever, which is precisely why Autorun declared itself unavailable while the host
190
+ * was publishing the signal one field away. Shapes differ across hosts and versions, so every form is folded
191
+ * here, in one pure function, with `undefined` (unknown) kept distinct from `false` (idle): the loop refuses
192
+ * to start a turn on an unknown, and that distinction is the whole safety property.
193
+ */
194
+ function deriveRunning(status) {
195
+ if (status === void 0 || status === null) return void 0;
196
+ if (typeof status === "boolean") return status;
197
+ if (typeof status === "string") {
198
+ if (/^(running|streaming|busy|active)$/i.test(status)) return true;
199
+ if (/^(idle|done|stopped|finished|ready|waiting)$/i.test(status)) return false;
200
+ return;
201
+ }
202
+ if (typeof status === "object") {
203
+ const s = status;
204
+ if (s.running !== void 0) return typeof s.running === "boolean" ? s.running : void 0;
205
+ return deriveRunning(s.status ?? s.state ?? s.phase);
206
+ }
207
+ }
208
+ // src/client/prompt-dock.ts
209
+ /**
210
+ * The Suggested Next Step dock row (#113) — thin by design.
211
+ *
212
+ * Every decision lives in `prompt-bar.ts` where a row can pin it; this file only moves pixels and bytes
213
+ * between the host slot and that reducer. That split is the reason it exists: the ghost was rejected for
214
+ * behaviour that had no home, and "one turn, one offer" cannot be tested in a component.
215
+ *
216
+ * Placement is measured, not styled: `conversation.input.dock` is the host's full-width stack rendered
217
+ * ABOVE the composer card (todo `order: 0`, goal `10`, queue `20`), so `order: 30` puts this row under every
218
+ * other dock entry and directly above the input — and because it is a docked row rather than an overlay, it
219
+ * can neither cover the placeholder nor collide with the send buttons. The ghost's ugliest defect was
220
+ * structural, and the structure fixes it.
221
+ *
222
+ * The bar is PUSH-SHAPED, PULL-FED. A registrant can own a reactive observable (`hooks: { x }` → prop
223
+ * `useX`) — that is how the goal bar gets its `activation` — but a plugin cannot publish a projection, and
224
+ * the only plugin-reachable call path is `remote.commands.execute`. So the component pulls the `state` verb
225
+ * on edges it can actually observe. Stated here because the alternative is a reader assuming a subscription
226
+ * that does not exist.
227
+ *
228
+ * @module dsh-chapters/src/client/prompt-dock
229
+ */
230
+ const CSS$1 = [
231
+ ".dsh-chapters_promptDock{box-sizing:border-box;width:calc(100% - 4 * var(--dsh-composer-dock-inset));max-width:calc(var(--dsh-composer-card-max-width) - 4 * var(--dsh-composer-dock-inset));margin:0 auto}",
232
+ ".dsh-chapters_promptBar{isolation:isolate;box-sizing:border-box;width:100%;border-radius:var(--dsw-radius-md);height:36px;box-shadow:var(--dsw-elevation-panel);border:0;align-items:center;gap:10px;padding:4px 5px 4px 12px;display:flex;position:relative}",
233
+ ".dsh-chapters_promptBar::before{z-index:-1;border-radius:inherit;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);content:\"\";pointer-events:none;position:absolute;inset:0}",
234
+ ".dsh-chapters_promptExpanded::before{align-items:flex-start;position:absolute;inset:0}",
235
+ ".dsh-chapters_promptGlyph{color:var(--dsw-alias-label-tertiary);flex:none;display:inline-flex}",
236
+ ".dsh-chapters_promptLabel{color:var(--dsw-alias-label-primary);flex:none;font-size:13px;font-weight:500;line-height:24px}",
237
+ ".dsh-chapters_promptText{min-width:0;color:var(--dsw-alias-label-primary-dimmed);font-size:13px;line-height:20px;cursor:pointer;border:0;background:0 0;padding:0;text-align:left;flex:1}",
238
+ ".dsh-chapters_promptEllipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;display:block}",
239
+ ".dsh-chapters_promptFull{white-space:pre-wrap;overflow:auto;max-height:9rem;display:block}",
240
+ ".dsh-chapters_promptError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:20px;flex:none;max-width:34%}",
241
+ ".dsh-chapters_promptNote{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:20px;min-width:0}",
242
+ ".dsh-chapters_promptIntent{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px}",
243
+ ".dsh-chapters_promptActions{flex:none;align-items:center;gap:10px;display:flex}",
244
+ ".dsh-chapters_promptBtn{corner-shape:round;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:999px;justify-content:center;align-items:center;padding:0;display:inline-flex;font-size:13px}",
245
+ ".dsh-chapters_promptBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
246
+ ".dsh-chapters_promptBtn:disabled{opacity:.4;cursor:default}"
247
+ ].join("\n");
248
+ const CSS_ID = "dsh-chapters/prompt-dock.css";
249
+ const ensureCss = () => {
250
+ if (typeof document === "undefined") return;
251
+ if (document.getElementById(CSS_ID) !== null) return;
252
+ const style = document.createElement("style");
253
+ style.id = CSS_ID;
254
+ style.setAttribute("data-plugin-css", CSS_ID);
255
+ style.textContent = CSS$1;
256
+ document.head.appendChild(style);
257
+ };
258
+ /** The analysis glyph, drawn to the goal icon's metric (16 viewBox, stroke currentColor, width 1).
259
+ * A prism-and-arrow: the feature reads the situation and points at the next move. */
260
+ const Glyph = () => (0, react.createElement)("svg", {
261
+ viewBox: "0 0 16 16",
262
+ width: 14,
263
+ height: 14,
264
+ fill: "none",
265
+ "aria-hidden": "true",
266
+ "stroke-width": "1"
267
+ }, (0, react.createElement)("path", {
268
+ d: "M2.5 3.5h11L8 9.5 2.5 3.5Z",
269
+ stroke: "currentColor"
270
+ }), (0, react.createElement)("path", {
271
+ d: "M8 9.5V14",
272
+ stroke: "currentColor"
273
+ }), (0, react.createElement)("path", {
274
+ d: "M6 12l2 2 2-2",
275
+ stroke: "currentColor"
276
+ }));
277
+ /** One action button, labelled through the host tooltip exactly as the goal bar labels its own. */
278
+ const Action = (props) => {
279
+ const button = (0, react.createElement)("button", {
280
+ type: "button",
281
+ className: "dsh-chapters_promptBtn",
282
+ "aria-label": props.label,
283
+ ...props.disabled === true ? { disabled: true } : {},
284
+ ...props.title !== void 0 ? { title: props.title } : {},
285
+ onClick: props.onClick
286
+ }, props.glyph);
287
+ return (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
288
+ label: props.label,
289
+ side: "bottom",
290
+ children: button
291
+ });
292
+ };
293
+ /** Run is allowed whenever there is an offer; AUTORUN is what the intent and the goal gate. Keeping the
294
+ * human's own button enabled is deliberate — the point of the protocol is to route a goal edit to a keystroke,
295
+ * never to disable the human's ability to send it. */
296
+ /**
297
+ * The words the collapsed row shows, per state. `idle` and `analyzing` must say something, because an empty
298
+ * row is indistinguishable from a broken one — the whole reason this feature took a day to read: the bar was
299
+ * live, enabled, and rendering nothing but whitespace.
300
+ */
301
+ function lineFor(state) {
302
+ if (state.status === "ready") return state.text ?? "";
303
+ if (state.status === "analyzing") return "Analyzing the session…";
304
+ if (state.status === "failed") return state.error !== null && state.error !== "" ? `Cannot recommend: ${state.error}` : "Cannot recommend a next step right now";
305
+ return "No suggestion yet — I will recommend one after your next turn";
306
+ }
307
+ const canRunHere = (state) => state.text !== null && state.text !== "";
308
+ /** How long a state pull may take before we call it failed. Generous on purpose: this is a local RPC that
309
+ * answers in milliseconds when the session is attachable, and the failure it guards against is a hang. */
310
+ const PULL_TIMEOUT_MS = 15e3;
311
+ /** Retry ceiling for the FIRST pull only, in attempts and spacing. A mount-time race must not be able to
312
+ * starve the bar forever, and a real `off` answer stops the loop on the first reply either way. */
313
+ /** Cadence of the keep-poll. A local RPC that answers in milliseconds; the cost is one command per
314
+ * interval, and it ends the moment the state is `off`. */
315
+ const POLL_SPACING_MS = 1e4;
316
+ function execWithTimeout(exec, line, ms) {
317
+ return new Promise((resolve, reject) => {
318
+ const timer = setTimeout(() => {
319
+ reject(/* @__PURE__ */ new Error(`no answer within ${String(ms)}ms`));
320
+ }, ms);
321
+ exec(line).then((text) => {
322
+ clearTimeout(timer);
323
+ resolve(text);
324
+ }, (error) => {
325
+ clearTimeout(timer);
326
+ reject(error instanceof Error ? error : new Error(String(error)));
327
+ });
328
+ });
329
+ }
330
+ function PromptDock(props) {
331
+ const { sessionId, inputActions, exec } = props;
332
+ const [state, dispatch] = (0, react.useReducer)(promptBarReducer, initialBarState(), (s) => ({
333
+ ...s,
334
+ running: props.running ?? true
335
+ }));
336
+ const pulling = (0, react.useRef)(false);
337
+ const probed = (0, react.useRef)(false);
338
+ const pulled = (0, react.useRef)(false);
339
+ const alive = (0, react.useRef)(true);
340
+ (0, react.useEffect)(() => () => {
341
+ alive.current = false;
342
+ }, []);
343
+ const [busy, setBusy] = (0, react.useState)(false);
344
+ const answered = (0, react.useRef)(false);
345
+ const replyStatus = (0, react.useRef)("unknown");
346
+ const stateRef = (0, react.useRef)(initialBarState());
347
+ stateRef.current = state;
348
+ if (!probed.current) {
349
+ probed.current = true;
350
+ console.info(`[client:prompt] inputActions keys: ${Object.keys(inputActions ?? {}).join(",") || "NONE"}`);
351
+ }
352
+ const refresh = (0, react.useCallback)(async () => {
353
+ if (exec === void 0 || pulling.current) return;
354
+ pulling.current = true;
355
+ try {
356
+ const text = await execWithTimeout(exec, BAR_STATE_COMMAND, PULL_TIMEOUT_MS);
357
+ try {
358
+ window.__dshPromptReply = text;
359
+ } catch {}
360
+ const ev = parseBarState(text);
361
+ if (ev !== null) {
362
+ answered.current = true;
363
+ replyStatus.current = ev.status ?? replyStatus.current;
364
+ dispatch(ev);
365
+ }
366
+ const autorun = parseBarAutorun(text);
367
+ if (autorun !== null) dispatch({
368
+ t: "autorun",
369
+ on: autorun === "armed"
370
+ });
371
+ } catch (error) {
372
+ console.warn(`[client:prompt] pull failed: ${String(error?.message ?? error).slice(0, 180)}`);
373
+ } finally {
374
+ pulling.current = false;
375
+ }
376
+ }, [exec]);
377
+ (0, react.useEffect)(() => {
378
+ ensureCss();
379
+ if (pulled.current) return;
380
+ pulled.current = true;
381
+ let timer;
382
+ const tick = async () => {
383
+ await refresh();
384
+ if (replyStatus.current === "off") return;
385
+ timer = setTimeout(() => {
386
+ tick();
387
+ }, POLL_SPACING_MS);
388
+ };
389
+ tick();
390
+ return () => {
391
+ if (timer !== void 0) clearTimeout(timer);
392
+ };
393
+ }, [refresh]);
394
+ (0, react.useEffect)(() => {
395
+ dispatch({
396
+ t: "running",
397
+ running: props.running ?? true
398
+ });
399
+ }, [props.running]);
400
+ const run = (0, react.useCallback)(() => {
401
+ if (state.text === null || state.text === "" || exec === void 0) return;
402
+ setBusy(true);
403
+ exec(BAR_RUN_COMMAND).then((out) => {
404
+ if (!alive.current) return;
405
+ if (/^cannot run:/i.test(out.trim())) dispatch({
406
+ t: "polled",
407
+ status: "failed",
408
+ text: state.text,
409
+ error: out.trim().slice(0, 200),
410
+ turnSeq: state.turnSeq
411
+ });
412
+ else dispatch({ t: "collapse" });
413
+ }).catch((error) => {
414
+ console.warn(`[client:prompt] run failed: ${String(error?.message ?? error).slice(0, 160)}`);
415
+ }).finally(() => {
416
+ if (alive.current) setBusy(false);
417
+ });
418
+ }, [
419
+ exec,
420
+ state.text,
421
+ state.turnSeq
422
+ ]);
423
+ (0, react.useEffect)(() => {
424
+ if (!shouldAutoFire(state)) return;
425
+ run();
426
+ }, [state, run]);
427
+ const sessionSnapshot = (props.useSession ?? (() => void 0))((snapshot) => snapshot);
428
+ const autorunDisabled = (props.running ?? deriveRunning(sessionSnapshot)) === void 0;
429
+ if (sessionId === void 0 || !barVisible(state)) return null;
430
+ const populate = () => {
431
+ console.info(`[client:prompt] populate expanded=${String(state.expanded)} hasInsertText=${String(typeof inputActions?.insertText)} hasSetDraft=${String(typeof inputActions?.setDraft)} textLen=${String(state.text?.length ?? -1)}`);
432
+ if (!state.expanded) {
433
+ dispatch({ t: "expand" });
434
+ return;
435
+ }
436
+ if (state.text === null || state.text === "") return;
437
+ if (typeof inputActions?.insertText === "function") {
438
+ try {
439
+ document.querySelector("[contenteditable=\"true\"][role=\"textbox\"]")?.focus();
440
+ } catch {}
441
+ const refuse = (why) => {
442
+ console.warn(`[client:prompt] click-to-draft refused: ${why}`);
443
+ dispatch({
444
+ t: "note",
445
+ note: "This host does not let a plugin write the composer, so the prompt cannot be edited before sending — Run submits it as your turn"
446
+ });
447
+ };
448
+ try {
449
+ inputActions.insertText(state.text);
450
+ const box = document.querySelector("[contenteditable=\"true\"][role=\"textbox\"]");
451
+ const head = state.text.slice(0, 24);
452
+ if ((box?.innerText ?? "").indexOf(head) === -1) setTimeout(() => {
453
+ if ((document.querySelector("[contenteditable=\"true\"][role=\"textbox\"]")?.innerText ?? "").indexOf(head) === -1) refuse("insertText reported success and left the editor empty");
454
+ }, 400);
455
+ } catch (error) {
456
+ refuse(String(error?.message ?? error).slice(0, 160));
457
+ }
458
+ setTimeout(() => {
459
+ try {
460
+ const box = document.querySelector("[contenteditable=\"true\"][role=\"textbox\"]");
461
+ console.info(`[client:prompt] after insertText: editorText=${String(box?.innerText?.length ?? "NO-BOX")} focused=${String(document.activeElement === box)}`);
462
+ } catch {}
463
+ }, 250);
464
+ return;
465
+ }
466
+ if (typeof inputActions?.setDraft === "function") {
467
+ inputActions.setDraft(state.text);
468
+ return;
469
+ }
470
+ console.warn("[client:prompt] click-to-draft refused: this host exposes neither insertText nor setDraft");
471
+ };
472
+ return (0, react.createElement)("div", {
473
+ className: "dsh-chapters_promptDock",
474
+ "data-prompt-bar": "true"
475
+ }, (0, react.createElement)("div", { className: `dsh-chapters_promptBar${state.expanded ? " dsh-chapters_promptExpanded" : ""}` }, (0, react.createElement)("span", { className: "dsh-chapters_promptGlyph" }, (0, react.createElement)(Glyph)), (0, react.createElement)("span", { className: "dsh-chapters_promptLabel" }, barLabel(state) ?? ""), ...state.intent === "goal-edit" ? [(0, react.createElement)("span", {
476
+ key: "intent",
477
+ className: "dsh-chapters_promptIntent",
478
+ "aria-label": "This step edits the goal"
479
+ }, "edits the goal · run it yourself")] : [], (0, react.createElement)("button", {
480
+ type: "button",
481
+ className: "dsh-chapters_promptText",
482
+ "aria-label": state.expanded ? "Put this suggested prompt in the input box" : "Show the full suggested prompt",
483
+ onClick: populate
484
+ }, state.expanded ? (0, react.createElement)("span", { className: "dsh-chapters_promptFull" }, state.text ?? "") : (0, react.createElement)("span", { className: "dsh-chapters_promptEllipsis" }, lineFor(state))), ...state.note === "" ? [] : [(0, react.createElement)("span", {
485
+ key: "note",
486
+ className: "dsh-chapters_promptNote",
487
+ role: "status"
488
+ }, state.note)], ...state.error === "" ? [] : [(0, react.createElement)("span", {
489
+ key: "err",
490
+ className: "dsh-chapters_promptError",
491
+ role: "alert"
492
+ }, state.error)], (0, react.createElement)("div", { className: "dsh-chapters_promptActions" }, (0, react.createElement)(Action, {
493
+ label: "Run the suggested next step",
494
+ glyph: "▶",
495
+ onClick: run,
496
+ disabled: busy,
497
+ ...state.text === null || state.text === "" || !canRunHere(state) ? { disabled: true } : {},
498
+ ...state.intent === "goal-edit" ? { title: "Goal edits need a human turn. Run still works; autorun will never do this one." } : {}
499
+ }), (0, react.createElement)(Action, {
500
+ label: state.autorun === "armed" ? "Autorun off" : "Autorun",
501
+ glyph: state.autorun === "armed" ? "⟳" : "⟲",
502
+ disabled: autorunDisabled,
503
+ ...autorunDisabled ? { title: "Autorun needs the host to publish an idle signal; this build does not, so nothing would fire safely." } : {},
504
+ onClick: () => {
505
+ (async () => {
506
+ const arm = state.autorun !== "armed";
507
+ if (exec !== void 0) await exec(`/chapters-prompt autorun ${arm ? "on" : "off"}`);
508
+ dispatch({
509
+ t: "autorun",
510
+ on: arm
511
+ });
512
+ await refresh();
513
+ })();
514
+ }
515
+ }), (0, react.createElement)(Action, {
516
+ label: "Turn prompt suggestions off for this session",
517
+ glyph: "✕",
518
+ onClick: () => {
519
+ (async () => {
520
+ if (exec !== void 0) await exec("/chapters-prompt off");
521
+ dispatch({ t: "off" });
522
+ })();
523
+ }
524
+ }))));
525
+ }
526
+ // src/client/doors.ts
10
527
  const DOOR_ACTIONS = [{
11
528
  id: "chapters-fork",
12
529
  order: 30,
@@ -275,105 +792,21 @@ window.__ModuleLoader__.load({
275
792
  }
276
793
  };
277
794
  }
278
- //#endregion
279
- //#region src/client/prompt-ghost.ts
280
- /**
281
- * The ONE visibility predicate, in the host's own terms: the shipped
282
- * placeholder shows on `draft === '' && attachments.length === 0`
283
- * (InputBar.tsx `showPlaceholder`) — whitespace-only is CONTENT, it hides the
284
- * guidance. The ghost agrees exactly, so the two layers can never disagree
285
- * about "empty", and a typed (or space-typed) draft hides ours instantly.
286
- */
287
- function shouldShowGhost(phase, running, draft, hasAttachments, suggestion) {
288
- return phase === "shown" && !running && !hasAttachments && draft === "" && suggestion !== null && suggestion.text !== "";
289
- }
290
- /**
291
- * The gesture decision for a keydown while the ghost is visible:
292
- * ArrowRight accepts, Escape dismisses, anything else is the host's business.
293
- * Returns the action for the caller to apply (and whether to preventDefault).
294
- */
295
- function ghostKeyAction(key) {
296
- if (key === "ArrowRight") return "accept";
297
- if (key === "Escape") return "dismiss";
298
- return null;
299
- }
300
- /**
301
- * Execute `/chapters-prompt suggest` for one session. Never throws: every arm
302
- * answers. `refused: true` marks a refusal worth retrying at the next boundary;
303
- * a wire/registry failure is also a refusal, so one bad network cannot loop.
304
- */
305
- function makePromptFetcher(remote) {
306
- return async (sessionId) => {
307
- if (remote.commands?.execute === void 0) return {
308
- ok: false,
309
- text: "commands remote unavailable",
310
- refused: true
311
- };
312
- try {
313
- const result = (await remote.commands.execute(sessionId, "/chapters-prompt suggest", [])).value?.result;
314
- const text = typeof result?.text === "string" ? result.text : "";
315
- if (result?.kind === "success" && text !== "") return {
316
- ok: true,
317
- text
318
- };
319
- return {
320
- ok: false,
321
- text,
322
- refused: true
323
- };
324
- } catch (error) {
325
- return {
326
- ok: false,
327
- text: String(error?.message ?? error).slice(0, 90),
328
- refused: true
329
- };
330
- }
331
- };
332
- }
333
795
  /**
334
- * Execute `/chapters-prompt model` once per mount and pull the spec out of the
335
- * status sentence for the hover label. Absent/unparseable ⇒ no label (the chip
336
- * still works) — a display fact is never a dependency.
796
+ * Execute a host command line for one session and return its text, or '' when the command plane is
797
+ * unavailable. The bar's `state` pull and `off` write ride this, because it is the ONE place the client
798
+ * talks to the wire: the guard row "there is no second runner body in the client chrome" exists to keep
799
+ * `index.ts` as registration chrome, and a duplicated `commands.execute` in two files is how a wire
800
+ * behaviour drifts from its own test (#59's one-predicate rule, applied to the transport).
337
801
  */
338
- function makeModelLabelFetcher(remote) {
339
- return async (sessionId) => {
340
- if (remote.commands?.execute === void 0) return null;
341
- try {
342
- const res = await remote.commands.execute(sessionId, "/chapters-prompt model", []);
343
- return (res.value?.result?.kind === "success" ? res.value.result.text ?? "" : "").match(/^prompt model: (\S+(?: \S+)*) \(.*\) — ON$/)?.[1] ?? null;
344
- } catch {
345
- return null;
346
- }
347
- };
802
+ async function runCommandText(remote, sessionId, line) {
803
+ const exec = remote?.commands?.execute;
804
+ if (exec === void 0) return "";
805
+ const res = await exec(sessionId, line, []);
806
+ if (res.ok !== true) return "";
807
+ return res.value?.result?.text ?? "";
348
808
  }
349
- /**
350
- * The boundary bookkeeping as a pure reducer so the component holds no logic:
351
- * `runningEdge` reports the true→false transition of the session.
352
- */
353
- function ghostReducer(state, event) {
354
- switch (event.type) {
355
- case "idle-edge": return {
356
- phase: "idle",
357
- boundary: state.boundary + 1
358
- };
359
- case "fetched":
360
- if (state.phase !== "idle") return state;
361
- return {
362
- ...state,
363
- phase: event.ok ? "shown" : "refused"
364
- };
365
- case "accept": return {
366
- ...state,
367
- phase: "accepted"
368
- };
369
- case "dismiss": return {
370
- ...state,
371
- phase: "dismissed"
372
- };
373
- }
374
- }
375
- //#endregion
376
- //#region src/client/index.ts
809
+ // src/client/index.ts
377
810
  /**
378
811
  * dsh-chapters client entry: the per-message door buttons.
379
812
  *
@@ -425,11 +858,6 @@ window.__ModuleLoader__.load({
425
858
  ".dsh-chapters_forkAction:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
426
859
  ".dsh-chapters_forkAction:disabled{cursor:default;opacity:.4}",
427
860
  ".dsh-chapters_forkNote{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:13px;line-height:20px}",
428
- ".dsh-chapters_ghostWrap{position:absolute;top:2px;left:14px;right:14px;display:flex;justify-content:flex-start;pointer-events:none;z-index:1}",
429
- ".dsh-chapters_ghost{pointer-events:auto;display:inline-flex;align-items:center;gap:6px;max-width:min(70%,560px);padding:2px 10px;border:none;border-radius:12px;cursor:pointer;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-caption);font-size:12px;line-height:18px}",
430
- ".dsh-chapters_ghost:hover{color:var(--dsw-alias-label-secondary)}",
431
- ".dsh-chapters_ghostText{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}",
432
- ".dsh-chapters_ghostGlyph{flex:none;font-weight:600}",
433
861
  "button[class*=\"_action\"][aria-label=\"Branch into a new conversation\"],button[class*=\"_action\"][aria-label=\"在新对话中分支\"]{display:none !important}"
434
862
  ].join("\n");
435
863
  const CSS_TAG_ID = "dsh-chapters/assistant-action.css";
@@ -489,118 +917,6 @@ window.__ModuleLoader__.load({
489
917
  }), note !== null ? (0, react.createElement)("span", { className: "dsh-chapters_forkNote" }, note) : null);
490
918
  };
491
919
  }
492
- function makeGhostView() {
493
- return function ChaptersPromptGhost(props) {
494
- const snap = props.useInput((s) => s);
495
- const running = props.useSession((s) => s.running) ?? false;
496
- const draft = snap?.draft ?? "";
497
- const hasAttachments = (snap?.attachmentIds.length ?? 0) > 0;
498
- const [state, setState] = (0, react.useState)({
499
- phase: "idle",
500
- boundary: 0
501
- });
502
- const [suggestion, setSuggestion] = (0, react.useState)(null);
503
- const [model, setModel] = (0, react.useState)(null);
504
- const [labelKnown, setLabelKnown] = (0, react.useState)(false);
505
- const alive = (0, react.useRef)(true);
506
- (0, react.useEffect)(() => () => {
507
- alive.current = false;
508
- }, []);
509
- const dispatch = (0, react.useCallback)((ev) => {
510
- setState((s) => ghostReducer(s, ev));
511
- }, []);
512
- const fetchLabelRef = (0, react.useRef)(props.fetchLabel);
513
- fetchLabelRef.current = props.fetchLabel;
514
- const labelFetched = (0, react.useRef)(false);
515
- (0, react.useEffect)(() => {
516
- if (labelFetched.current) return;
517
- labelFetched.current = true;
518
- fetchLabelRef.current().then((m) => {
519
- if (!alive.current) return;
520
- setModel(m);
521
- setLabelKnown(true);
522
- });
523
- }, []);
524
- const prevRunning = (0, react.useRef)(null);
525
- (0, react.useEffect)(() => {
526
- const first = prevRunning.current === null;
527
- const edge = !running && (first || prevRunning.current === true);
528
- prevRunning.current = running;
529
- if (edge) dispatch({ type: "idle-edge" });
530
- }, [running, dispatch]);
531
- const fetchedFor = (0, react.useRef)(-1);
532
- const { phase, boundary } = state;
533
- (0, react.useEffect)(() => {
534
- if (!labelKnown || model === null) return;
535
- if (phase !== "idle" || fetchedFor.current === boundary) return;
536
- fetchedFor.current = boundary;
537
- props.fetchSuggestion().then((r) => {
538
- if (!alive.current) return;
539
- setSuggestion(r.ok ? {
540
- text: r.text,
541
- model: model !== null ? model : void 0
542
- } : null);
543
- dispatch({
544
- type: "fetched",
545
- ok: r.ok
546
- });
547
- });
548
- }, [
549
- phase,
550
- boundary,
551
- props,
552
- dispatch,
553
- labelKnown,
554
- model
555
- ]);
556
- const accept = (0, react.useCallback)(() => {
557
- if (suggestion === null) return;
558
- props.inputActions?.setDraft(suggestion.text);
559
- dispatch({ type: "accept" });
560
- }, [
561
- suggestion,
562
- props,
563
- dispatch
564
- ]);
565
- const visible = shouldShowGhost(state.phase, running, draft, hasAttachments, suggestion);
566
- (0, react.useEffect)(() => {
567
- if (!visible) return;
568
- const onKey = (event) => {
569
- const action = ghostKeyAction(event.key);
570
- if (action === null) return;
571
- const target = event.target;
572
- if (target === null || typeof target.closest !== "function" || target.closest("[data-composer-input]") === null) return;
573
- event.preventDefault();
574
- event.stopPropagation();
575
- if (action === "accept") accept();
576
- else dispatch({ type: "dismiss" });
577
- };
578
- document.addEventListener("keydown", onKey, true);
579
- return () => {
580
- document.removeEventListener("keydown", onKey, true);
581
- };
582
- }, [
583
- visible,
584
- accept,
585
- dispatch
586
- ]);
587
- if (!visible || suggestion === null) return null;
588
- const tip = `next step suggested${model !== null ? ` by ${model}` : ""} — → or click puts it in the draft, Esc dismisses`;
589
- return (0, react.createElement)("span", { className: "dsh-chapters_ghostWrap" }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
590
- label: tip,
591
- side: "top",
592
- children: (0, react.createElement)("button", {
593
- type: "button",
594
- className: "dsh-chapters_ghost",
595
- "aria-label": "Accept the suggested next prompt",
596
- onClick: accept
597
- }, (0, react.createElement)("span", {
598
- className: "dsh-chapters_ghostGlyph",
599
- "aria-hidden": "true"
600
- }, "→"), (0, react.createElement)("span", { className: "dsh-chapters_ghostText" }, suggestion.text))
601
- }));
602
- };
603
- }
604
920
  function apply(ctx) {
605
921
  injectStyles();
606
922
  const slots = ctx.slots;
@@ -643,20 +959,13 @@ window.__ModuleLoader__.load({
643
959
  order: door.order,
644
960
  inject: (sessionId) => ({ run: () => run(sessionId) })
645
961
  }, component)));
646
- const ghostView = makeGhostView();
647
- const fetchSuggestion = makePromptFetcher(remote);
648
- const fetchLabel = makeModelLabelFetcher(remote);
649
- slots.inject("conversation.input.overlay", () => slots.register({
650
- name: "conversation.input.overlay",
651
- id: "chapters-prompt-ghost",
652
- order: 12,
653
- inject: (sessionId) => ({
654
- fetchSuggestion: () => fetchSuggestion(sessionId),
655
- fetchLabel: () => fetchLabel(sessionId)
656
- })
657
- }, ghostView));
962
+ slots.inject("conversation.input.dock", () => slots.register({
963
+ name: "conversation.input.dock",
964
+ id: "chapters-prompt",
965
+ order: 30,
966
+ inject: (sessionId) => ({ exec: (line) => runCommandText(remote, sessionId, line) })
967
+ }, PromptDock));
658
968
  }
659
- //#endregion
660
969
  exports.apply = apply;
661
970
  exports.inject = inject;
662
971
  return module.exports;