@treeseed/dsh-chapters 0.2.8 → 0.2.11
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 +40 -0
- package/lib/client.js +154 -24
- package/lib/client.js.map +1 -1
- package/lib/commands.js +79 -2
- package/lib/commands.js.map +1 -1
- package/lib/index.js +114 -6
- package/lib/index.js.map +1 -1
- package/lib/prompt-keys.d.ts +7 -0
- package/lib/prompt-keys.js +8 -0
- package/lib/prompt-keys.js.map +1 -0
- package/lib/prompt-push.d.ts +47 -0
- package/lib/prompt-push.js +115 -0
- package/lib/prompt-push.js.map +1 -0
- package/lib/prompt-runner.d.ts +4 -0
- package/lib/prompt-runner.js +5 -0
- package/lib/prompt-runner.js.map +1 -1
- package/lib/prompt-state.d.ts +10 -5
- package/lib/prompt.d.ts +9 -0
- package/lib/prompt.js.map +1 -1
- package/lib/tools.js +31 -5
- package/lib/tools.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/package.json +1 -1
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
|
@@ -64,6 +64,14 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
function nativeTitle(label) {
|
|
65
65
|
return resolveTooltip() === null ? { title: label } : {};
|
|
66
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";
|
|
67
75
|
// src/client/prompt-bar.ts
|
|
68
76
|
function initialBarState(autorun = "disarmed") {
|
|
69
77
|
return {
|
|
@@ -176,11 +184,12 @@ window.__ModuleLoader__.load({
|
|
|
176
184
|
const ACTIVE_POLL_MS = 1e4;
|
|
177
185
|
const QUIET_POLL_MS = 6e4;
|
|
178
186
|
function pollSpacingFor(s) {
|
|
187
|
+
if (s.turnEndPulse === true) return ACTIVE_POLL_MS;
|
|
179
188
|
if (s.edges) {
|
|
180
189
|
if (s.status === "analyzing" || s.autorun === "armed") return ACTIVE_POLL_MS;
|
|
181
190
|
return null;
|
|
182
191
|
}
|
|
183
|
-
const active = s.status === "analyzing" || s.autorun === "armed"
|
|
192
|
+
const active = s.status === "analyzing" || s.autorun === "armed";
|
|
184
193
|
if (!active && s.hidden) return null;
|
|
185
194
|
return active ? ACTIVE_POLL_MS : QUIET_POLL_MS;
|
|
186
195
|
}
|
|
@@ -319,6 +328,58 @@ window.__ModuleLoader__.load({
|
|
|
319
328
|
return /^\s*model:\s*unset\b/m.test(text);
|
|
320
329
|
}
|
|
321
330
|
/**
|
|
331
|
+
* A pushed payload becomes the SAME events the pull path dispatches — one fold, one reducer, no
|
|
332
|
+
* second implementation to drift (#59). Null when the payload is not ours or not trustworthy: an
|
|
333
|
+
* unrecognised push is ignored, never guessed at, and the pull schedule underneath keeps running
|
|
334
|
+
* until a payload this function accepts actually arrives.
|
|
335
|
+
*/
|
|
336
|
+
function pushEventsOf(payload) {
|
|
337
|
+
if (payload === null || payload === void 0 || typeof payload !== "object") return null;
|
|
338
|
+
const p = payload;
|
|
339
|
+
if (p.v !== 1 || typeof p.status !== "string") return null;
|
|
340
|
+
const status = p.status;
|
|
341
|
+
if (status !== "off" && status !== "idle" && status !== "analyzing" && status !== "ready" && status !== "failed") return null;
|
|
342
|
+
return {
|
|
343
|
+
polled: {
|
|
344
|
+
t: "polled",
|
|
345
|
+
status,
|
|
346
|
+
...status === "ready" ? {
|
|
347
|
+
text: typeof p.text === "string" ? p.text : "",
|
|
348
|
+
turnSeq: p.turnSeq ?? null,
|
|
349
|
+
intent: p.intent ?? "worker-step"
|
|
350
|
+
} : {},
|
|
351
|
+
...status === "failed" ? { error: p.error ?? "the suggestion call failed" } : {}
|
|
352
|
+
},
|
|
353
|
+
autorun: {
|
|
354
|
+
t: "autorun",
|
|
355
|
+
on: p.autorun === "armed"
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The running signal's resolution order (scalar prop → `useSession` snapshot via `deriveRunning` →
|
|
361
|
+
* unknown) lives in the dock as `props.running ?? deriveRunning(sessionSnapshot)` — ONE expression,
|
|
362
|
+
* pinned by a source-scan row, folded by the deriveRunning rows that already cover every shape.
|
|
363
|
+
* A second pure helper for the same decision would be exactly the drift #59 forbids; the draft of
|
|
364
|
+
* #194 shipped one and this comment is its headstone.
|
|
365
|
+
*/
|
|
366
|
+
/** The staleness-confirm's one question: did a push arrive after the edge that scheduled it? */
|
|
367
|
+
function pushIsFresher(lastPushAt, edgeAt) {
|
|
368
|
+
return lastPushAt !== null && lastPushAt > edgeAt;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* #218: a DEAD-PUSH HEARTBEAT, not a staleness confirm — 20s fired mid-turn before the turn's
|
|
372
|
+
* record could exist (turns outlast 20s), which was confirm spam wearing a safety hat. 5 minutes:
|
|
373
|
+
* during normal work a record arrives every turn and the push effect retires the pending timer, so
|
|
374
|
+
* a healthy push NEVER fires it; only a push silent for a whole 5 minutes pulls once — a frozen
|
|
375
|
+
* bar is the failure this bounds, and it is worth one node every 5 minutes, not one every 20
|
|
376
|
+
* seconds.
|
|
377
|
+
* within moments (the runner writes it BEFORE calling the model). If no push arrived this long
|
|
378
|
+
* after the edge, the push is presumed dead and ONE confirm pull runs — a silently dead push
|
|
379
|
+
* degrades to one pull per turn, never to a frozen bar.
|
|
380
|
+
*/
|
|
381
|
+
const PUSH_CONFIRM_GRACE_MS = 3e5;
|
|
382
|
+
/**
|
|
322
383
|
* #211 — the edge kick's delay, measured on the maintainer's host the hard way: some hosts publish
|
|
323
384
|
* `running` at STEP granularity (it flickers at every tool call and thinking block), and an
|
|
324
385
|
* immediate kick per falling edge was a rendered `chapters-prompt` node per tool call — all waste,
|
|
@@ -332,6 +393,26 @@ window.__ModuleLoader__.load({
|
|
|
332
393
|
function edgeKickDelayMs(msSinceLastPull) {
|
|
333
394
|
return Math.max(EDGE_DEBOUNCE_MS, EDGE_FLOOR_MS - msSinceLastPull);
|
|
334
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* #221: the turn-end pulse, derived from the host's own turn-boundary projection — no plugin
|
|
398
|
+
* record API needed, so it works on 0.2.0 hosts (where appendPluginRecord is absent) exactly as
|
|
399
|
+
* on newer ones. The host maintains a `turnBoundary` projection (the agent loop reads its
|
|
400
|
+
* lastTurn for its own bookkeeping); reading what the host already folds costs zero writes and
|
|
401
|
+
* delivers reactively through the same standard useProjection seat as any other projection.
|
|
402
|
+
* Returns true when a NEW turn ended since the last observation; the dock calls it once per
|
|
403
|
+
* render and stores the returned stamp back into `seen`.
|
|
404
|
+
*/
|
|
405
|
+
function turnEndPulse(lastTurn, seen) {
|
|
406
|
+
const cur = typeof lastTurn === "number" && Number.isFinite(lastTurn) ? lastTurn : 0;
|
|
407
|
+
if (cur > seen) return {
|
|
408
|
+
pulse: true,
|
|
409
|
+
seen: cur
|
|
410
|
+
};
|
|
411
|
+
return {
|
|
412
|
+
pulse: false,
|
|
413
|
+
seen
|
|
414
|
+
};
|
|
415
|
+
}
|
|
335
416
|
// src/client/prompt-dock.ts
|
|
336
417
|
/**
|
|
337
418
|
* The Suggested Next Step dock row (#113) — thin by design.
|
|
@@ -457,11 +538,21 @@ window.__ModuleLoader__.load({
|
|
|
457
538
|
}
|
|
458
539
|
function PromptDock(props) {
|
|
459
540
|
const { sessionId, inputActions, exec } = props;
|
|
541
|
+
const sessionSnapshot = (props.useSession ?? (() => void 0))((snapshot) => snapshot);
|
|
542
|
+
const running = props.running ?? deriveRunning(sessionSnapshot);
|
|
543
|
+
const useProjectionHook = props.useProjection ?? (() => void 0);
|
|
544
|
+
const pushed = useProjectionHook(PROMPT_PROJECTION_KEY);
|
|
545
|
+
const turnBoundary = useProjectionHook("turnBoundary");
|
|
460
546
|
const [state, dispatch] = (0, react.useReducer)(promptBarReducer, initialBarState(), (s) => {
|
|
547
|
+
const pushEvs = pushEventsOf(pushed);
|
|
548
|
+
if (pushEvs !== null) return {
|
|
549
|
+
...promptBarReducer(promptBarReducer(s, pushEvs.polled), pushEvs.autorun),
|
|
550
|
+
running: running ?? true
|
|
551
|
+
};
|
|
461
552
|
const c = sessionId !== void 0 ? stateCache.get(sessionId) : void 0;
|
|
462
553
|
return {
|
|
463
554
|
...c !== void 0 ? stateFromCache(c.ev, c.autorun) : s,
|
|
464
|
-
running:
|
|
555
|
+
running: running ?? true
|
|
465
556
|
};
|
|
466
557
|
});
|
|
467
558
|
const pulling = (0, react.useRef)(false);
|
|
@@ -475,6 +566,9 @@ window.__ModuleLoader__.load({
|
|
|
475
566
|
const kick = (0, react.useRef)(void 0);
|
|
476
567
|
const lastPullAt = (0, react.useRef)(null);
|
|
477
568
|
const edgeTimer = (0, react.useRef)(void 0);
|
|
569
|
+
const pushLiveRef = (0, react.useRef)(false);
|
|
570
|
+
const seenTurnRef = (0, react.useRef)(0);
|
|
571
|
+
const lastPushAt = (0, react.useRef)(null);
|
|
478
572
|
const answered = (0, react.useRef)(false);
|
|
479
573
|
const replyStatus = (0, react.useRef)("unknown");
|
|
480
574
|
const stateRef = (0, react.useRef)(initialBarState());
|
|
@@ -517,19 +611,36 @@ window.__ModuleLoader__.load({
|
|
|
517
611
|
pulling.current = false;
|
|
518
612
|
}
|
|
519
613
|
}, [exec]);
|
|
614
|
+
(0, react.useEffect)(() => {
|
|
615
|
+
const evs = pushEventsOf(pushed);
|
|
616
|
+
if (evs === null) return;
|
|
617
|
+
pushLiveRef.current = true;
|
|
618
|
+
lastPushAt.current = Date.now();
|
|
619
|
+
dispatch(evs.polled);
|
|
620
|
+
dispatch(evs.autorun);
|
|
621
|
+
if (edgeTimer.current !== void 0) {
|
|
622
|
+
clearTimeout(edgeTimer.current);
|
|
623
|
+
edgeTimer.current = void 0;
|
|
624
|
+
}
|
|
625
|
+
}, [pushed]);
|
|
520
626
|
(0, react.useEffect)(() => {
|
|
521
627
|
ensureCss();
|
|
522
628
|
if (pulled.current) return;
|
|
523
629
|
pulled.current = true;
|
|
524
630
|
let timer;
|
|
525
631
|
const effectiveStatus = () => replyStatus.current === "unknown" ? stateRef.current.status : replyStatus.current;
|
|
526
|
-
const spacingNow = () =>
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
632
|
+
const spacingNow = () => {
|
|
633
|
+
const pe = turnEndPulse(turnBoundary?.lastTurn, seenTurnRef.current);
|
|
634
|
+
if (pe.pulse) seenTurnRef.current = pe.seen;
|
|
635
|
+
return pollSpacingFor({
|
|
636
|
+
status: effectiveStatus(),
|
|
637
|
+
autorun: stateRef.current.autorun,
|
|
638
|
+
running: stateRef.current.running,
|
|
639
|
+
hidden: typeof document !== "undefined" && document.hidden === true,
|
|
640
|
+
edges: edgesRef.current,
|
|
641
|
+
turnEndPulse: pe.pulse
|
|
642
|
+
});
|
|
643
|
+
};
|
|
533
644
|
const duePull = () => {
|
|
534
645
|
const c = sessionId !== void 0 ? stateCache.get(sessionId) : void 0;
|
|
535
646
|
return mountPullDecision({
|
|
@@ -540,6 +651,7 @@ window.__ModuleLoader__.load({
|
|
|
540
651
|
});
|
|
541
652
|
};
|
|
542
653
|
const tick = async (opts) => {
|
|
654
|
+
if (pushLiveRef.current && opts?.force !== true) return;
|
|
543
655
|
if (featureAbsent) return;
|
|
544
656
|
if (effectiveStatus() === "off") {
|
|
545
657
|
if (!(opts?.mount === true && duePull())) return;
|
|
@@ -571,9 +683,9 @@ window.__ModuleLoader__.load({
|
|
|
571
683
|
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
|
|
572
684
|
};
|
|
573
685
|
}, [refresh]);
|
|
574
|
-
const prevRunning = (0, react.useRef)(
|
|
686
|
+
const prevRunning = (0, react.useRef)(running ?? true);
|
|
575
687
|
(0, react.useEffect)(() => {
|
|
576
|
-
const next =
|
|
688
|
+
const next = running ?? true;
|
|
577
689
|
const prev = prevRunning.current;
|
|
578
690
|
prevRunning.current = next;
|
|
579
691
|
dispatch({
|
|
@@ -583,17 +695,20 @@ window.__ModuleLoader__.load({
|
|
|
583
695
|
if (prev && !next) {
|
|
584
696
|
if (edgeTimer.current !== void 0) clearTimeout(edgeTimer.current);
|
|
585
697
|
const since = lastPullAt.current === null ? Number.MAX_SAFE_INTEGER : Date.now() - lastPullAt.current;
|
|
698
|
+
const delay = pushLiveRef.current ? Math.max(PUSH_CONFIRM_GRACE_MS, edgeKickDelayMs(since)) : edgeKickDelayMs(since);
|
|
699
|
+
const edgeAt = Date.now();
|
|
586
700
|
edgeTimer.current = setTimeout(() => {
|
|
587
701
|
edgeTimer.current = void 0;
|
|
702
|
+
if (pushLiveRef.current && pushIsFresher(lastPushAt.current, edgeAt)) return;
|
|
588
703
|
kick.current?.();
|
|
589
|
-
},
|
|
704
|
+
}, delay);
|
|
590
705
|
} else if (!prev && next) {
|
|
591
706
|
if (edgeTimer.current !== void 0) {
|
|
592
707
|
clearTimeout(edgeTimer.current);
|
|
593
708
|
edgeTimer.current = void 0;
|
|
594
709
|
}
|
|
595
710
|
}
|
|
596
|
-
}, [
|
|
711
|
+
}, [running]);
|
|
597
712
|
const run = (0, react.useCallback)(() => {
|
|
598
713
|
if (state.text === null || state.text === "" || exec === void 0) return;
|
|
599
714
|
setBusy(true);
|
|
@@ -621,8 +736,7 @@ window.__ModuleLoader__.load({
|
|
|
621
736
|
if (!shouldAutoFire(state)) return;
|
|
622
737
|
run();
|
|
623
738
|
}, [state, run]);
|
|
624
|
-
const
|
|
625
|
-
const autorunDisabled = (props.running ?? deriveRunning(sessionSnapshot)) === void 0;
|
|
739
|
+
const autorunDisabled = running === void 0;
|
|
626
740
|
if (sessionId === void 0 || !barVisible(state)) return null;
|
|
627
741
|
const populate = () => {
|
|
628
742
|
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)}`);
|
|
@@ -706,7 +820,7 @@ window.__ModuleLoader__.load({
|
|
|
706
820
|
t: "autorun",
|
|
707
821
|
on: arm
|
|
708
822
|
});
|
|
709
|
-
await refresh();
|
|
823
|
+
if (!pushLiveRef.current) await refresh();
|
|
710
824
|
})();
|
|
711
825
|
}
|
|
712
826
|
}), (0, react.createElement)(Action, {
|
|
@@ -912,13 +1026,14 @@ window.__ModuleLoader__.load({
|
|
|
912
1026
|
return tryClick();
|
|
913
1027
|
}
|
|
914
1028
|
function makeDoorRunner(remote, sessions, door, verifySwitched, opts) {
|
|
915
|
-
return async function run(sessionId) {
|
|
1029
|
+
return async function run(sessionId, anchorSeq) {
|
|
916
1030
|
try {
|
|
917
1031
|
if (remote.commands?.execute === void 0) return {
|
|
918
1032
|
ok: false,
|
|
919
1033
|
message: "commands remote unavailable"
|
|
920
1034
|
};
|
|
921
|
-
const
|
|
1035
|
+
const line = typeof anchorSeq === "number" && Number.isFinite(anchorSeq) ? `${door.command} ${String(Math.trunc(anchorSeq))}` : door.command;
|
|
1036
|
+
const res = await remote.commands.execute(sessionId, line, []);
|
|
922
1037
|
if (res.ok !== true) return {
|
|
923
1038
|
ok: false,
|
|
924
1039
|
message: (res.error?.message ?? `the ${door.noun} call failed`).slice(0, 90)
|
|
@@ -1155,12 +1270,27 @@ window.__ModuleLoader__.load({
|
|
|
1155
1270
|
}, { ...uiWorkspace !== void 0 ? { navigator: uiWorkspace } : {} }),
|
|
1156
1271
|
component: makeDoorAction(door)
|
|
1157
1272
|
}));
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1273
|
+
console.info(`[client:chapters] injecting ${String(rows.length)} assistant-actions door(s)`);
|
|
1274
|
+
const disposeDoors = slots.inject("conversation.chat.assistant-actions", () => {
|
|
1275
|
+
const effects = rows.map(({ door, run, component }) => slots.register({
|
|
1276
|
+
name: "conversation.chat.assistant-actions",
|
|
1277
|
+
id: door.id,
|
|
1278
|
+
label: door.label,
|
|
1279
|
+
inject: (sessionId, owner) => ({ run: async () => {
|
|
1280
|
+
let anchor = null;
|
|
1281
|
+
const mid = typeof owner?.messageId === "string" ? owner.messageId : "";
|
|
1282
|
+
if (mid !== "") try {
|
|
1283
|
+
const out = await runCommandText(remote, sessionId, `/chapters-anchor ${mid}`);
|
|
1284
|
+
const n = Number.parseInt(out.trim(), 10);
|
|
1285
|
+
if (Number.isFinite(n) && n > 0) anchor = n;
|
|
1286
|
+
} catch {}
|
|
1287
|
+
return run(sessionId, anchor);
|
|
1288
|
+
} })
|
|
1289
|
+
}, component));
|
|
1290
|
+
console.info(`[client:chapters] door factory ran: ${String(effects.length)} register effect(s), types: ${effects.map((e) => typeof e).join(",")}`);
|
|
1291
|
+
return effects;
|
|
1292
|
+
});
|
|
1293
|
+
console.info(`[client:chapters] assistant-actions inject returned ${typeof disposeDoors}`);
|
|
1164
1294
|
slots.inject("conversation.input.dock", () => slots.register({
|
|
1165
1295
|
name: "conversation.input.dock",
|
|
1166
1296
|
id: "chapters-prompt",
|