@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 +40 -0
- package/lib/client.js +221 -27
- package/lib/client.js.map +1 -1
- package/lib/commands.js +26 -4
- 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
|
@@ -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
|
-
|
|
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 (
|
|
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:
|
|
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)(
|
|
657
|
+
const prevRunning = (0, react.useRef)(running ?? true);
|
|
481
658
|
(0, react.useEffect)(() => {
|
|
482
|
-
const next =
|
|
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)
|
|
490
|
-
|
|
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
|
|
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)(
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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",
|