@treeseed/dsh-chapters 0.2.7 → 0.2.9

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/contract.md CHANGED
@@ -706,6 +706,46 @@ version supplies, so Autorun reported itself unavailable on a host that exposes
706
706
  `{ captureInsertion, insertText, setDraft, addAttachments, removeAttachment, pruneAttachments, submit }`, and
707
707
  the composer-write limitation in #126 is measured against that same kit.
708
708
 
709
+ **The running signal has seats, and the scalar is only one of them (measured 2026-10-31, #194).**
710
+ `SessionSnapshot.running: boolean` (`packages/api/session-controller/src/client/contract/snapshot.ts:69-73`)
711
+ arrives through the dock's standard prop `useSession` — a `MaybeSnapshotSelectorHook<SessionSnapshot>`
712
+ (`useSession((s) => s.running)`), the same seat `ui-session` documents in `SessionStandardProps`.
713
+ Some builds ALSO hand a scalar `running` prop. The shipped dock declared both hook channels
714
+ (`useSessionStatus?`, `useSession?`) and consumed only the scalar — so on a hook-only host the #193
715
+ idle edge never fired and the cadence silently degraded to the conservative 10s-active fallback
716
+ (`props.running ?? true`). Resolution order, locked here: scalar `running` → `useSession((s) =>
717
+ s.running)` → unknown (conservative true, legacy cadence). Conditional invocation of an optional
718
+ hook prop is deliberate: the prop face is fixed per host build, so hook order is stable across
719
+ renders within any one deployment, which is the invariant React actually keeps.
720
+
721
+ **Push transport (interface lock, #194 — probe-verified on host 0.2.1-rc.5, `spikes/push-probe/FINDINGS.md`).**
722
+ Where the host has `appendPluginRecord` (0.2.1+; absent from 0.2.0-rc.2 — grep-verified on both tags),
723
+ the bar's state rides a plugin record and a projection, and the steady state costs ZERO command runs:
724
+
725
+ - Record type: `plugin:dsh-chapters/prompt`. Payload = the bar's durable facts, versioned:
726
+ `{ v: 1, status, text, turnSeq, error, intent, goalRevision, autorun, at }`.
727
+ - Append sites: every durable row write — the runner's `analyzing`/`ready`/`failed`/quiet-`off`
728
+ transitions (it holds the live session) and the bar-access mutations (`setAutorun`, `setOff`,
729
+ `setOn`, `runNow`'s consumption), which gain an optional live-session parameter for the purpose.
730
+ - The function comes ONLY from the host's own module instance — `createRequire(process.argv[1])
731
+ → resolve('@deepseek-ai/dsh-session') → await import(resolved)`. The plugin's OWN copy at the
732
+ IDENTICAL version throws `Receiver must be an instance of class Session` against a host session
733
+ (measured), and a bare import from a `link:` install resolves by realpath into the source tree —
734
+ on a dev machine, into a DIFFERENT host checkout than the running server. No plugin dependency is
735
+ declared for it; detection is `typeof appendPluginRecord === 'function'`, one try behind try/catch,
736
+ and any failure marks push unavailable for the process.
737
+ - Projection: key `chaptersPrompt`, registered ONLY when the writer resolved — registration is the
738
+ feature signal. `apply` folds records last-wins; `wire.view` passes the payload through.
739
+ - Client: the dock reads the standard prop `useProjection('chaptersPrompt')`. A live value renders
740
+ through the SAME reducer (one fold path); `undefined` ⇒ the #203/#208 pull schedule stands,
741
+ unchanged — the fallback is the floor. In push mode the only pulls are the bar's own actions and
742
+ ONE staleness-confirm per idle edge, scheduled at +20s and cancelled when a fresher record arrives
743
+ (the runner writes `analyzing` within moments of turn/end, so a healthy push cancels every confirm):
744
+ a silently dead push degrades to one pull per turn, never to a frozen bar.
745
+ - Records are `ignorable: true` with an unknown `plugin:` type — skipped by readers, rendered by
746
+ nothing; the projection value travels the host's own push channel. Verbs, state block, and the
747
+ pull schedule below are unchanged and remain the contract for pre-0.2.1 hosts.
748
+
709
749
  **Verbs (interface lock, #113).** `/chapters-prompt` accepts `model …|clear`, `off`, `on`, `state`, `autorun on|off`,
710
750
  and **`run`** — submit the offer currently on screen as a **human-authored user turn**, through the same follow-up
711
751
  path autorun uses (`agent.followup(suggestPromptMessage(…))`). Run goes through the command plane rather than the
package/lib/client.js CHANGED
@@ -5,7 +5,73 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ // src/client/tooltip.ts
9
+ /**
10
+ * The host's Tooltip seat, resolved OPTIONALLY (#194-compat — measured on 0.2.1-rc.5, not guessed):
11
+ *
12
+ * `@deepseek-ai/dsh-client-ui-primitives` exists in the 0.2.0-rc.2 host tree and is ABSENT from the
13
+ * entire 0.2.1-rc.5 install tree — no successor package exports `Tooltip`. Our bundle's static
14
+ * import becomes a LAZY require in the built CJS: the bundle loads and components run, but the
15
+ * first render that touches `Tooltip` throws, and React's error boundary swallows the subtree
16
+ * silently. Measured symptoms on 0.2.1: door registration fully healthy (inject returned a
17
+ * disposer; the factory ran; both register effects are functions) yet the assistant-actions region
18
+ * renders ZERO entries, and the dock renders no row — because both wrap their buttons in Tooltip.
19
+ *
20
+ * The fix keeps one resolution point and one fallback (#59 — never per-component try/catch):
21
+ * probe the module once; when it is absent, buttons carry the platform's own `title` tooltip, so
22
+ * hover text survives on every host and the fancy tooltip survives where it exists.
23
+ */
24
+ /**
25
+ * The tooltip recipe, field for field the goal bar's own (GoalBar.tsx:101-142: `portal`,
26
+ * `side="bottom"`, `delayMs={500}`) — the shipped occupant of the same dock strip, whose tooltips
27
+ * are NOT covered by the composer. `portal` is the load-bearing one: it renders the popup at
28
+ * document level instead of inside our dock's stacking context, which is exactly the maintainer's
29
+ * report (#210: "the hoverover tooltips are covered by the main chat input container"). Our old
30
+ * compile-time types rejected portal/delayMs at the overload; the runtime host always accepted
31
+ * them, and since the seat is typed by us, the recipe is pinned by a row instead of by luck.
32
+ */
33
+ function tooltipPropsFor(label) {
34
+ return {
35
+ label,
36
+ side: "bottom",
37
+ portal: true,
38
+ delayMs: 500
39
+ };
40
+ }
41
+ let resolved;
42
+ function resolveTooltip() {
43
+ if (resolved !== void 0) return resolved;
44
+ resolved = null;
45
+ try {
46
+ if (typeof require === "function") {
47
+ const T = require("@deepseek-ai/dsh-client-ui-primitives")?.Tooltip;
48
+ if (typeof T === "function" || typeof T === "object") resolved = T;
49
+ }
50
+ } catch {
51
+ resolved = null;
52
+ }
53
+ return resolved;
54
+ }
55
+ /** Wrap in the host Tooltip when it exists; otherwise return the child (which must carry `title`). */
56
+ function withTooltip(label, child) {
57
+ const T = resolveTooltip();
58
+ return T !== null ? (0, react.createElement)(T, {
59
+ ...tooltipPropsFor(label),
60
+ children: child
61
+ }) : child;
62
+ }
63
+ /** The native-tooltip attribute, present ONLY when the host seat is absent — no double tooltips. */
64
+ function nativeTitle(label) {
65
+ return resolveTooltip() === null ? { title: label } : {};
66
+ }
67
+ // src/prompt-keys.ts
68
+ /**
69
+ * Identifiers shared by BOTH planes of the push transport (#194). One module, one spelling: the
70
+ * server registers the projection under this key and the client reads `useProjection(key)` — two
71
+ * literals in two planes would be exactly the drift #59 forbids. No node imports (the client
72
+ * compiles this), no DOM (the server imports this).
73
+ */
74
+ const PROMPT_PROJECTION_KEY = "chaptersPrompt";
9
75
  // src/client/prompt-bar.ts
10
76
  function initialBarState(autorun = "disarmed") {
11
77
  return {
@@ -136,6 +202,7 @@ window.__ModuleLoader__.load({
136
202
  */
137
203
  function mountPullDecision(c) {
138
204
  if (!c.hasCache) return true;
205
+ if (c.status === "off") return false;
139
206
  if (c.autorun === "armed" || c.status === "analyzing") return true;
140
207
  return c.ageMs > QUIET_POLL_MS;
141
208
  }
@@ -250,6 +317,81 @@ window.__ModuleLoader__.load({
250
317
  return deriveRunning(s.status ?? s.state ?? s.phase);
251
318
  }
252
319
  }
320
+ /**
321
+ * #208: the plane-wide relevance fact, read from the `state` block's own model line. `model: unset`
322
+ * means the feature does not exist on this plane — one discovery reply carrying it silences EVERY
323
+ * dock on the page (the pull transport cannot do better than one discovery; the push transport
324
+ * (#194) removes even that). An unrecognised block is false, never a guess.
325
+ */
326
+ function parseModelAbsent(text) {
327
+ return /^\s*model:\s*unset\b/m.test(text);
328
+ }
329
+ /**
330
+ * A pushed payload becomes the SAME events the pull path dispatches — one fold, one reducer, no
331
+ * second implementation to drift (#59). Null when the payload is not ours or not trustworthy: an
332
+ * unrecognised push is ignored, never guessed at, and the pull schedule underneath keeps running
333
+ * until a payload this function accepts actually arrives.
334
+ */
335
+ function pushEventsOf(payload) {
336
+ if (payload === null || payload === void 0 || typeof payload !== "object") return null;
337
+ const p = payload;
338
+ if (p.v !== 1 || typeof p.status !== "string") return null;
339
+ const status = p.status;
340
+ if (status !== "off" && status !== "idle" && status !== "analyzing" && status !== "ready" && status !== "failed") return null;
341
+ return {
342
+ polled: {
343
+ t: "polled",
344
+ status,
345
+ ...status === "ready" ? {
346
+ text: typeof p.text === "string" ? p.text : "",
347
+ turnSeq: p.turnSeq ?? null,
348
+ intent: p.intent ?? "worker-step"
349
+ } : {},
350
+ ...status === "failed" ? { error: p.error ?? "the suggestion call failed" } : {}
351
+ },
352
+ autorun: {
353
+ t: "autorun",
354
+ on: p.autorun === "armed"
355
+ }
356
+ };
357
+ }
358
+ /**
359
+ * The running signal's resolution order (scalar prop → `useSession` snapshot via `deriveRunning` →
360
+ * unknown) lives in the dock as `props.running ?? deriveRunning(sessionSnapshot)` — ONE expression,
361
+ * pinned by a source-scan row, folded by the deriveRunning rows that already cover every shape.
362
+ * A second pure helper for the same decision would be exactly the drift #59 forbids; the draft of
363
+ * #194 shipped one and this comment is its headstone.
364
+ */
365
+ /** The staleness-confirm's one question: did a push arrive after the edge that scheduled it? */
366
+ function pushIsFresher(lastPushAt, edgeAt) {
367
+ return lastPushAt !== null && lastPushAt > edgeAt;
368
+ }
369
+ /**
370
+ * #218: a DEAD-PUSH HEARTBEAT, not a staleness confirm — 20s fired mid-turn before the turn's
371
+ * record could exist (turns outlast 20s), which was confirm spam wearing a safety hat. 5 minutes:
372
+ * during normal work a record arrives every turn and the push effect retires the pending timer, so
373
+ * a healthy push NEVER fires it; only a push silent for a whole 5 minutes pulls once — a frozen
374
+ * bar is the failure this bounds, and it is worth one node every 5 minutes, not one every 20
375
+ * seconds.
376
+ * within moments (the runner writes it BEFORE calling the model). If no push arrived this long
377
+ * after the edge, the push is presumed dead and ONE confirm pull runs — a silently dead push
378
+ * degrades to one pull per turn, never to a frozen bar.
379
+ */
380
+ const PUSH_CONFIRM_GRACE_MS = 3e5;
381
+ /**
382
+ * #211 — the edge kick's delay, measured on the maintainer's host the hard way: some hosts publish
383
+ * `running` at STEP granularity (it flickers at every tool call and thinking block), and an
384
+ * immediate kick per falling edge was a rendered `chapters-prompt` node per tool call — all waste,
385
+ * because the server row changes ONLY at turn boundaries. So: a falling edge schedules its pull
386
+ * after the debounce, a rising edge cancels it (step flicker coalesces to zero pulls), and the rate
387
+ * floor bounds even a host that holds `running` low through long tool calls. A real turn end — a
388
+ * falling edge with no rising edge after it — pulls exactly once.
389
+ */
390
+ const EDGE_DEBOUNCE_MS = 2e3;
391
+ const EDGE_FLOOR_MS = 15e3;
392
+ function edgeKickDelayMs(msSinceLastPull) {
393
+ return Math.max(EDGE_DEBOUNCE_MS, EDGE_FLOOR_MS - msSinceLastPull);
394
+ }
253
395
  // src/client/prompt-dock.ts
254
396
  /**
255
397
  * The Suggested Next Step dock row (#113) — thin by design.
@@ -273,7 +415,7 @@ window.__ModuleLoader__.load({
273
415
  * @module dsh-chapters/src/client/prompt-dock
274
416
  */
275
417
  const CSS$1 = [
276
- ".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}",
418
+ ".dsh-chapters_promptDock{box-sizing:border-box;width:calc(100% - 2 * var(--dsh-composer-side-clearance) - 4 * var(--dsh-composer-dock-inset));max-width:calc(var(--dsh-composer-card-max-width) - 4 * var(--dsh-composer-dock-inset));margin:0 auto}",
277
419
  ".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}",
278
420
  ".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}",
279
421
  ".dsh-chapters_promptExpanded::before{align-items:flex-start;position:absolute;inset:0}",
@@ -326,14 +468,10 @@ window.__ModuleLoader__.load({
326
468
  className: "dsh-chapters_promptBtn",
327
469
  "aria-label": props.label,
328
470
  ...props.disabled === true ? { disabled: true } : {},
329
- ...props.title !== void 0 ? { title: props.title } : {},
471
+ ...props.title !== void 0 ? { title: props.title } : nativeTitle(props.label),
330
472
  onClick: props.onClick
331
473
  }, props.glyph);
332
- return (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
333
- label: props.label,
334
- side: "bottom",
335
- children: button
336
- });
474
+ return withTooltip(props.label, button);
337
475
  };
338
476
  /** Run is allowed whenever there is an offer; AUTORUN is what the intent and the goal gate. Keeping the
339
477
  * human's own button enabled is deliberate — the point of the protocol is to route a goal edit to a keystroke,
@@ -354,6 +492,13 @@ window.__ModuleLoader__.load({
354
492
  * answers in milliseconds when the session is attachable, and the failure it guards against is a hang. */
355
493
  const PULL_TIMEOUT_MS = 15e3;
356
494
  const stateCache = /* @__PURE__ */ new Map();
495
+ /**
496
+ * #208: set by the first reply reporting the plane-wide `model: unset` — the feature does not
497
+ * exist here, so NO dock on this page pulls again for anything: not mounts, not idle edges, not
498
+ * timers, not visibility. One discovery per page load is the pull transport's floor; a page reload
499
+ * is the only revival, and the enabling commands say so.
500
+ */
501
+ let featureAbsent = false;
357
502
  /** Retry ceiling for the FIRST pull only, in attempts and spacing. A mount-time race must not be able to
358
503
  * starve the bar forever, and a real `off` answer stops the loop on the first reply either way. */
359
504
  function execWithTimeout(exec, line, ms) {
@@ -372,11 +517,19 @@ window.__ModuleLoader__.load({
372
517
  }
373
518
  function PromptDock(props) {
374
519
  const { sessionId, inputActions, exec } = props;
520
+ const sessionSnapshot = (props.useSession ?? (() => void 0))((snapshot) => snapshot);
521
+ const running = props.running ?? deriveRunning(sessionSnapshot);
522
+ const pushed = (props.useProjection ?? (() => void 0))(PROMPT_PROJECTION_KEY);
375
523
  const [state, dispatch] = (0, react.useReducer)(promptBarReducer, initialBarState(), (s) => {
524
+ const pushEvs = pushEventsOf(pushed);
525
+ if (pushEvs !== null) return {
526
+ ...promptBarReducer(promptBarReducer(s, pushEvs.polled), pushEvs.autorun),
527
+ running: running ?? true
528
+ };
376
529
  const c = sessionId !== void 0 ? stateCache.get(sessionId) : void 0;
377
530
  return {
378
531
  ...c !== void 0 ? stateFromCache(c.ev, c.autorun) : s,
379
- running: props.running ?? true
532
+ running: running ?? true
380
533
  };
381
534
  });
382
535
  const pulling = (0, react.useRef)(false);
@@ -388,6 +541,10 @@ window.__ModuleLoader__.load({
388
541
  }, []);
389
542
  const [busy, setBusy] = (0, react.useState)(false);
390
543
  const kick = (0, react.useRef)(void 0);
544
+ const lastPullAt = (0, react.useRef)(null);
545
+ const edgeTimer = (0, react.useRef)(void 0);
546
+ const pushLiveRef = (0, react.useRef)(false);
547
+ const lastPushAt = (0, react.useRef)(null);
391
548
  const answered = (0, react.useRef)(false);
392
549
  const replyStatus = (0, react.useRef)("unknown");
393
550
  const stateRef = (0, react.useRef)(initialBarState());
@@ -401,6 +558,7 @@ window.__ModuleLoader__.load({
401
558
  const refresh = (0, react.useCallback)(async () => {
402
559
  if (exec === void 0 || pulling.current) return;
403
560
  pulling.current = true;
561
+ lastPullAt.current = Date.now();
404
562
  try {
405
563
  const text = await execWithTimeout(exec, BAR_STATE_COMMAND, PULL_TIMEOUT_MS);
406
564
  try {
@@ -417,6 +575,7 @@ window.__ModuleLoader__.load({
417
575
  t: "autorun",
418
576
  on: autorun === "armed"
419
577
  });
578
+ if (parseModelAbsent(text)) featureAbsent = true;
420
579
  if (ev !== null && sessionId !== void 0) stateCache.set(sessionId, {
421
580
  ev,
422
581
  autorun: autorun === "armed" || autorun === "disarmed" ? autorun : null,
@@ -428,6 +587,18 @@ window.__ModuleLoader__.load({
428
587
  pulling.current = false;
429
588
  }
430
589
  }, [exec]);
590
+ (0, react.useEffect)(() => {
591
+ const evs = pushEventsOf(pushed);
592
+ if (evs === null) return;
593
+ pushLiveRef.current = true;
594
+ lastPushAt.current = Date.now();
595
+ dispatch(evs.polled);
596
+ dispatch(evs.autorun);
597
+ if (edgeTimer.current !== void 0) {
598
+ clearTimeout(edgeTimer.current);
599
+ edgeTimer.current = void 0;
600
+ }
601
+ }, [pushed]);
431
602
  (0, react.useEffect)(() => {
432
603
  ensureCss();
433
604
  if (pulled.current) return;
@@ -451,6 +622,8 @@ window.__ModuleLoader__.load({
451
622
  });
452
623
  };
453
624
  const tick = async (opts) => {
625
+ if (pushLiveRef.current && opts?.force !== true) return;
626
+ if (featureAbsent) return;
454
627
  if (effectiveStatus() === "off") {
455
628
  if (!(opts?.mount === true && duePull())) return;
456
629
  }
@@ -473,21 +646,40 @@ window.__ModuleLoader__.load({
473
646
  tick({ mount: true });
474
647
  return () => {
475
648
  if (timer !== void 0) clearTimeout(timer);
649
+ if (edgeTimer.current !== void 0) {
650
+ clearTimeout(edgeTimer.current);
651
+ edgeTimer.current = void 0;
652
+ }
476
653
  kick.current = void 0;
477
654
  if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
478
655
  };
479
656
  }, [refresh]);
480
- const prevRunning = (0, react.useRef)(props.running ?? true);
657
+ const prevRunning = (0, react.useRef)(running ?? true);
481
658
  (0, react.useEffect)(() => {
482
- const next = props.running ?? true;
659
+ const next = running ?? true;
483
660
  const prev = prevRunning.current;
484
661
  prevRunning.current = next;
485
662
  dispatch({
486
663
  t: "running",
487
664
  running: next
488
665
  });
489
- if (prev && !next) kick.current?.();
490
- }, [props.running]);
666
+ if (prev && !next) {
667
+ if (edgeTimer.current !== void 0) clearTimeout(edgeTimer.current);
668
+ const since = lastPullAt.current === null ? Number.MAX_SAFE_INTEGER : Date.now() - lastPullAt.current;
669
+ const delay = pushLiveRef.current ? Math.max(PUSH_CONFIRM_GRACE_MS, edgeKickDelayMs(since)) : edgeKickDelayMs(since);
670
+ const edgeAt = Date.now();
671
+ edgeTimer.current = setTimeout(() => {
672
+ edgeTimer.current = void 0;
673
+ if (pushLiveRef.current && pushIsFresher(lastPushAt.current, edgeAt)) return;
674
+ kick.current?.();
675
+ }, delay);
676
+ } else if (!prev && next) {
677
+ if (edgeTimer.current !== void 0) {
678
+ clearTimeout(edgeTimer.current);
679
+ edgeTimer.current = void 0;
680
+ }
681
+ }
682
+ }, [running]);
491
683
  const run = (0, react.useCallback)(() => {
492
684
  if (state.text === null || state.text === "" || exec === void 0) return;
493
685
  setBusy(true);
@@ -515,8 +707,7 @@ window.__ModuleLoader__.load({
515
707
  if (!shouldAutoFire(state)) return;
516
708
  run();
517
709
  }, [state, run]);
518
- const sessionSnapshot = (props.useSession ?? (() => void 0))((snapshot) => snapshot);
519
- const autorunDisabled = (props.running ?? deriveRunning(sessionSnapshot)) === void 0;
710
+ const autorunDisabled = running === void 0;
520
711
  if (sessionId === void 0 || !barVisible(state)) return null;
521
712
  const populate = () => {
522
713
  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)}`);
@@ -600,7 +791,7 @@ window.__ModuleLoader__.load({
600
791
  t: "autorun",
601
792
  on: arm
602
793
  });
603
- await refresh();
794
+ if (!pushLiveRef.current) await refresh();
604
795
  })();
605
796
  }
606
797
  }), (0, react.createElement)(Action, {
@@ -995,6 +1186,7 @@ window.__ModuleLoader__.load({
995
1186
  const button = (0, react.createElement)("button", {
996
1187
  type: "button",
997
1188
  "aria-label": door.label,
1189
+ ...nativeTitle(door.label),
998
1190
  disabled: busy,
999
1191
  onClick,
1000
1192
  className: "dsh-chapters_forkAction"
@@ -1009,11 +1201,7 @@ window.__ModuleLoader__.load({
1009
1201
  display: "inline-flex",
1010
1202
  alignItems: "center",
1011
1203
  gap: "4px"
1012
- } }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1013
- label: door.label,
1014
- side: "bottom",
1015
- children: button
1016
- }), note !== null ? (0, react.createElement)("span", { className: "dsh-chapters_forkNote" }, note) : null);
1204
+ } }, withTooltip(door.label, button), note !== null ? (0, react.createElement)("span", { className: "dsh-chapters_forkNote" }, note) : null);
1017
1205
  };
1018
1206
  }
1019
1207
  function apply(ctx) {
@@ -1052,12 +1240,18 @@ window.__ModuleLoader__.load({
1052
1240
  }, { ...uiWorkspace !== void 0 ? { navigator: uiWorkspace } : {} }),
1053
1241
  component: makeDoorAction(door)
1054
1242
  }));
1055
- slots.inject("conversation.chat.assistant-actions", () => rows.map(({ door, run, component }) => slots.register({
1056
- name: "conversation.chat.assistant-actions",
1057
- id: door.id,
1058
- order: door.order,
1059
- inject: (sessionId) => ({ run: () => run(sessionId) })
1060
- }, component)));
1243
+ console.info(`[client:chapters] injecting ${String(rows.length)} assistant-actions door(s)`);
1244
+ const disposeDoors = slots.inject("conversation.chat.assistant-actions", () => {
1245
+ const effects = rows.map(({ door, run, component }) => slots.register({
1246
+ name: "conversation.chat.assistant-actions",
1247
+ id: door.id,
1248
+ label: door.label,
1249
+ inject: (sessionId) => ({ run: () => run(sessionId) })
1250
+ }, component));
1251
+ console.info(`[client:chapters] door factory ran: ${String(effects.length)} register effect(s), types: ${effects.map((e) => typeof e).join(",")}`);
1252
+ return effects;
1253
+ });
1254
+ console.info(`[client:chapters] assistant-actions inject returned ${typeof disposeDoors}`);
1061
1255
  slots.inject("conversation.input.dock", () => slots.register({
1062
1256
  name: "conversation.input.dock",
1063
1257
  id: "chapters-prompt",