@treeseed/dsh-chapters 0.2.9 → 0.2.12

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/lib/client.js CHANGED
@@ -182,15 +182,29 @@ window.__ModuleLoader__.load({
182
182
  * autorun and pausing it silently would be the loop dying with nobody told.
183
183
  */
184
184
  const ACTIVE_POLL_MS = 1e4;
185
- const QUIET_POLL_MS = 6e4;
185
+ /**
186
+ * #229: the quiet heartbeat is CONFIG-GATED (`promptQuietHeartbeatSec`) and defaults to ZERO.
187
+ * Measured on the maintainer's live log after 0.2.11 installed correctly (gaps exactly 60.0s): every
188
+ * pull is two durable nodes — a host `command/run` + `command/done` pair — and the user's rule is
189
+ * absolute: never repeated chapters-prompt executions. The sanctioned moments (mount discovery once
190
+ * per page load, the turn-end pulse, visibility return with an untrusted cache, forced confirms)
191
+ * remain; the ambient drumbeat does not exist unless configured. This module is client-safe (no node
192
+ * builtins), so the wiring injects the config value at boot via `configureQuietHeartbeat`.
193
+ */
194
+ let QUIET_POLL_MS = 0;
195
+ function configureQuietHeartbeat(seconds) {
196
+ QUIET_POLL_MS = typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 ? Math.trunc(seconds * 1e3) : 0;
197
+ }
186
198
  function pollSpacingFor(s) {
199
+ if (s.turnEndPulse === true) return ACTIVE_POLL_MS;
187
200
  if (s.edges) {
188
201
  if (s.status === "analyzing" || s.autorun === "armed") return ACTIVE_POLL_MS;
189
202
  return null;
190
203
  }
191
- const active = s.status === "analyzing" || s.autorun === "armed" || s.running;
204
+ const active = s.status === "analyzing" || s.autorun === "armed";
192
205
  if (!active && s.hidden) return null;
193
- return active ? ACTIVE_POLL_MS : QUIET_POLL_MS;
206
+ if (!active) return QUIET_POLL_MS > 0 ? QUIET_POLL_MS : null;
207
+ return ACTIVE_POLL_MS;
194
208
  }
195
209
  /**
196
210
  * #203: whether a freshly mounted bar pulls, or renders its cache and stays silent. The maintainer's
@@ -204,7 +218,7 @@ window.__ModuleLoader__.load({
204
218
  if (!c.hasCache) return true;
205
219
  if (c.status === "off") return false;
206
220
  if (c.autorun === "armed" || c.status === "analyzing") return true;
207
- return c.ageMs > QUIET_POLL_MS;
221
+ return QUIET_POLL_MS > 0 && c.ageMs > QUIET_POLL_MS;
208
222
  }
209
223
  /**
210
224
  * #203: fold a cached wire event back into row state through the SAME reducer the live path uses —
@@ -271,6 +285,7 @@ window.__ModuleLoader__.load({
271
285
  }
272
286
  scalars[key] !== void 0 ? scalars[key] += ` ${value}` : scalars[key] = value;
273
287
  }
288
+ configureQuietHeartbeat(Number.parseInt((scalars["heartbeat"] ?? "").trim(), 10) || 0);
274
289
  const turnRaw = scalars["turn"] ?? "";
275
290
  const turnSeq = /^\d+$/.test(turnRaw) ? Number(turnRaw) : null;
276
291
  const errorRaw = scalars["error"] ?? "";
@@ -392,6 +407,26 @@ window.__ModuleLoader__.load({
392
407
  function edgeKickDelayMs(msSinceLastPull) {
393
408
  return Math.max(EDGE_DEBOUNCE_MS, EDGE_FLOOR_MS - msSinceLastPull);
394
409
  }
410
+ /**
411
+ * #221: the turn-end pulse, derived from the host's own turn-boundary projection — no plugin
412
+ * record API needed, so it works on 0.2.0 hosts (where appendPluginRecord is absent) exactly as
413
+ * on newer ones. The host maintains a `turnBoundary` projection (the agent loop reads its
414
+ * lastTurn for its own bookkeeping); reading what the host already folds costs zero writes and
415
+ * delivers reactively through the same standard useProjection seat as any other projection.
416
+ * Returns true when a NEW turn ended since the last observation; the dock calls it once per
417
+ * render and stores the returned stamp back into `seen`.
418
+ */
419
+ function turnEndPulse(lastTurn, seen) {
420
+ const cur = typeof lastTurn === "number" && Number.isFinite(lastTurn) ? lastTurn : 0;
421
+ if (cur > seen) return {
422
+ pulse: true,
423
+ seen: cur
424
+ };
425
+ return {
426
+ pulse: false,
427
+ seen
428
+ };
429
+ }
395
430
  // src/client/prompt-dock.ts
396
431
  /**
397
432
  * The Suggested Next Step dock row (#113) — thin by design.
@@ -419,11 +454,12 @@ window.__ModuleLoader__.load({
419
454
  ".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}",
420
455
  ".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}",
421
456
  ".dsh-chapters_promptExpanded::before{align-items:flex-start;position:absolute;inset:0}",
457
+ ".dsh-chapters_promptExpanded{height:auto;min-height:36px;padding-top:8px;padding-bottom:8px}",
422
458
  ".dsh-chapters_promptGlyph{color:var(--dsw-alias-label-tertiary);flex:none;display:inline-flex}",
423
459
  ".dsh-chapters_promptLabel{color:var(--dsw-alias-label-primary);flex:none;font-size:13px;font-weight:500;line-height:24px}",
424
460
  ".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}",
425
461
  ".dsh-chapters_promptEllipsis{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;display:block}",
426
- ".dsh-chapters_promptFull{white-space:pre-wrap;overflow:auto;max-height:9rem;display:block}",
462
+ ".dsh-chapters_promptFull{white-space:pre-wrap;overflow-wrap:anywhere;line-height:20px;font-size:13px;color:var(--dsw-alias-label-primary);display:block}",
427
463
  ".dsh-chapters_promptError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:20px;flex:none;max-width:34%}",
428
464
  ".dsh-chapters_promptNote{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:20px;min-width:0}",
429
465
  ".dsh-chapters_promptIntent{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px}",
@@ -519,7 +555,9 @@ window.__ModuleLoader__.load({
519
555
  const { sessionId, inputActions, exec } = props;
520
556
  const sessionSnapshot = (props.useSession ?? (() => void 0))((snapshot) => snapshot);
521
557
  const running = props.running ?? deriveRunning(sessionSnapshot);
522
- const pushed = (props.useProjection ?? (() => void 0))(PROMPT_PROJECTION_KEY);
558
+ const useProjectionHook = props.useProjection ?? (() => void 0);
559
+ const pushed = useProjectionHook(PROMPT_PROJECTION_KEY);
560
+ const turnBoundary = useProjectionHook("turnBoundary");
523
561
  const [state, dispatch] = (0, react.useReducer)(promptBarReducer, initialBarState(), (s) => {
524
562
  const pushEvs = pushEventsOf(pushed);
525
563
  if (pushEvs !== null) return {
@@ -544,6 +582,7 @@ window.__ModuleLoader__.load({
544
582
  const lastPullAt = (0, react.useRef)(null);
545
583
  const edgeTimer = (0, react.useRef)(void 0);
546
584
  const pushLiveRef = (0, react.useRef)(false);
585
+ const seenTurnRef = (0, react.useRef)(0);
547
586
  const lastPushAt = (0, react.useRef)(null);
548
587
  const answered = (0, react.useRef)(false);
549
588
  const replyStatus = (0, react.useRef)("unknown");
@@ -605,13 +644,19 @@ window.__ModuleLoader__.load({
605
644
  pulled.current = true;
606
645
  let timer;
607
646
  const effectiveStatus = () => replyStatus.current === "unknown" ? stateRef.current.status : replyStatus.current;
608
- const spacingNow = () => pollSpacingFor({
609
- status: effectiveStatus(),
610
- autorun: stateRef.current.autorun,
611
- running: stateRef.current.running,
612
- hidden: typeof document !== "undefined" && document.hidden === true,
613
- edges: edgesRef.current
614
- });
647
+ const spacingNow = () => {
648
+ if (stateRef.current.autorun === "armed") return ACTIVE_POLL_MS;
649
+ const pe = turnEndPulse(turnBoundary?.lastTurn, seenTurnRef.current);
650
+ if (pe.pulse) seenTurnRef.current = pe.seen;
651
+ return pollSpacingFor({
652
+ status: effectiveStatus(),
653
+ autorun: stateRef.current.autorun,
654
+ running: stateRef.current.running,
655
+ hidden: typeof document !== "undefined" && document.hidden === true,
656
+ edges: edgesRef.current,
657
+ turnEndPulse: pe.pulse
658
+ });
659
+ };
615
660
  const duePull = () => {
616
661
  const c = sessionId !== void 0 ? stateCache.get(sessionId) : void 0;
617
662
  return mountPullDecision({
@@ -628,7 +673,8 @@ window.__ModuleLoader__.load({
628
673
  if (!(opts?.mount === true && duePull())) return;
629
674
  }
630
675
  const spacing = spacingNow();
631
- if (opts?.force === true || spacing !== null && !(opts?.mount === true && !duePull())) await refresh();
676
+ const mountDiscovery = opts?.mount === true && duePull();
677
+ if (opts?.force === true || mountDiscovery || spacing !== null && opts?.mount !== true) await refresh();
632
678
  if (replyStatus.current === "off") return;
633
679
  const next = spacingNow();
634
680
  if (next !== null) timer = setTimeout(() => {
@@ -763,7 +809,10 @@ window.__ModuleLoader__.load({
763
809
  className: "dsh-chapters_promptText",
764
810
  "aria-label": state.expanded ? "Put this suggested prompt in the input box" : "Show the full suggested prompt",
765
811
  onClick: populate
766
- }, 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", {
812
+ }, state.expanded ? (0, react.createElement)("span", { className: "dsh-chapters_promptFull" }, state.text ?? "") : (0, react.createElement)("span", {
813
+ className: "dsh-chapters_promptEllipsis",
814
+ title: state.text ?? void 0
815
+ }, lineFor(state))), ...state.note === "" ? [] : [(0, react.createElement)("span", {
767
816
  key: "note",
768
817
  className: "dsh-chapters_promptNote",
769
818
  role: "status"
@@ -997,13 +1046,14 @@ window.__ModuleLoader__.load({
997
1046
  return tryClick();
998
1047
  }
999
1048
  function makeDoorRunner(remote, sessions, door, verifySwitched, opts) {
1000
- return async function run(sessionId) {
1049
+ return async function run(sessionId, anchorSeq) {
1001
1050
  try {
1002
1051
  if (remote.commands?.execute === void 0) return {
1003
1052
  ok: false,
1004
1053
  message: "commands remote unavailable"
1005
1054
  };
1006
- const res = await remote.commands.execute(sessionId, door.command, []);
1055
+ const line = typeof anchorSeq === "number" && Number.isFinite(anchorSeq) ? `${door.command} ${String(Math.trunc(anchorSeq))}` : door.command;
1056
+ const res = await remote.commands.execute(sessionId, line, []);
1007
1057
  if (res.ok !== true) return {
1008
1058
  ok: false,
1009
1059
  message: (res.error?.message ?? `the ${door.noun} call failed`).slice(0, 90)
@@ -1246,7 +1296,16 @@ window.__ModuleLoader__.load({
1246
1296
  name: "conversation.chat.assistant-actions",
1247
1297
  id: door.id,
1248
1298
  label: door.label,
1249
- inject: (sessionId) => ({ run: () => run(sessionId) })
1299
+ inject: (sessionId, owner) => ({ run: async () => {
1300
+ let anchor = null;
1301
+ const mid = typeof owner?.messageId === "string" ? owner.messageId : "";
1302
+ if (mid !== "") try {
1303
+ const out = await runCommandText(remote, sessionId, `/chapters-anchor ${mid}`);
1304
+ const n = Number.parseInt(out.trim(), 10);
1305
+ if (Number.isFinite(n) && n > 0) anchor = n;
1306
+ } catch {}
1307
+ return run(sessionId, anchor);
1308
+ } })
1250
1309
  }, component));
1251
1310
  console.info(`[client:chapters] door factory ran: ${String(effects.length)} register effect(s), types: ${effects.map((e) => typeof e).join(",")}`);
1252
1311
  return effects;