@treeseed/dsh-chapters 0.2.8 → 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 +40 -0
- package/lib/client.js +105 -14
- package/lib/client.js.map +1 -1
- package/lib/commands.js +24 -2
- package/lib/commands.js.map +1 -1
- package/lib/index.js +78 -5
- 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/version.d.ts +1 -1
- package/lib/version.js +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 {
|
|
@@ -319,6 +327,58 @@ window.__ModuleLoader__.load({
|
|
|
319
327
|
return /^\s*model:\s*unset\b/m.test(text);
|
|
320
328
|
}
|
|
321
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
|
+
/**
|
|
322
382
|
* #211 — the edge kick's delay, measured on the maintainer's host the hard way: some hosts publish
|
|
323
383
|
* `running` at STEP granularity (it flickers at every tool call and thinking block), and an
|
|
324
384
|
* immediate kick per falling edge was a rendered `chapters-prompt` node per tool call — all waste,
|
|
@@ -457,11 +517,19 @@ window.__ModuleLoader__.load({
|
|
|
457
517
|
}
|
|
458
518
|
function PromptDock(props) {
|
|
459
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);
|
|
460
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
|
+
};
|
|
461
529
|
const c = sessionId !== void 0 ? stateCache.get(sessionId) : void 0;
|
|
462
530
|
return {
|
|
463
531
|
...c !== void 0 ? stateFromCache(c.ev, c.autorun) : s,
|
|
464
|
-
running:
|
|
532
|
+
running: running ?? true
|
|
465
533
|
};
|
|
466
534
|
});
|
|
467
535
|
const pulling = (0, react.useRef)(false);
|
|
@@ -475,6 +543,8 @@ window.__ModuleLoader__.load({
|
|
|
475
543
|
const kick = (0, react.useRef)(void 0);
|
|
476
544
|
const lastPullAt = (0, react.useRef)(null);
|
|
477
545
|
const edgeTimer = (0, react.useRef)(void 0);
|
|
546
|
+
const pushLiveRef = (0, react.useRef)(false);
|
|
547
|
+
const lastPushAt = (0, react.useRef)(null);
|
|
478
548
|
const answered = (0, react.useRef)(false);
|
|
479
549
|
const replyStatus = (0, react.useRef)("unknown");
|
|
480
550
|
const stateRef = (0, react.useRef)(initialBarState());
|
|
@@ -517,6 +587,18 @@ window.__ModuleLoader__.load({
|
|
|
517
587
|
pulling.current = false;
|
|
518
588
|
}
|
|
519
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]);
|
|
520
602
|
(0, react.useEffect)(() => {
|
|
521
603
|
ensureCss();
|
|
522
604
|
if (pulled.current) return;
|
|
@@ -540,6 +622,7 @@ window.__ModuleLoader__.load({
|
|
|
540
622
|
});
|
|
541
623
|
};
|
|
542
624
|
const tick = async (opts) => {
|
|
625
|
+
if (pushLiveRef.current && opts?.force !== true) return;
|
|
543
626
|
if (featureAbsent) return;
|
|
544
627
|
if (effectiveStatus() === "off") {
|
|
545
628
|
if (!(opts?.mount === true && duePull())) return;
|
|
@@ -571,9 +654,9 @@ window.__ModuleLoader__.load({
|
|
|
571
654
|
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
|
|
572
655
|
};
|
|
573
656
|
}, [refresh]);
|
|
574
|
-
const prevRunning = (0, react.useRef)(
|
|
657
|
+
const prevRunning = (0, react.useRef)(running ?? true);
|
|
575
658
|
(0, react.useEffect)(() => {
|
|
576
|
-
const next =
|
|
659
|
+
const next = running ?? true;
|
|
577
660
|
const prev = prevRunning.current;
|
|
578
661
|
prevRunning.current = next;
|
|
579
662
|
dispatch({
|
|
@@ -583,17 +666,20 @@ window.__ModuleLoader__.load({
|
|
|
583
666
|
if (prev && !next) {
|
|
584
667
|
if (edgeTimer.current !== void 0) clearTimeout(edgeTimer.current);
|
|
585
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();
|
|
586
671
|
edgeTimer.current = setTimeout(() => {
|
|
587
672
|
edgeTimer.current = void 0;
|
|
673
|
+
if (pushLiveRef.current && pushIsFresher(lastPushAt.current, edgeAt)) return;
|
|
588
674
|
kick.current?.();
|
|
589
|
-
},
|
|
675
|
+
}, delay);
|
|
590
676
|
} else if (!prev && next) {
|
|
591
677
|
if (edgeTimer.current !== void 0) {
|
|
592
678
|
clearTimeout(edgeTimer.current);
|
|
593
679
|
edgeTimer.current = void 0;
|
|
594
680
|
}
|
|
595
681
|
}
|
|
596
|
-
}, [
|
|
682
|
+
}, [running]);
|
|
597
683
|
const run = (0, react.useCallback)(() => {
|
|
598
684
|
if (state.text === null || state.text === "" || exec === void 0) return;
|
|
599
685
|
setBusy(true);
|
|
@@ -621,8 +707,7 @@ window.__ModuleLoader__.load({
|
|
|
621
707
|
if (!shouldAutoFire(state)) return;
|
|
622
708
|
run();
|
|
623
709
|
}, [state, run]);
|
|
624
|
-
const
|
|
625
|
-
const autorunDisabled = (props.running ?? deriveRunning(sessionSnapshot)) === void 0;
|
|
710
|
+
const autorunDisabled = running === void 0;
|
|
626
711
|
if (sessionId === void 0 || !barVisible(state)) return null;
|
|
627
712
|
const populate = () => {
|
|
628
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)}`);
|
|
@@ -706,7 +791,7 @@ window.__ModuleLoader__.load({
|
|
|
706
791
|
t: "autorun",
|
|
707
792
|
on: arm
|
|
708
793
|
});
|
|
709
|
-
await refresh();
|
|
794
|
+
if (!pushLiveRef.current) await refresh();
|
|
710
795
|
})();
|
|
711
796
|
}
|
|
712
797
|
}), (0, react.createElement)(Action, {
|
|
@@ -1155,12 +1240,18 @@ window.__ModuleLoader__.load({
|
|
|
1155
1240
|
}, { ...uiWorkspace !== void 0 ? { navigator: uiWorkspace } : {} }),
|
|
1156
1241
|
component: makeDoorAction(door)
|
|
1157
1242
|
}));
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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}`);
|
|
1164
1255
|
slots.inject("conversation.input.dock", () => slots.register({
|
|
1165
1256
|
name: "conversation.input.dock",
|
|
1166
1257
|
id: "chapters-prompt",
|