@treeseed/dsh-chapters 0.1.24 → 0.2.1

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 (103) hide show
  1. package/README.md +2 -1
  2. package/cordis.patch.yml +49 -1
  3. package/docs/architecture.md +1 -496
  4. package/docs/backlog.md +234 -4
  5. package/docs/contract.md +127 -1
  6. package/docs/development.md +248 -12
  7. package/docs/host-compaction-seam.md +108 -0
  8. package/docs/knowledge-repo.md +532 -16
  9. package/docs/project-state.md +323 -0
  10. package/docs/verify.md +266 -0
  11. package/lib/archive.d.ts +49 -1
  12. package/lib/archive.js +56 -2
  13. package/lib/archive.js.map +1 -1
  14. package/lib/arrival.js +24 -10
  15. package/lib/arrival.js.map +1 -1
  16. package/lib/attach.d.ts +46 -0
  17. package/lib/attach.js +120 -0
  18. package/lib/attach.js.map +1 -0
  19. package/lib/auto-save.d.ts +74 -0
  20. package/lib/auto-save.js +175 -26
  21. package/lib/auto-save.js.map +1 -1
  22. package/lib/client.js +413 -11
  23. package/lib/client.js.map +1 -1
  24. package/lib/commands.d.ts +8 -1
  25. package/lib/commands.js +135 -7
  26. package/lib/commands.js.map +1 -1
  27. package/lib/compose.js +25 -6
  28. package/lib/compose.js.map +1 -1
  29. package/lib/continue-core.d.ts +24 -1
  30. package/lib/continue-core.js +41 -9
  31. package/lib/continue-core.js.map +1 -1
  32. package/lib/diag.d.ts +55 -0
  33. package/lib/diag.js +186 -0
  34. package/lib/diag.js.map +1 -0
  35. package/lib/engine-core.d.ts +53 -1
  36. package/lib/engine-core.js +64 -7
  37. package/lib/engine-core.js.map +1 -1
  38. package/lib/engine.d.ts +99 -0
  39. package/lib/engine.js +488 -36
  40. package/lib/engine.js.map +1 -1
  41. package/lib/enrich-queue.js +28 -2
  42. package/lib/enrich-queue.js.map +1 -1
  43. package/lib/enrich-wire.d.ts +92 -2
  44. package/lib/enrich-wire.js +135 -17
  45. package/lib/enrich-wire.js.map +1 -1
  46. package/lib/enrich.d.ts +27 -2
  47. package/lib/enrich.js +114 -18
  48. package/lib/enrich.js.map +1 -1
  49. package/lib/index.d.ts +52 -0
  50. package/lib/index.js +182 -2
  51. package/lib/index.js.map +1 -1
  52. package/lib/indexing.d.ts +7 -0
  53. package/lib/indexing.js.map +1 -1
  54. package/lib/injections.d.ts +1 -0
  55. package/lib/injections.js +38 -0
  56. package/lib/injections.js.map +1 -1
  57. package/lib/last-resort.d.ts +24 -0
  58. package/lib/last-resort.js +27 -0
  59. package/lib/last-resort.js.map +1 -0
  60. package/lib/prompt.d.ts +130 -0
  61. package/lib/prompt.js +255 -0
  62. package/lib/prompt.js.map +1 -0
  63. package/lib/registry.d.ts +39 -1
  64. package/lib/registry.js +76 -9
  65. package/lib/registry.js.map +1 -1
  66. package/lib/render.d.ts +31 -3
  67. package/lib/render.js +75 -3
  68. package/lib/render.js.map +1 -1
  69. package/lib/repair-run.d.ts +49 -0
  70. package/lib/repair-run.js +423 -0
  71. package/lib/repair-run.js.map +1 -0
  72. package/lib/repair.d.ts +200 -0
  73. package/lib/repair.js +354 -0
  74. package/lib/repair.js.map +1 -0
  75. package/lib/rules-commands.d.ts +22 -0
  76. package/lib/rules-commands.js +177 -20
  77. package/lib/rules-commands.js.map +1 -1
  78. package/lib/rules.d.ts +184 -7
  79. package/lib/rules.js +312 -27
  80. package/lib/rules.js.map +1 -1
  81. package/lib/save-core.d.ts +31 -1
  82. package/lib/save-core.js +153 -17
  83. package/lib/save-core.js.map +1 -1
  84. package/lib/session-rules.d.ts +222 -0
  85. package/lib/session-rules.js +454 -0
  86. package/lib/session-rules.js.map +1 -0
  87. package/lib/store.d.ts +22 -1
  88. package/lib/store.js +94 -17
  89. package/lib/store.js.map +1 -1
  90. package/lib/sync.js +13 -1
  91. package/lib/sync.js.map +1 -1
  92. package/lib/tools.d.ts +14 -0
  93. package/lib/tools.js +321 -32
  94. package/lib/tools.js.map +1 -1
  95. package/lib/types.d.ts +12 -1
  96. package/lib/types.js.map +1 -1
  97. package/lib/version.d.ts +9 -0
  98. package/lib/version.js +10 -0
  99. package/lib/version.js.map +1 -0
  100. package/lib/warnings.d.ts +10 -0
  101. package/lib/warnings.js +18 -0
  102. package/lib/warnings.js.map +1 -0
  103. package/package.json +9 -3
package/lib/client.js CHANGED
@@ -24,20 +24,132 @@ window.__ModuleLoader__.load({
24
24
  }];
25
25
  /** Pull the child id out of either door's durable success text — both end in
26
26
  * `… is session ch-…` (src/tools.ts), so this regex is door-agnostic. */
27
+ /**
28
+ * Which build of THIS file produced a door note (#77). When the live probe reported a roster
29
+ * trace of `missing → missing → missing` — meaning adoption should have fired three times — while
30
+ * showing no adoption outcome at all, the first question stopped being "why did the client refuse"
31
+ * and became "did the browser run the code I am reading?". A note that names its own build makes
32
+ * that a glance instead of an archaeology session into symlinked plugin trees.
33
+ */
34
+ const DOOR_BUILD = "2026-10-22T23:55 door-build-stamp";
27
35
  const CHILD_ID = /is session (ch-[\w-]+)/;
28
36
  /** The quoted title the door gave the child, matched by its OWN noun so a
29
37
  * refusal or an unrelated quote cannot pass for it. */
30
38
  const doorTitle = (noun) => new RegExp(`${noun} \\u201C([^\\u201D]+)\\u201D is session`);
31
- function openByService(sessions, sessionId) {
39
+ /**
40
+ * A refusal reason that can never render as nothing. `String(err.message ?? err)` is `''` for a
41
+ * host error built with an empty message, and an empty reason disappears from the note — the
42
+ * reader then concludes "no refusal happened", which is the opposite of the truth (#77).
43
+ */
44
+ function refusalReason(error) {
45
+ const trimmed = (error instanceof Error ? error.message : String(error)).trim();
46
+ const named = trimmed === "" ? error instanceof Error && error.name.trim() !== "" ? error.name : "" : trimmed;
47
+ return (named === "" ? error === null ? "null thrown" : typeof error : named).replace(/\s+/g, " ").slice(0, 90);
48
+ }
49
+ /**
50
+ * Is the child in the client's OWN roster? This is the question that separates two very
51
+ * different failures, and the door used to report the symptom without it:
52
+ * - the client never learned the session exists (roster: missing) → the fix is server-side
53
+ * publication, and no amount of clicking `open` will ever work;
54
+ * - the client knows it and `open` still did nothing (roster: contains) → a navigation bug.
55
+ * Measured host 0.2.0-rc.2: the sessions service exposes `projectList`, `refresh`, `retain`,
56
+ * `handleSessionAdded` — the roster is a projection the client fills from control frames.
57
+ */
58
+ /**
59
+ * The client's own session ids, or `null` when the store cannot be read. ONE reader for both
60
+ * the note and the adoption decision, so the door never acts on a verdict it did not report.
61
+ */
62
+ function rosterIds(sessions) {
63
+ const store = sessions?.list;
64
+ if (typeof store?.getSnapshot !== "function") return null;
65
+ const snap = store.getSnapshot();
66
+ const ids = /* @__PURE__ */ new Set();
67
+ if (Array.isArray(snap?.ids)) for (const id of snap.ids) ids.add(String(id));
68
+ else if (snap?.byId !== void 0) for (const k of Object.keys(snap.byId)) ids.add(k);
69
+ else return null;
70
+ return ids;
71
+ }
72
+ function rosterNote(sessions, child) {
73
+ try {
74
+ const ids = rosterIds(sessions);
75
+ if (ids === null) return ` (roster: ${typeof (sessions?.list)?.getSnapshot !== "function" ? "no list store on the service" : "snapshot unreadable"} — presence of ${child.slice(0, 11)}… is UNKNOWN, not absent)`;
76
+ if (ids.size === 0) return ` (roster: empty — presence of ${child.slice(0, 11)}… is UNKNOWN, not absent)`;
77
+ const n = ids.size;
78
+ return ids.has(child) ? ` (roster: contains ${child.slice(0, 11)}…, ${String(n)} sessions — open() is the failure)` : ` (roster: MISSING, ${String(n)} sessions — the client never catalogued this session)`;
79
+ } catch {
80
+ return "";
81
+ }
82
+ }
83
+ /** The service's own method names, as a hint attached to a failure note (never a guess). */
84
+ function verbsNote(sessions) {
85
+ try {
86
+ const svc = sessions;
87
+ if (svc === null || svc === void 0 || typeof svc !== "object") return " (sessions service: absent)";
88
+ const found = /* @__PURE__ */ new Set();
89
+ let node = svc;
90
+ for (let depth = 0; depth < 6 && node !== null && node !== void 0 && node !== Object.prototype; depth++) {
91
+ for (const key of Object.getOwnPropertyNames(node)) {
92
+ if (key === "constructor") continue;
93
+ try {
94
+ if (typeof svc[key] === "function") found.add(key);
95
+ } catch {}
96
+ }
97
+ node = Object.getPrototypeOf(node);
98
+ }
99
+ const verbs = [...found].sort().slice(0, 24);
100
+ return verbs.length === 0 ? " (sessions service: no methods on the chain)" : ` (sessions verbs: ${verbs.join(", ")})`;
101
+ } catch {
102
+ return " (sessions service: unreadable)";
103
+ }
104
+ }
105
+ function openByService(sessions, sessionId, adopt, nav) {
32
106
  return (async () => {
33
107
  const svc = sessions;
34
- if (typeof svc?.open !== "function") return false;
108
+ if (svc === void 0) return false;
109
+ const navigate = typeof nav?.openSession === "function" ? (id) => nav.openSession(id) : typeof svc?.open === "function" ? (id) => svc.open(id) : null;
110
+ if (navigate === null) {
111
+ adopt?.onStep?.("no-verb");
112
+ return false;
113
+ }
35
114
  try {
36
115
  await svc.refresh?.();
37
116
  } catch {}
117
+ try {
118
+ svc.projectList?.();
119
+ } catch {}
120
+ if (adopt?.needed === true && typeof svc.create === "function") {
121
+ adopt.onStep?.("enter-create");
122
+ try {
123
+ await svc.create({ sessionId });
124
+ adopt.onStep?.("settle-create");
125
+ try {
126
+ svc.projectList?.();
127
+ } catch {}
128
+ const after = rosterIds(sessions);
129
+ adopt.onAfter?.(after === null ? null : after.has(sessionId));
130
+ } catch (error) {
131
+ adopt.onStep?.("refuse-create");
132
+ adopt.onRefused(refusalReason(error));
133
+ }
134
+ }
135
+ if (adopt?.needed === true && typeof svc.using === "function" && rosterIds(sessions)?.has(sessionId) === false) {
136
+ adopt.onStep?.("enter-using");
137
+ try {
138
+ await svc.using(sessionId, { source: "dsh-chapters-door" }, async () => void 0);
139
+ adopt.onStep?.("settle-using");
140
+ try {
141
+ svc.projectList?.();
142
+ } catch {}
143
+ const seen = rosterIds(sessions);
144
+ adopt.onAfter?.(seen === null ? null : seen.has(sessionId));
145
+ } catch (error) {
146
+ adopt.onStep?.("refuse-using");
147
+ adopt.onSubscribeRefused?.(refusalReason(error));
148
+ }
149
+ }
38
150
  const started = Date.now();
39
151
  for (;;) try {
40
- svc.open(sessionId);
152
+ navigate(sessionId);
41
153
  return true;
42
154
  } catch {
43
155
  if (Date.now() - started > 4e3) return false;
@@ -77,7 +189,7 @@ window.__ModuleLoader__.load({
77
189
  }
78
190
  return tryClick();
79
191
  }
80
- function makeDoorRunner(remote, sessions, door) {
192
+ function makeDoorRunner(remote, sessions, door, verifySwitched, opts) {
81
193
  return async function run(sessionId) {
82
194
  try {
83
195
  if (remote.commands?.execute === void 0) return {
@@ -100,18 +212,60 @@ window.__ModuleLoader__.load({
100
212
  ok: true,
101
213
  message: `${door.noun} created — appearing in the sidebar`
102
214
  };
103
- if (await openByService(sessions, child)) return {
104
- ok: true,
105
- message: ""
215
+ const switchedOnce = async () => {
216
+ if (verifySwitched === void 0) return true;
217
+ try {
218
+ return await verifySwitched(child);
219
+ } catch {
220
+ return false;
221
+ }
106
222
  };
223
+ const maxAttempts = typeof sessions?.refresh === "function" ? opts?.attempts ?? 3 : 1;
224
+ let tried = 0;
225
+ let adoptRefusal = "";
226
+ let subscribeRefusal = "";
227
+ const rosterTrace = [];
228
+ const steps = [];
229
+ let adoptSaw;
230
+ for (let i = 0; i < maxAttempts; i++) {
231
+ tried += 1;
232
+ const ids = rosterIds(sessions);
233
+ const needed = ids !== null && !ids.has(child);
234
+ const svcShape = sessions;
235
+ rosterTrace.push(`${ids === null ? "unknown" : ids.size === 0 ? "empty" : ids.has(child) ? "has" : "missing"}${typeof svcShape?.create === "function" ? "+c" : "-c"}${typeof svcShape?.using === "function" ? "+u" : "-u"}${typeof svcShape?.open === "function" ? "+o" : "-o"}${typeof opts?.navigator?.openSession === "function" ? "+w" : "-w"}`);
236
+ if (await openByService(sessions, child, {
237
+ needed,
238
+ onRefused: (reason) => {
239
+ adoptRefusal = reason;
240
+ },
241
+ onSubscribeRefused: (reason) => {
242
+ subscribeRefusal = reason;
243
+ },
244
+ onAfter: (seen) => {
245
+ adoptSaw = seen;
246
+ },
247
+ onStep: (step) => {
248
+ if (!steps.includes(step)) steps.push(step);
249
+ }
250
+ }, opts?.navigator) && await switchedOnce()) return {
251
+ ok: true,
252
+ message: ""
253
+ };
254
+ if (i + 1 < maxAttempts) await new Promise((r) => {
255
+ setTimeout(r, opts?.attemptPauseMs ?? 300).unref?.();
256
+ });
257
+ }
107
258
  const title = doorTitle(door.noun).exec(text)?.[1];
108
- if (title !== void 0 && await openBranchRow(title)) return {
259
+ if (title !== void 0 && await openBranchRow(title) && await switchedOnce()) return {
109
260
  ok: true,
110
261
  message: ""
111
262
  };
112
- return {
263
+ return verifySwitched === void 0 ? {
113
264
  ok: true,
114
265
  message: `${door.noun} created — open it in the sidebar`
266
+ } : {
267
+ ok: true,
268
+ message: `${door.noun} created — the app did not switch after ${String(tried)} attempt${tried === 1 ? "" : "s"}; open it from the sidebar [roster per attempt: ${rosterTrace.join(" → ")}] [tried: ${steps.length === 0 ? "none" : steps.includes("no-verb") ? "no open or openSession verb on any injected service" : steps.join(", ")}] (build: ${DOOR_BUILD})${adoptRefusal === "" ? "" : ` (adoption refused: ${adoptRefusal})`}${subscribeRefusal === "" ? "" : ` (subscribe refused: ${subscribeRefusal})`}${adoptSaw === void 0 ? "" : ` (right after adopt: ${adoptSaw ? "contains" : "missing"} ${child.slice(0, 15)})`}${rosterNote(sessions, child)}${verbsNote(sessions)}`
115
269
  };
116
270
  } catch (error) {
117
271
  return {
@@ -122,6 +276,103 @@ window.__ModuleLoader__.load({
122
276
  };
123
277
  }
124
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
+ /**
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.
337
+ */
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
+ };
348
+ }
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
125
376
  //#region src/client/index.ts
126
377
  /**
127
378
  * dsh-chapters client entry: the per-message door buttons.
@@ -152,7 +403,8 @@ window.__ModuleLoader__.load({
152
403
  "slots",
153
404
  "remote",
154
405
  "remote.commands",
155
- "sessions"
406
+ "sessions",
407
+ "uiWorkspace"
156
408
  ];
157
409
  /**
158
410
  * Style parity with the native IconActions row: rules copied verbatim from the
@@ -173,6 +425,11 @@ window.__ModuleLoader__.load({
173
425
  ".dsh-chapters_forkAction:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
174
426
  ".dsh-chapters_forkAction:disabled{cursor:default;opacity:.4}",
175
427
  ".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}",
176
433
  "button[class*=\"_action\"][aria-label=\"Branch into a new conversation\"],button[class*=\"_action\"][aria-label=\"在新对话中分支\"]{display:none !important}"
177
434
  ].join("\n");
178
435
  const CSS_TAG_ID = "dsh-chapters/assistant-action.css";
@@ -232,6 +489,118 @@ window.__ModuleLoader__.load({
232
489
  }), note !== null ? (0, react.createElement)("span", { className: "dsh-chapters_forkNote" }, note) : null);
233
490
  };
234
491
  }
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
+ }
235
604
  function apply(ctx) {
236
605
  injectStyles();
237
606
  const slots = ctx.slots;
@@ -242,9 +611,30 @@ window.__ModuleLoader__.load({
242
611
  } catch {
243
612
  sessions = void 0;
244
613
  }
614
+ let uiWorkspace;
615
+ try {
616
+ uiWorkspace = ctx.get("uiWorkspace");
617
+ } catch {
618
+ uiWorkspace = void 0;
619
+ }
620
+ const currentSessionId = () => {
621
+ try {
622
+ const raw = window.localStorage.getItem("dsh.sessions.current");
623
+ if (raw === null) return null;
624
+ return JSON.parse(raw)?.sessionId ?? null;
625
+ } catch {
626
+ return null;
627
+ }
628
+ };
245
629
  const rows = DOOR_ACTIONS.map((door) => ({
246
630
  door,
247
- run: makeDoorRunner(remote, sessions, door),
631
+ run: makeDoorRunner(remote, sessions, door, async (child) => {
632
+ for (let i = 0; i < 8; i++) {
633
+ if (currentSessionId() === child) return true;
634
+ await new Promise((r) => setTimeout(r, 250));
635
+ }
636
+ return currentSessionId() === child;
637
+ }, { ...uiWorkspace !== void 0 ? { navigator: uiWorkspace } : {} }),
248
638
  component: makeDoorAction(door)
249
639
  }));
250
640
  slots.inject("conversation.chat.assistant-actions", () => rows.map(({ door, run, component }) => slots.register({
@@ -253,6 +643,18 @@ window.__ModuleLoader__.load({
253
643
  order: door.order,
254
644
  inject: (sessionId) => ({ run: () => run(sessionId) })
255
645
  }, 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));
256
658
  }
257
659
  //#endregion
258
660
  exports.apply = apply;