dsh-pause-session 0.0.0-stage → 0.1.0

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 ADDED
@@ -0,0 +1,2299 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-pause-session",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
10
+ //#region lib/types/shared.js
11
+ /**
12
+ * The identities both halves of dsh-pause-session name, the status encodings they
13
+ * share, and the one spelling a host log line gives a caught value.
14
+ *
15
+ * The browser bundle may value-import only the shell's baseline module table,
16
+ * so this module imports nothing: the client half reads command names and the
17
+ * decode functions from it directly, and the host half derives the branded
18
+ * identities from the same strings.
19
+ *
20
+ * A third-party plugin's only live channel to the browser is the command
21
+ * registry — it can add neither a `ctx.remote` namespace nor a forwarded event,
22
+ * both of which are compiled into the harness's own API assembly. A command
23
+ * descriptor's only free-form field is its `description`, and a command
24
+ * result's only free-form field is its `text`, so the park state travels as a
25
+ * prefix plus `;`-separated fields on one of those. The text stays
26
+ * human-readable: the same words are what the slash-command menu shows.
27
+ *
28
+ * Every value inside a field is escaped, so a decoded name is the exact name
29
+ * that was encoded. Session ids, tool names, and call ids are the host's own
30
+ * data and may contain the `;`, `,`, `/`, or newline a field is separated by; a
31
+ * folded spelling would decode as a different name — a different row, looked up
32
+ * again.
33
+ * @module dsh-pause-session/shared
34
+ */
35
+ /** Command that pauses the receiving session before its next model request. */
36
+ const PAUSE_COMMAND = "pause";
37
+ /** Command that resumes the receiving session; registered only while that session is paused. */
38
+ const RESUME_COMMAND = "pause-resume";
39
+ /** Command that resumes every parked session the registry remembers. */
40
+ const RESUME_ALL_COMMAND = "pause-resume-all";
41
+ /** Command that registers and parks every running session tree in one action. */
42
+ const PAUSE_ALL_COMMAND = "pause-all";
43
+ /** Command that reports the whole-app park state; registered globally, always. */
44
+ const STATE_ALL_COMMAND = "pause-state-all";
45
+ /** Command that reports the receiving session's park state; registered only while it is paused. */
46
+ const STATUS_COMMAND = "pause-state";
47
+ /** Command that turns the receiving session's own goal re-arm on or off; registered only while it is paused. */
48
+ const GOAL_COMMAND = "pause-goal";
49
+ /** Command that reports every session that did not stop cleanly. */
50
+ const TRIAGE_COMMAND = "pause-triage";
51
+ /** Command that resumes one session from the decision list exactly as the clean path would. */
52
+ const TRIAGE_CONTINUE_COMMAND = "pause-triage-continue";
53
+ /** Command that resumes one session from the decision list with the unknown-outcome note. */
54
+ const TRIAGE_NOTE_COMMAND = "pause-triage-note";
55
+ /** Command that opens one session from the decision list without spending a request. */
56
+ const TRIAGE_REVIEW_COMMAND = "pause-triage-review";
57
+ /** Command that leaves one session from the decision list exactly as it is. */
58
+ const TRIAGE_LEAVE_COMMAND = "pause-triage-leave";
59
+ /** Command that turns one listed session's goal re-arm on or off. */
60
+ const TRIAGE_GOAL_COMMAND = "pause-triage-goal";
61
+ /**
62
+ * One list field's text when the list is empty. A value that would itself spell
63
+ * this marker is escaped by {@link escapeField}, so an empty list and a list
64
+ * holding the marker stay different texts.
65
+ */
66
+ const EMPTY_FIELD = "-";
67
+ /**
68
+ * One value previously written by {@link escapeField}.
69
+ * @param text - the encoded value, escapes included.
70
+ * @returns the value the host holds.
71
+ */
72
+ function unescapeField(text) {
73
+ return text.replace(/\\(.)/g, (_escape, character) => character === "n" ? "\n" : character);
74
+ }
75
+ /**
76
+ * Split one encoded text on the separators that are not escaped, keeping each
77
+ * escape beside the character it protects so {@link unescapeField} can read it.
78
+ * @param text - an encoded text, without its prefix.
79
+ * @param separator - the single character to split on.
80
+ * @returns the parts, in order; always at least one.
81
+ */
82
+ function splitFields(text, separator) {
83
+ const parts = [];
84
+ let part = "";
85
+ for (let at = 0; at < text.length; at += 1) {
86
+ const character = text[at] ?? "";
87
+ if (character === "\\") {
88
+ part += character + (text[at + 1] ?? "");
89
+ at += 1;
90
+ continue;
91
+ }
92
+ if (character === separator) {
93
+ parts.push(part);
94
+ part = "";
95
+ continue;
96
+ }
97
+ part += character;
98
+ }
99
+ parts.push(part);
100
+ return parts;
101
+ }
102
+ /**
103
+ * One field of an encoded text, by its key.
104
+ * @param fields - the separated fields of one encoded text.
105
+ * @param key - the field's key, without its `:`.
106
+ * @returns the field's value with its escapes still in place, or `undefined`
107
+ * when the text does not carry that field.
108
+ */
109
+ function fieldOf(fields, key) {
110
+ const prefix = `${key}:`;
111
+ const at = fields.findIndex((field) => field.trim().startsWith(prefix));
112
+ return at < 0 ? void 0 : fields[at]?.trim().slice(prefix.length).trim();
113
+ }
114
+ /** The one word each phase is published as, and the whole decoding vocabulary. */
115
+ const PHASE_WORD = {
116
+ draining: "pausing",
117
+ parking: "saving",
118
+ safe: "paused",
119
+ "flush-failed": "save failed"
120
+ };
121
+ /**
122
+ * The two states of one record's goal re-arm field, in the one spelling both
123
+ * the per-session status and the decision list publish it with.
124
+ */
125
+ const GOAL_ON = "on";
126
+ const GOAL_OFF = "off";
127
+ /**
128
+ * Decode a status previously written by {@link encodeParkStatus}.
129
+ * @param text - a command descriptor's description.
130
+ * @returns the status, or `undefined` when this text carries none — a foreign
131
+ * command, or a reading taken before the host published anything.
132
+ */
133
+ function decodeParkStatus(text) {
134
+ if (!text.startsWith("pause: ")) return void 0;
135
+ const [head = "", ...rest] = splitFields(text.slice(7), ";");
136
+ const word = head.trim();
137
+ const phase = Object.keys(PHASE_WORD).find((candidate) => PHASE_WORD[candidate] === word);
138
+ if (phase === void 0) return void 0;
139
+ const goal = fieldOf(rest, "goal");
140
+ return {
141
+ phase,
142
+ safe: phase === "safe",
143
+ runningTools: decodeNames(fieldOf(rest, "tools")),
144
+ ...goal === GOAL_ON ? { rearmGoal: true } : goal === GOAL_OFF ? { rearmGoal: false } : {}
145
+ };
146
+ }
147
+ /**
148
+ * Decode one id previously written by {@link encodeId}.
149
+ * @param field - the encoded field, escapes included.
150
+ * @returns the id; `undefined` when the text carries none — an absent field and
151
+ * the empty marker both mean the answer names no session, and a client that
152
+ * read either as one would address a session the host never named.
153
+ */
154
+ function decodeId(field) {
155
+ const text = field?.trim();
156
+ if (text === void 0 || text.length === 0 || text === EMPTY_FIELD) return void 0;
157
+ return unescapeField(text);
158
+ }
159
+ /** Decode one list field. */
160
+ function decodeNames(field) {
161
+ const text = field?.trim();
162
+ if (text === void 0 || text.length === 0 || text === EMPTY_FIELD) return [];
163
+ return splitFields(text, ",").map((name) => unescapeField(name.trim())).filter((name) => name.length > 0);
164
+ }
165
+ /**
166
+ * Decode a whole-app status previously written by {@link encodeGlobalStatus}.
167
+ * @param text - a command result's text.
168
+ * @returns the status, or `undefined` when this text carries none.
169
+ */
170
+ function decodeGlobalStatus(text) {
171
+ if (!text.startsWith("pause all: ")) return void 0;
172
+ const [head = "", ...fields] = splitFields(text.slice(11), ";");
173
+ const verdict = head.trim();
174
+ if (verdict !== "idle" && verdict !== "waiting" && verdict !== "safe") return void 0;
175
+ const byKey = /* @__PURE__ */ new Map();
176
+ for (const field of fields) {
177
+ const at = field.indexOf(":");
178
+ if (at < 0) continue;
179
+ byKey.set(field.slice(0, at).trim(), field.slice(at + 1).trim());
180
+ }
181
+ const count = (key) => {
182
+ const value = Number(byKey.get(key));
183
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
184
+ };
185
+ const parkedAt = decodeId(byKey.get("parked-at"));
186
+ return {
187
+ verdict,
188
+ registered: count("registered"),
189
+ safe: count("safe"),
190
+ ...parkedAt === void 0 ? {} : { parkedAt },
191
+ draining: decodeNames(byKey.get("draining")),
192
+ parking: count("parking"),
193
+ stalled: decodeNames(byKey.get("stalled")),
194
+ unresumable: decodeNames(byKey.get("unresumable")),
195
+ unreadTrees: decodeNames(byKey.get("unread-trees")),
196
+ jobs: decodeNames(byKey.get("jobs")),
197
+ blind: decodeNames(byKey.get("blind")).filter((spot) => spot === "trees" || spot === "jobs")
198
+ };
199
+ }
200
+ /** How one row is spelled in the published list; the wake field's state when the host can hand over the owed request. */
201
+ const WAKE_YES = "yes";
202
+ /** Decode {@link encodeTraces}; an item without a tool name is not a trace. */
203
+ function decodeTraces(field) {
204
+ const text = field?.trim();
205
+ if (text === void 0 || text.length === 0 || text === EMPTY_FIELD) return [];
206
+ return splitFields(text, ",").flatMap((item) => {
207
+ const [tool = "", callId = ""] = splitFields(item.trim(), "/");
208
+ if (tool.length === 0) return [];
209
+ return [{
210
+ tool: unescapeField(tool),
211
+ callId: unescapeField(callId)
212
+ }];
213
+ });
214
+ }
215
+ /**
216
+ * Decode a decision list previously written by {@link encodeTriageList}.
217
+ * @param text - a command result's text.
218
+ * @returns the list, or `undefined` when this text carries none.
219
+ */
220
+ function decodeTriageList(text) {
221
+ if (!text.startsWith("pause triage: ")) return void 0;
222
+ const [head = "", ...rest] = splitFields(text.slice(14), ";");
223
+ if (!Number.isSafeInteger(Number(head.trim())) || Number(head.trim()) < 0) return void 0;
224
+ const rows = [];
225
+ let fields = [];
226
+ const flush = () => {
227
+ if (fields.length === 0) return;
228
+ const sessionId = fieldOf(fields, "row");
229
+ const phase = fieldOf(fields, "phase");
230
+ if (sessionId === void 0 || phase === void 0 || phase === "safe") return;
231
+ const stopped = fieldOf(fields, "stopped");
232
+ const reading = stopped === "unreadable" ? "unreadable" : "read";
233
+ const position = reading === "unreadable" || stopped === void 0 || stopped === EMPTY_FIELD ? null : /^(.*?)(?:\/turn (\d+)(?: step (\d+))?)?$/.exec(stopped);
234
+ rows.push({
235
+ sessionId: unescapeField(sessionId),
236
+ phase,
237
+ updatedAt: 0,
238
+ reading,
239
+ ...position === null ? {} : {
240
+ lastEvent: unescapeField(position[1] ?? ""),
241
+ ...position[2] === void 0 ? {} : { turn: Number(position[2]) },
242
+ ...position[3] === void 0 ? {} : { step: Number(position[3]) }
243
+ },
244
+ traces: reading === "unreadable" ? [] : decodeTraces(fieldOf(fields, "traces")),
245
+ owed: fieldOf(fields, "owed") === WAKE_YES,
246
+ wakeable: fieldOf(fields, "wake") === WAKE_YES,
247
+ rearmGoal: fieldOf(fields, "goal") !== GOAL_OFF
248
+ });
249
+ fields = [];
250
+ };
251
+ for (const field of rest) {
252
+ if (field.trim().startsWith("row:")) flush();
253
+ fields.push(field);
254
+ }
255
+ flush();
256
+ return { rows };
257
+ }
258
+ //#endregion
259
+ //#region lib/types/client/burst.js
260
+ /**
261
+ * The re-read burst both browser controls share: a handful of reads after
262
+ * something moved, and silence once it settles.
263
+ *
264
+ * What a control reads is published through the host's command channel, and
265
+ * every read is two durable `command/run|done` records in the address session's
266
+ * log plus an aggregate read on the host. Reading is therefore something the
267
+ * host announces and the user asks for, never a heartbeat:
268
+ *
269
+ * - the host announces a transition by registering or unregistering the paused
270
+ * session's marker commands, and — for a phase change, which no registration
271
+ * of its own would show — by registering that session's status verb again.
272
+ * Each of those emits `commands/change`, and that event, plus the user's own
273
+ * press or decision, is what starts a burst;
274
+ * - one transition announces itself several times: a park registers two or three
275
+ * marker commands per session and a phase change re-registers one, and the host
276
+ * emits one `commands/change` per step. The burst therefore waits out a short
277
+ * `windowMs` before its first read and any trigger inside that window
278
+ * postpones it, so the announcements of one transition start one burst rather
279
+ * than one each;
280
+ * - a change that arrives within `ACTION_ABSORB_MS` of the user's own action is
281
+ * that action's own consequence, not news: the action already read the host,
282
+ * so the answer is on screen, and the unregistrations it caused — one event
283
+ * per marker command, measured seconds apart, which the window above cannot
284
+ * merge — would only be read back. Such a change is absorbed, and each one
285
+ * absorbed pushes the window out, because a storm that keeps arriving is
286
+ * still the same storm. The window always closes: the first change after
287
+ * `ACTION_ABSORB_MS` of quiet is news again, and a user action reads on its
288
+ * own whatever the window says;
289
+ * - an absorbed change is deferred rather than lost: a control whose action left
290
+ * a phase still moving asks for one look at the moment the window closes
291
+ * ({@link ReadBurst.recheckAfterEcho}), which is the read that finds a
292
+ * transition the window hid — the park can reach its final phase inside that
293
+ * window and never announce anything again. One look per action, and only
294
+ * while the state it left is still moving;
295
+ * - a burst is a fixed short list of waits, so at most that many reads answer
296
+ * one transition, however long the page stays open;
297
+ * - a read that finds the verdict settled ends the burst at once, and the last
298
+ * reading stays on screen — "the park is settled" is not a state change, so
299
+ * it must not be re-asked;
300
+ * - a read that finds a phase still moving ends it just as fast. Every phase
301
+ * transition is announced at its source, the flush that resolves `parking`
302
+ * into `safe` included, and that announcement is the very event that starts
303
+ * the next burst — so watching a moving phase from here spends records on a
304
+ * transition that will speak for itself;
305
+ * - what the burst still owes after a read is delivery: the read one
306
+ * announcement bought, and one more when a change lands while a pass is in
307
+ * flight, which that pass cannot have answered. The bound is a bound, not a
308
+ * heartbeat: a phase that never settles is left on screen unread until
309
+ * something else moves;
310
+ * - the bound is a trade, taken knowingly: a reading left on screen when the
311
+ * burst runs out is the host's own last answer, and the next trigger — a
312
+ * forwarded change, a press, or the shell handing a control its session again
313
+ * — reads the truth again. A reading nobody answered is never rendered as a
314
+ * confident state. A longer watch is a longer list of waits, at a linear price
315
+ * in the address session's log;
316
+ * - while the page is hidden no read is armed at all, and the wait a burst
317
+ * still owed is armed again when the page comes back: hidden time is neither
318
+ * charged to the burst nor lost from it.
319
+ *
320
+ * A burst is not restarted by a trigger that lands while one is running: every
321
+ * read it still owes happens after that trigger, so absorbing it loses nothing
322
+ * and a storm of registry changes costs one burst rather than one each.
323
+ * @module dsh-pause-session/client/burst
324
+ */
325
+ /** The policy every control uses unless a caller says otherwise. */
326
+ const DEFAULT_BURST_POLICY = {
327
+ windowMs: 400,
328
+ waitsMs: [
329
+ 400,
330
+ 1e3,
331
+ 2400,
332
+ 4800
333
+ ],
334
+ visible: defaultVisibility
335
+ };
336
+ /**
337
+ * How long after a client action a registry change is that action's own echo.
338
+ *
339
+ * A press, a release, a decision, a goal switch and an opened panel each read
340
+ * the host as they run, so their answer is already on screen when the host
341
+ * announces what they did — one `commands/change` per marker command, measured
342
+ * 0.44 s to 2.04 s apart, a cadence the coalescing window cannot merge. Reading
343
+ * those back is paying for an answer the user already has.
344
+ *
345
+ * The window is what makes `commands/change` mean "something moved that this
346
+ * browser did not do". It is refreshed by every change it absorbs, so a storm
347
+ * that keeps arriving stays absorbed rather than leaking one read per change,
348
+ * and it ends for good `ACTION_ABSORB_MS` after the last change: a change after
349
+ * that is news, and so is the next user action, which reads on its own.
350
+ *
351
+ * The margin over the measured 2.04 s gap is deliberate: a transition's own
352
+ * storm has to fit inside the window for the whole storm to stay absorbed.
353
+ */
354
+ const ACTION_ABSORB_MS = 2500;
355
+ /**
356
+ * The waits one burst uses when its first wait is `first`: four reads spread
357
+ * over roughly ten times that. Long enough that a read a hidden page or an
358
+ * in-flight pass deferred is still taken, short enough that a state which never
359
+ * settles costs a handful of log records rather than a night of them.
360
+ * @param first - milliseconds before the burst's first read, after its window; `<= 0` disables bursting.
361
+ * @returns the waits, in order; empty when bursting is disabled.
362
+ */
363
+ function burstWaits(first) {
364
+ return first > 0 ? [
365
+ first,
366
+ first * 2.5,
367
+ first * 6,
368
+ first * 12
369
+ ] : [];
370
+ }
371
+ /**
372
+ * Whether the browser reports the page as on screen. A shell without a
373
+ * `document` (a test harness, a non-browser host) counts as visible: a control
374
+ * that could never read would be a worse lie than one that reads.
375
+ * @returns whether the page is visible now.
376
+ */
377
+ function defaultVisibility() {
378
+ return (typeof document === "undefined" ? void 0 : document.visibilityState) !== "hidden";
379
+ }
380
+ /** The real clock: `setTimeout` clearing itself. */
381
+ const SYSTEM_CLOCK = {
382
+ set: (callback, ms) => setTimeout(callback, ms),
383
+ clear: (timer) => {
384
+ clearTimeout(timer);
385
+ }
386
+ };
387
+ /**
388
+ * One control's re-read burst. Every method is safe to call in any order and as
389
+ * often as the caller likes: a trigger starts at most one burst, and one burst
390
+ * never arms more than one read at a time.
391
+ */
392
+ var ReadBurst = class {
393
+ onRead;
394
+ policy;
395
+ clock;
396
+ /** The armed wait, when one is counting down. */
397
+ timer;
398
+ /** Whether the burst is waiting out its collection window rather than a wait between reads. */
399
+ window = false;
400
+ /** Waits this burst has not spent yet, front first. */
401
+ left = [];
402
+ /** Whether a burst is in progress: it may be collecting, waiting, reading, or reading again. */
403
+ running = false;
404
+ /** The absorption window a client action opened, while one is open. */
405
+ absorbed;
406
+ /** Whether a control asked for the one look owed when the echo window closes. */
407
+ recheck = false;
408
+ /**
409
+ * @param onRead - one more read. Called only for a read the burst owes and the page can show.
410
+ * @param policy - the collection window, the waits, and the visibility source.
411
+ * @param clock - the timer implementation.
412
+ */
413
+ constructor(onRead, policy, clock = SYSTEM_CLOCK) {
414
+ this.onRead = onRead;
415
+ this.policy = policy;
416
+ this.clock = clock;
417
+ }
418
+ /** Whether this control bursts at all. */
419
+ get enabled() {
420
+ return this.policy.waitsMs.length > 0;
421
+ }
422
+ /** Whether one more read is already counting down. */
423
+ get armed() {
424
+ return this.timer !== void 0;
425
+ }
426
+ /** Whether a burst is in progress, with or without a read counting down. */
427
+ get live() {
428
+ return this.running;
429
+ }
430
+ /** Whether a change right now is a client action's own echo. */
431
+ get absorbing() {
432
+ return this.absorbed !== void 0;
433
+ }
434
+ /**
435
+ * The user acted on this control and the action reads: a press, a release, a
436
+ * decision, a goal switch, an opened panel. Its read is the answer, and the
437
+ * registry changes it sets off announce themselves for a while afterwards, so
438
+ * until the window closes a trigger is that echo rather than news.
439
+ *
440
+ * Called as the action is taken, before its read lands, because the echo can
441
+ * arrive while the action is still running.
442
+ */
443
+ absorb() {
444
+ if (!this.enabled) return;
445
+ this.armAbsorb();
446
+ }
447
+ /**
448
+ * Ask for one read at the moment the echo window closes, because the action's
449
+ * own read left a state that is still moving.
450
+ *
451
+ * The echo window swallows a transition announced inside it — the forwarded
452
+ * event carries no payload, so nothing distinguishes a marker registration
453
+ * from a phase change — and after the window no change is guaranteed to come:
454
+ * the transition that was hidden may be the last one this park makes. The
455
+ * look owed here is the read that finds it, one per action, and it is not a
456
+ * watch: once the window has closed, every further change is news and buys its
457
+ * own read. A control whose action left a settled state owes nothing and asks
458
+ * for nothing.
459
+ */
460
+ recheckAfterEcho() {
461
+ if (!this.enabled) return;
462
+ this.recheck = true;
463
+ }
464
+ /**
465
+ * Something moved — a registry change. The burst first waits out its
466
+ * collection window, and every trigger that lands inside it restarts that
467
+ * window: the two or three registrations of one park start one burst rather
468
+ * than one each. A burst already past its window takes this as covered —
469
+ * every read it still owes happens after the change.
470
+ *
471
+ * @returns whether the change is news rather than a client action's own echo.
472
+ * The echo announces what the action's read already answered, so only a news
473
+ * trigger is a change a control still owes a read for; a change that lands
474
+ * while a burst is under way is covered by the read that burst already owes.
475
+ * `false` also for a disabled burst, which reads nothing at all.
476
+ */
477
+ trigger() {
478
+ if (!this.enabled) return false;
479
+ if (this.absorbing) {
480
+ this.armAbsorb();
481
+ return false;
482
+ }
483
+ if (!this.running) {
484
+ this.running = true;
485
+ this.left = this.policy.waitsMs;
486
+ this.armWindow();
487
+ return true;
488
+ }
489
+ if (this.window) this.armWindow();
490
+ return true;
491
+ }
492
+ /**
493
+ * The control knows it still owes a read: a change that is news has arrived,
494
+ * or a reading just revealed something only a read can answer. A call outside
495
+ * any burst starts one. Absorption never applies here — this is not a change
496
+ * some action caused, it is a read the control already knows it needs.
497
+ */
498
+ next() {
499
+ if (this.armed || this.window) return;
500
+ if (!this.running) {
501
+ this.running = true;
502
+ this.left = this.policy.waitsMs;
503
+ }
504
+ this.owe();
505
+ }
506
+ /**
507
+ * Nothing is moving: end the burst and forget it. The last reading stays
508
+ * published, and the next trigger starts a fresh burst. Two things outlive
509
+ * this: an action's absorption window, which is about what the action caused
510
+ * rather than about the state, and the one look it may still owe — see
511
+ * {@link recheckAfterEcho}.
512
+ */
513
+ stop() {
514
+ this.running = false;
515
+ this.left = [];
516
+ this.window = false;
517
+ this.cancel();
518
+ }
519
+ /** Drop the armed wait. A burst that is still owed its read survives it; see {@link checkVisibility}. */
520
+ cancel() {
521
+ if (this.timer === void 0) return;
522
+ this.clock.clear(this.timer);
523
+ this.timer = void 0;
524
+ }
525
+ /**
526
+ * Re-evaluate one page-visibility change. Going off screen drops the armed
527
+ * wait — a hidden page is not being read — and coming back arms it again, so
528
+ * the burst resumes where it stopped instead of being spent while nobody was
529
+ * looking. A burst still collecting resumes its window, restarted from the
530
+ * moment the page came back: a trigger that arrived while it was hidden still
531
+ * postpones the read.
532
+ */
533
+ checkVisibility() {
534
+ if (this.visibleNow()) {
535
+ this.arm();
536
+ return;
537
+ }
538
+ this.cancel();
539
+ }
540
+ /** Spend the next wait of the burst, or end it when the bound is reached. */
541
+ owe() {
542
+ const wait = this.left[0];
543
+ if (wait === void 0) {
544
+ this.running = false;
545
+ return;
546
+ }
547
+ this.armWait(wait);
548
+ }
549
+ /**
550
+ * (Re)start the collection window. While the page is hidden nothing is armed
551
+ * — hidden time is not charged to the burst — and a trigger that lands then
552
+ * is still collected, because coming back starts a whole window.
553
+ */
554
+ armWindow() {
555
+ const wait = Math.max(this.policy.windowMs, 0);
556
+ if (!this.visibleNow()) {
557
+ this.window = true;
558
+ return;
559
+ }
560
+ this.cancel();
561
+ this.window = true;
562
+ this.timer = this.clock.set(() => {
563
+ this.timer = void 0;
564
+ this.window = false;
565
+ this.owe();
566
+ }, wait);
567
+ }
568
+ /** Put back whatever the burst still owed: its window, or the wait after a read. */
569
+ arm() {
570
+ if (this.window) {
571
+ this.armWindow();
572
+ return;
573
+ }
574
+ const wait = this.left[0];
575
+ if (wait !== void 0) this.armWait(wait);
576
+ }
577
+ /**
578
+ * (Re)start the absorption window a client action opened. A hidden page keeps
579
+ * absorbing: this window is about what the action caused, not about what the
580
+ * page is doing. When the window closes, the one look an unsettled action owes
581
+ * is taken; the window is restarted by every change it absorbs, so that look
582
+ * waits for the echo to actually stop.
583
+ */
584
+ armAbsorb() {
585
+ this.cancelAbsorb();
586
+ this.absorbed = this.clock.set(() => {
587
+ this.absorbed = void 0;
588
+ if (!this.recheck) return;
589
+ this.recheck = false;
590
+ this.next();
591
+ }, Math.max(ACTION_ABSORB_MS, 0));
592
+ }
593
+ /** Close the absorption window: changes are news again. */
594
+ cancelAbsorb() {
595
+ if (this.absorbed === void 0) return;
596
+ this.clock.clear(this.absorbed);
597
+ this.absorbed = void 0;
598
+ }
599
+ /**
600
+ * Arm one wait, spending it when it fires. The wait stays in `left` until
601
+ * then, so a page hidden mid-wait takes the whole wait again: hidden time is
602
+ * neither charged to the burst nor lost from it.
603
+ */
604
+ armWait(wait) {
605
+ if (this.timer !== void 0 || !this.visibleNow()) return;
606
+ this.timer = this.clock.set(() => {
607
+ this.timer = void 0;
608
+ this.left = this.left.slice(1);
609
+ this.onRead();
610
+ }, Math.max(wait, 0));
611
+ }
612
+ /** Whether the page counts as on screen now, with an absent `document` read as on screen. */
613
+ visibleNow() {
614
+ try {
615
+ return this.policy.visible();
616
+ } catch {
617
+ return true;
618
+ }
619
+ }
620
+ };
621
+ //#endregion
622
+ //#region lib/types/client/control.js
623
+ /**
624
+ * Browser-side reading and driving of the host's park state for one session.
625
+ *
626
+ * The host publishes no push channel of its own — a third-party plugin cannot
627
+ * add a Client Remote namespace or a forwarded event — so this control reads
628
+ * the host's command registry, which is the only live surface it can publish
629
+ * through: `commands.list(sessionId)` is a Client-callable read, and the
630
+ * forwarded `commands/change` event says when to read it again. The resume
631
+ * definition's presence in that list IS the host's "this session is paused"
632
+ * fact, and the companion status definition answers with the park state as of
633
+ * the read; this module never derives either from its own press. That one
634
+ * answer carries the record's goal opt-out as well, so the switch beside the
635
+ * pause button can show it without a second host round-trip — and without it,
636
+ * the control renders no switch at all.
637
+ *
638
+ * Every phase the host reaches announces itself on that channel: a transition
639
+ * re-registers the status verb, so a transition is what buys the next read and a
640
+ * read that finds a phase still moving is the answer to the transition that
641
+ * produced it rather than a reason to keep asking (`./burst.ts`). One transition
642
+ * arrives as several registry changes — parking a session also registers its
643
+ * three commands — so the burst's own collection window is what turns a tick of
644
+ * changes into one read per shown session. A press or a goal switch reads as it
645
+ * runs, and the marker changes it causes are that action's own echo: those are
646
+ * absorbed for a short window (`./burst.ts`) rather than read back — except for
647
+ * the one look an action owes when that window closes, if the reading it left
648
+ * behind is still moving, because a transition announced inside the window would
649
+ * otherwise never be read at all.
650
+ *
651
+ * The whole-app control reads the same park and publishes what it found
652
+ * (`./global.ts`), and a header consults that answer before spending a read of
653
+ * its own: with nothing registered, no session can be paused, so a header that
654
+ * already says "running" is left saying it rather than paying a round-trip to
655
+ * hear it again. A header the host last showed on its way to a park is never
656
+ * answered that way — that reading is the newer fact — and the derivation is
657
+ * voided by the next whole-app answer that names a registered session, which
658
+ * the forwarded change a park causes is what brings in.
659
+ * @module dsh-pause-session/client/control
660
+ */
661
+ const INITIAL$1 = { bySession: {} };
662
+ /** Phases that name a moment on the way to a park rather than a state it reached. */
663
+ const MOVING = /* @__PURE__ */ new Set(["draining", "parking"]);
664
+ /** Owns the published reading per session; nothing else writes that state. */
665
+ var PauseControl = class {
666
+ commands;
667
+ lifetime;
668
+ aggregate;
669
+ /** State source the Session-header contribution binds its selector hook to. */
670
+ store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(INITIAL$1);
671
+ /** Sessions this control has shown; a host registry change re-reads every one of them. */
672
+ shown = /* @__PURE__ */ new Set();
673
+ /** Highest read sequence requested per session: an answer older than it is dropped. */
674
+ latest = /* @__PURE__ */ new Map();
675
+ /**
676
+ * Sessions a change has put back on the list of ones to read once — a
677
+ * transition this browser has been told about and has not answered yet.
678
+ */
679
+ owed = /* @__PURE__ */ new Set();
680
+ /**
681
+ * Whether the look owed at the close of an echo window is still to be taken.
682
+ * It reads every shown session, exactly as a change does: the look exists
683
+ * because a change may have been swallowed, and a swallowed change carries no
684
+ * payload either.
685
+ */
686
+ trailing = false;
687
+ /**
688
+ * Sessions whose header was answered from the whole-app "nothing is
689
+ * registered" instead of a read of their own, and which therefore have to be
690
+ * read for real if that answer stops holding.
691
+ */
692
+ derived = /* @__PURE__ */ new Set();
693
+ /** The bounded burst of reads one transition buys; see `./burst.ts`. */
694
+ burst;
695
+ reads = 0;
696
+ /**
697
+ * @param commands - the Client-callable command namespace.
698
+ * @param lifetime - aborts in-flight calls and the burst when the plugin unloads.
699
+ * @param windowMs - how long the burst collects the registrations of one
700
+ * transition before its first read; also the wait after a press.
701
+ * @param policy - the burst waits and visibility source; defaults to the real page.
702
+ * @param aggregate - the whole-app control's published answer; absent means it is never consulted.
703
+ */
704
+ constructor(commands, lifetime, windowMs = 400, policy = DEFAULT_BURST_POLICY, aggregate = () => void 0) {
705
+ this.commands = commands;
706
+ this.lifetime = lifetime;
707
+ this.aggregate = aggregate;
708
+ this.burst = new ReadBurst(() => {
709
+ this.readAll();
710
+ }, {
711
+ ...policy,
712
+ windowMs,
713
+ waitsMs: burstWaits(windowMs)
714
+ });
715
+ }
716
+ /**
717
+ * Read the host's state for one session.
718
+ * @param sessionId - session whose control was rendered.
719
+ * @returns after this read's answer is published or superseded by a newer one.
720
+ */
721
+ async refresh(sessionId) {
722
+ await this.readOne(sessionId);
723
+ this.follow();
724
+ }
725
+ /**
726
+ * The host's command registry changed, so every session this control has
727
+ * shown may have moved — the forwarded event carries no payload, so the only
728
+ * honest answer is to look at all of them once. The reads run on the burst's
729
+ * own schedule rather than here, which is what makes a tick's worth of changes
730
+ * — parking a session registers three commands, and a phase change re-registers
731
+ * one more — one burst instead of one each. A change that is this browser's own
732
+ * action's echo owes nothing: the action read the host as it ran.
733
+ */
734
+ refreshAll() {
735
+ if (this.shown.size === 0 || this.lifetime.aborted) return;
736
+ if (!this.burst.trigger()) return;
737
+ for (const sessionId of this.shown) this.owed.add(sessionId);
738
+ }
739
+ /**
740
+ * One pass of the burst: every shown session a change still owes a read. The
741
+ * forwarded event carries no payload, so a change may have touched any of
742
+ * them and each is read once. A session the whole-app answer has already
743
+ * spoken for is answered from it instead of being asked about; see
744
+ * {@link answerIsKnown}.
745
+ */
746
+ async readAll() {
747
+ if (this.lifetime.aborted) {
748
+ this.burst.stop();
749
+ return;
750
+ }
751
+ const pass = this.owed.size > 0 ? [...this.owed] : this.trailing ? [...this.shown] : [];
752
+ this.owed.clear();
753
+ this.trailing = false;
754
+ for (const sessionId of pass) {
755
+ if (this.answerIsKnown(sessionId)) {
756
+ this.deriveActive(sessionId);
757
+ continue;
758
+ }
759
+ await this.readOne(sessionId);
760
+ }
761
+ this.follow();
762
+ }
763
+ /** One session's read, published unless a newer read of that session supersedes it. */
764
+ async readOne(sessionId) {
765
+ this.shown.add(sessionId);
766
+ this.derived.delete(sessionId);
767
+ const sequence = ++this.reads;
768
+ this.latest.set(sessionId, sequence);
769
+ const reading = await this.read(sessionId);
770
+ if (this.latest.get(sessionId) !== sequence) return;
771
+ this.publish(sessionId, reading);
772
+ }
773
+ /**
774
+ * Press the control: pause an active session, or take back a pause that is
775
+ * still on its way in (or already settled). The phase shown afterwards is the
776
+ * host's answer, never an assumed flip.
777
+ * @param sessionId - session whose control was pressed.
778
+ * @returns after the control re-reads the host.
779
+ */
780
+ async toggle(sessionId) {
781
+ const current = this.store.getSnapshot().bySession[String(sessionId)]?.phase;
782
+ const line = current === void 0 || current === "active" ? `/${PAUSE_COMMAND}` : `/${RESUME_COMMAND}`;
783
+ this.burst.absorb();
784
+ if (!(await this.commands.execute(sessionId, line, [], this.lifetime)).ok) {
785
+ this.publish(sessionId, {
786
+ phase: "unavailable",
787
+ runningTools: []
788
+ });
789
+ this.follow();
790
+ return;
791
+ }
792
+ await this.refresh(sessionId);
793
+ }
794
+ /**
795
+ * Flip this session's goal opt-out. The verb belongs to the session it
796
+ * changes — the host registers it there, and it takes no other target — so
797
+ * this press needs no listed row. What the switch shows afterwards is the
798
+ * host's next answer, never a local flip: a refused switch leaves the record
799
+ * exactly as it was, and the re-read is what says so.
800
+ * @param sessionId - session whose switch was pressed.
801
+ * @returns after the control re-reads the host.
802
+ */
803
+ async toggleGoal(sessionId) {
804
+ this.burst.absorb();
805
+ await this.commands.execute(sessionId, `/${GOAL_COMMAND}`, [], this.lifetime);
806
+ await this.refresh(sessionId);
807
+ }
808
+ /** Ask the host what this session's park state is, in the browser's vocabulary. */
809
+ async read(sessionId) {
810
+ const listed = await this.commands.list(sessionId);
811
+ if (!listed.ok) return {
812
+ phase: "unavailable",
813
+ runningTools: []
814
+ };
815
+ if (!listed.value.some((descriptor) => descriptor.definitionId === "dsh-pause-session/resume")) return {
816
+ phase: "active",
817
+ runningTools: []
818
+ };
819
+ if (listed.value.find((descriptor) => descriptor.definitionId === "dsh-pause-session/status") === void 0) return {
820
+ phase: "parking",
821
+ runningTools: []
822
+ };
823
+ const answered = await this.commands.execute(sessionId, `/${STATUS_COMMAND}`, [], this.lifetime);
824
+ if (!answered.ok || answered.value === void 0) return {
825
+ phase: "unavailable",
826
+ runningTools: []
827
+ };
828
+ const text = answered.value.result.kind === "success" ? answered.value.result.text : void 0;
829
+ const decoded = text === void 0 ? void 0 : decodeParkStatus(text);
830
+ if (decoded === void 0) return {
831
+ phase: "parking",
832
+ runningTools: []
833
+ };
834
+ return {
835
+ phase: decoded.phase,
836
+ runningTools: decoded.runningTools,
837
+ ...decoded.rearmGoal === void 0 ? {} : { rearmGoal: decoded.rearmGoal }
838
+ };
839
+ }
840
+ publish(sessionId, reading) {
841
+ this.store.update((state) => {
842
+ state.bySession = {
843
+ ...state.bySession,
844
+ [String(sessionId)]: reading
845
+ };
846
+ });
847
+ }
848
+ /**
849
+ * Whether the park has already answered for this header: with nothing
850
+ * registered, no session can be paused, so a header that says this session is
851
+ * running carries the whole fact and the read that would confirm it is a host
852
+ * round-trip plus two records in that session's log for nothing.
853
+ *
854
+ * A header the host last showed on its way to a park is never answered this
855
+ * way: that reading is newer than the aggregate the derivation rests on, and
856
+ * only a fresh host answer may replace it.
857
+ */
858
+ answerIsKnown(sessionId) {
859
+ const phase = this.store.getSnapshot().bySession[String(sessionId)]?.phase;
860
+ if (phase !== void 0 && phase !== "active") return false;
861
+ return this.nothingParked();
862
+ }
863
+ /**
864
+ * Whether the whole-app answer says the park holds nothing at all: the one
865
+ * predicate the derivation below and its revocation share.
866
+ */
867
+ nothingParked() {
868
+ const aggregate = this.aggregate();
869
+ return aggregate?.kind === "reported" && aggregate.status.registered === 0;
870
+ }
871
+ /**
872
+ * Answer one header from the whole-app verdict, and remember that it was that
873
+ * kind of answer: the sessions to read for real are exactly the ones the
874
+ * aggregate may have been wrong about.
875
+ */
876
+ deriveActive(sessionId) {
877
+ this.derived.add(sessionId);
878
+ this.publish(sessionId, {
879
+ phase: "active",
880
+ runningTools: []
881
+ });
882
+ }
883
+ /**
884
+ * The whole-app answer moved. Whatever was answered from "nothing is
885
+ * registered" is read for real the moment that stops holding — the session a
886
+ * header stood in for may be the one the park just took, possibly parked from
887
+ * another window whose change this browser only sees as the forwarded event.
888
+ * Called on every published aggregate, so it must do nothing while the
889
+ * derivation still holds.
890
+ */
891
+ reconsider() {
892
+ if (this.derived.size === 0 || this.lifetime.aborted || this.nothingParked()) return;
893
+ for (const sessionId of this.derived) this.owed.add(sessionId);
894
+ this.derived.clear();
895
+ this.burst.next();
896
+ }
897
+ /**
898
+ * Decide what the burst owes after a read: another look while a change this
899
+ * browser has not answered is still outstanding, and nothing once none is.
900
+ *
901
+ * A phase that is still moving is left alone. The host announces every
902
+ * transition on its command channel — the flush that settles a park included,
903
+ * which changes no registration of its own — so a read that finds `draining`
904
+ * or `parking` is the answer to the transition that produced it, and watching
905
+ * that phase from here would spend reads on a move that is going to speak for
906
+ * itself. What the burst still owes is the delivery of an announcement: a read
907
+ * still armed when the change arrived, and a read for a change that landed
908
+ * while the last pass was in flight.
909
+ *
910
+ * The one transition that cannot speak for itself is the one this browser's
911
+ * own echo window swallowed: a press reads the host, the park moves while the
912
+ * window is still open, and the changes that would have announced it look
913
+ * exactly like the press's own registrations. So an action that left a phase
914
+ * still moving asks for one look when that window closes, and only then — the
915
+ * read is owed at a moment, not watched. Once nothing is owed, the last
916
+ * reading stays on screen; the next change, the next user action, or the shell
917
+ * handing this control a session again is what asks next.
918
+ */
919
+ follow() {
920
+ if (this.lifetime.aborted) {
921
+ this.burst.stop();
922
+ return;
923
+ }
924
+ if (this.owed.size === 0) {
925
+ const hidden = this.burst.absorbing && this.moving();
926
+ if (hidden) this.burst.recheckAfterEcho();
927
+ this.trailing = hidden;
928
+ this.burst.stop();
929
+ return;
930
+ }
931
+ this.burst.next();
932
+ }
933
+ /**
934
+ * Whether any shown session's last reading is a phase the host has not
935
+ * finished walking — the only states an action's own echo could be hiding a
936
+ * transition behind.
937
+ */
938
+ moving() {
939
+ const shown = this.store.getSnapshot().bySession;
940
+ for (const sessionId of this.shown) {
941
+ const phase = shown[String(sessionId)]?.phase;
942
+ if (phase !== void 0 && MOVING.has(phase)) return true;
943
+ }
944
+ return false;
945
+ }
946
+ /**
947
+ * Follow the page in and out of view. A hidden page is not being read — and
948
+ * the read the burst was about to take is not lost, it waits for the page to
949
+ * come back.
950
+ */
951
+ watchVisibility() {
952
+ this.burst.checkVisibility();
953
+ }
954
+ };
955
+ //#endregion
956
+ //#region lib/types/client/view.js
957
+ /**
958
+ * The controls' phase → view mapping.
959
+ *
960
+ * Kept pure functions of the published reading so the rendering decision — what
961
+ * the button says, whether it accepts a press, and which state label is visible
962
+ * — is asserted without a browser renderer. Labels are KEYS, not copy: the
963
+ * visible sentence is localized, and the host's own names join it as data.
964
+ * @module dsh-pause-session/client/view
965
+ */
966
+ /**
967
+ * Whether the pause control renders on one session at all.
968
+ *
969
+ * The control is the user's button for a session's work, and that work includes
970
+ * the subagents under it — which is why the press covers the tree and why the
971
+ * button does not belong inside the tree. The client can tell: a Session
972
+ * snapshot carries a `subagent` field, the same signal the read-only composer
973
+ * reads. A subagent's service is not the user's to pause: its parent's control
974
+ * already speaks for it, and a second button here would offer to stop "a
975
+ * session" from inside a child.
976
+ * @param subagent - the session snapshot's own subagent fact, absent when unknown.
977
+ * @returns whether this session's header renders the control.
978
+ */
979
+ function showsPauseControl(subagent) {
980
+ return subagent === null || subagent === void 0;
981
+ }
982
+ /**
983
+ * One reading's goal opt-out, as the view carries it. Only an answered flag
984
+ * becomes a field: a missing one must stay missing through the mapping, or the
985
+ * renderer would show the product default as if the host had said it.
986
+ * @param reading - the host's answer.
987
+ * @returns the field to spread into the view, empty when the host said nothing.
988
+ */
989
+ function goalOf(reading) {
990
+ return reading.rearmGoal === void 0 ? {} : { rearmGoal: reading.rearmGoal };
991
+ }
992
+ /**
993
+ * Map one reading to its view.
994
+ * @param reading - the host-reported reading, or `undefined` before the first answer.
995
+ * @returns the view for that reading.
996
+ */
997
+ function pauseView(reading) {
998
+ switch (reading?.phase) {
999
+ case "active": return {
1000
+ paused: false,
1001
+ enabled: true,
1002
+ ariaKey: "pause.aria",
1003
+ runningTools: []
1004
+ };
1005
+ case "draining": return {
1006
+ paused: true,
1007
+ enabled: true,
1008
+ ariaKey: "draining.aria",
1009
+ labelKey: "state.draining",
1010
+ runningTools: reading.runningTools,
1011
+ ...goalOf(reading)
1012
+ };
1013
+ case "parking": return {
1014
+ paused: true,
1015
+ enabled: true,
1016
+ ariaKey: "parking.aria",
1017
+ labelKey: "state.parking",
1018
+ runningTools: [],
1019
+ ...goalOf(reading)
1020
+ };
1021
+ case "safe": return {
1022
+ paused: true,
1023
+ enabled: true,
1024
+ ariaKey: "safe.aria",
1025
+ labelKey: "state.safe",
1026
+ runningTools: [],
1027
+ ...goalOf(reading)
1028
+ };
1029
+ case "flush-failed": return {
1030
+ paused: true,
1031
+ enabled: true,
1032
+ ariaKey: "flushFailed.aria",
1033
+ labelKey: "state.flushFailed",
1034
+ runningTools: [],
1035
+ ...goalOf(reading)
1036
+ };
1037
+ case "unavailable": return {
1038
+ paused: false,
1039
+ enabled: false,
1040
+ ariaKey: "unavailable.aria",
1041
+ runningTools: []
1042
+ };
1043
+ case void 0: return {
1044
+ paused: false,
1045
+ enabled: false,
1046
+ ariaKey: "checking.aria",
1047
+ runningTools: []
1048
+ };
1049
+ }
1050
+ }
1051
+ /** The four decisions, in the order a user reads them: least to most cautious. */
1052
+ const TRIAGE_ACTION_VIEWS = [
1053
+ {
1054
+ action: "continue",
1055
+ labelKey: "triage.continue",
1056
+ ariaKey: "triage.continue.aria"
1057
+ },
1058
+ {
1059
+ action: "note",
1060
+ labelKey: "triage.note",
1061
+ ariaKey: "triage.note.aria"
1062
+ },
1063
+ {
1064
+ action: "review",
1065
+ labelKey: "triage.review",
1066
+ ariaKey: "triage.review.aria"
1067
+ },
1068
+ {
1069
+ action: "leave",
1070
+ labelKey: "triage.leave",
1071
+ ariaKey: "triage.leave.aria"
1072
+ }
1073
+ ];
1074
+ /**
1075
+ * The fifth listed verb, rendered apart from the four decisions: it is a
1076
+ * switch on what the NEXT resume may spend, not a way to decide this row.
1077
+ */
1078
+ const TRIAGE_GOAL_VIEW = {
1079
+ action: "goal",
1080
+ labelKey: "triage.goal",
1081
+ ariaKey: "triage.goal.aria"
1082
+ };
1083
+ /** The word each non-clean phase is shown as. */
1084
+ const PHASE_KEY = {
1085
+ draining: "triage.phase.draining",
1086
+ parking: "triage.phase.parking",
1087
+ "flush-failed": "triage.phase.flushFailed"
1088
+ };
1089
+ /**
1090
+ * Map one decision-list reading to its view.
1091
+ * @param reading - the host-reported list, or `undefined` before the panel asked.
1092
+ * @returns the rows as the panel renders them.
1093
+ */
1094
+ function triageView(reading) {
1095
+ if (reading === void 0 || reading.kind === "unavailable") return {
1096
+ visible: false,
1097
+ rows: []
1098
+ };
1099
+ return {
1100
+ visible: true,
1101
+ rows: reading.rows.map((row) => ({
1102
+ sessionId: row.sessionId,
1103
+ phaseKey: PHASE_KEY[row.phase],
1104
+ ...row.lastEvent === void 0 ? {} : { stopped: row.turn === void 0 ? row.lastEvent : `${row.lastEvent} · turn ${String(row.turn)}${row.step === void 0 ? "" : ` step ${String(row.step)}`}` },
1105
+ unknownTools: row.traces.map((trace) => trace.tool),
1106
+ wakeable: row.wakeable,
1107
+ readable: row.reading === "read",
1108
+ rearmGoal: row.rearmGoal
1109
+ }))
1110
+ };
1111
+ }
1112
+ /**
1113
+ * Map one whole-app reading to its view.
1114
+ * @param reading - the host-reported reading, or `undefined` before the first answer.
1115
+ * @returns the view for that reading.
1116
+ */
1117
+ function globalPauseView(reading) {
1118
+ if (reading === void 0) return {
1119
+ visible: false,
1120
+ enabled: false,
1121
+ ariaKey: "all.checking.aria",
1122
+ action: "none",
1123
+ detail: "",
1124
+ caveats: [],
1125
+ unclean: 0
1126
+ };
1127
+ if (reading.kind === "unavailable") return {
1128
+ visible: false,
1129
+ enabled: false,
1130
+ ariaKey: "unavailable.aria",
1131
+ action: "none",
1132
+ detail: "",
1133
+ caveats: [],
1134
+ unclean: 0
1135
+ };
1136
+ const { status } = reading;
1137
+ const caveats = [];
1138
+ if (status.blind.includes("jobs")) caveats.push({
1139
+ key: "all.caveat.blindJobs",
1140
+ names: []
1141
+ });
1142
+ else if (status.jobs.length > 0) caveats.push({
1143
+ key: "all.caveat.jobs",
1144
+ names: status.jobs
1145
+ });
1146
+ if (status.blind.includes("trees")) caveats.push({
1147
+ key: "all.caveat.blindTrees",
1148
+ names: []
1149
+ });
1150
+ else if (status.unreadTrees.length > 0) caveats.push({
1151
+ key: "all.caveat.unread",
1152
+ names: status.unreadTrees
1153
+ });
1154
+ if (status.unresumable.length > 0) caveats.push({
1155
+ key: "all.caveat.unresumable",
1156
+ names: status.unresumable
1157
+ });
1158
+ const verdict = status.verdict;
1159
+ if (verdict !== "safe" && status.stalled.length > 0) caveats.push({
1160
+ key: "all.caveat.stalled",
1161
+ names: status.stalled
1162
+ });
1163
+ const base = {
1164
+ visible: true,
1165
+ enabled: true,
1166
+ caveats,
1167
+ unclean: status.stalled.length
1168
+ };
1169
+ switch (verdict) {
1170
+ case "idle": return {
1171
+ ...base,
1172
+ ariaKey: "all.pause.aria",
1173
+ action: "pause-all",
1174
+ detail: ""
1175
+ };
1176
+ case "safe": return {
1177
+ ...base,
1178
+ ariaKey: "all.resume.aria",
1179
+ action: "resume-all",
1180
+ labelKey: "all.safe",
1181
+ detail: `${String(status.safe)}/${String(status.registered)}`
1182
+ };
1183
+ case "waiting": return status.draining.length > 0 || status.parking > 0 ? {
1184
+ ...base,
1185
+ ariaKey: "all.resume.aria",
1186
+ action: "resume-all",
1187
+ labelKey: "all.finishing",
1188
+ detail: `${String(status.safe)}/${String(status.registered)}`
1189
+ } : {
1190
+ ...base,
1191
+ enabled: false,
1192
+ ariaKey: "all.unclean.aria",
1193
+ action: "none",
1194
+ labelKey: "all.unclean",
1195
+ detail: String(status.stalled.length)
1196
+ };
1197
+ }
1198
+ }
1199
+ //#endregion
1200
+ //#region lib/types/client/global.js
1201
+ /**
1202
+ * Browser-side reading and driving of the host's whole-app park state.
1203
+ *
1204
+ * The same one live channel the per-session control uses — a session command
1205
+ * run through `ctx.remote.commands.execute`, plus the forwarded
1206
+ * `commands/change` event as the only notification — carries the aggregate
1207
+ * here. There is no push channel a third-party plugin can add, so the browser
1208
+ * asks: `pause-state-all` is registered globally and its RESULT carries the
1209
+ * whole-app status as of the read.
1210
+ *
1211
+ * The command channel is session-addressed, so the control needs one session
1212
+ * to talk through. It picks a user session the host can serve (never a
1213
+ * subagent's own session), preferring the one on screen; with no session at
1214
+ * all there is nothing to ask, and the control claims nothing rather than
1215
+ * inventing an answer. Once the host's own answer names the session the park
1216
+ * belongs to, reads are addressed there instead: a read writes two records into
1217
+ * the address session's transcript, and they belong in the session being parked
1218
+ * rather than in whatever the user happens to be looking at. That name outlives
1219
+ * the phases — a session that drained and parked cleanly names nothing else — so
1220
+ * a clean park is addressed like a moving one. Failing that, the session the
1221
+ * last answer reported as registered is remembered, then the one a read was last
1222
+ * served through — an address change is the user looking somewhere else, not
1223
+ * news about the park, and a park that has emptied out is no reason to start
1224
+ * writing rows into every session the user opens next. An address that stops
1225
+ * answering (its agent is gone) is forgotten and the read falls back to the next
1226
+ * one, the session the shell asks with last, so a stale address costs an honest
1227
+ * answer rather than a control that can never speak again.
1228
+ *
1229
+ * Every phase a session reaches announces itself through the registry — a
1230
+ * transition re-registers that session's status verb — so a change is what buys
1231
+ * the next read, and the flush that settles a park is announced like every other
1232
+ * step (`./burst.ts`). A read that finds work still in flight is therefore the
1233
+ * answer to the transition that produced it rather than a reason to keep asking;
1234
+ * what the burst still owes is the delivery of a change, and the rows of a panel
1235
+ * whose own read did not answer. A settled answer is left on screen unread: the
1236
+ * panel stays readable and the pill keeps saying what it says, but the browser
1237
+ * stops spending host round-trips and log records on a state that has stopped
1238
+ * moving. What owes a read is the host's forwarded registry change, the user's
1239
+ * own press or decision, and the first read at mount; once an answer is on
1240
+ * screen, the shell handing this control another session to ask through is none
1241
+ * of those. A press, a decision and an opened panel each read the host as they
1242
+ * run, and the changes they set off are that action's own echo: those are
1243
+ * absorbed for a short window (`./burst.ts`), so a forwarded change owes a read
1244
+ * exactly when something moved that this browser did not do — with one look owed
1245
+ * at the close of that window when the reading the action left is still moving,
1246
+ * which is how a park that reaches its final phase inside the echo window is
1247
+ * still read.
1248
+ * @module dsh-pause-session/client/global
1249
+ */
1250
+ const INITIAL = {
1251
+ global: void 0,
1252
+ panelOpen: false,
1253
+ triage: void 0
1254
+ };
1255
+ /** The command one listed verb runs, named from the one vocabulary the host registers. */
1256
+ const TRIAGE_DECISION_COMMAND = {
1257
+ continue: TRIAGE_CONTINUE_COMMAND,
1258
+ note: TRIAGE_NOTE_COMMAND,
1259
+ review: TRIAGE_REVIEW_COMMAND,
1260
+ leave: TRIAGE_LEAVE_COMMAND,
1261
+ goal: TRIAGE_GOAL_COMMAND
1262
+ };
1263
+ /**
1264
+ * The session a whole-app command is addressed to.
1265
+ *
1266
+ * A subagent's own session is never addressed: its agent belongs to a parent
1267
+ * and is not the host's business to drive from the shell. Among the rest, the
1268
+ * one on screen is the best address (its agent is live by construction), then
1269
+ * any session the host reports as running, then any at all.
1270
+ * @param state - the session list snapshot the shell provides.
1271
+ * @returns the address, or `undefined` when the app has no session to ask through.
1272
+ */
1273
+ function addressingSession(state) {
1274
+ const usable = state.ids.filter((id) => {
1275
+ const row = state.byId[String(id)];
1276
+ return row !== void 0 && row.origin !== "subagent" && row.parentId === void 0;
1277
+ });
1278
+ return usable.find((id) => (state.byId[String(id)]?.retainedBy?.mainView ?? 0) > 0) ?? usable.find((id) => state.byId[String(id)]?.running === true) ?? usable[0];
1279
+ }
1280
+ /**
1281
+ * The registered sessions one whole-app answer names, best address first: the
1282
+ * one still draining is the one this host is executing against right now, the
1283
+ * rest is the host's own vocabulary for a registered session it is not going to
1284
+ * move any further.
1285
+ *
1286
+ * A stalled or unreadable record is deliberately not an address for a read —
1287
+ * this host may not hold that session at all, and a read that could not be
1288
+ * delivered would report "unavailable" for a transport reason rather than a
1289
+ * state — but it is still a registered session, which is all the remembered
1290
+ * address is for. The host's own answer is JSON: the branded ids arrive as the
1291
+ * strings they were encoded as, exactly like the ids the shell hands the pill.
1292
+ * @param status - the host's last whole-app answer.
1293
+ * @returns the registered sessions the answer names, best first.
1294
+ */
1295
+ function registeredSessions(status) {
1296
+ return [
1297
+ ...status.draining,
1298
+ ...status.stalled,
1299
+ ...status.unresumable,
1300
+ ...status.unreadTrees
1301
+ ];
1302
+ }
1303
+ /**
1304
+ * The session a registered park owns, when the host's answer names one that is
1305
+ * still moving. This is how a read stops landing in whatever session the user
1306
+ * happens to be looking at.
1307
+ * @param status - the host's last whole-app answer.
1308
+ * @returns a draining session, or `undefined` when the answer names none.
1309
+ */
1310
+ function registeredAddress(status) {
1311
+ const draining = status.draining[0];
1312
+ return draining === void 0 ? void 0 : draining;
1313
+ }
1314
+ /**
1315
+ * The session the host names as the park's own, whatever phase the park
1316
+ * reached — the address a whole-app read belongs at.
1317
+ *
1318
+ * This is the fact {@link registeredAddress} cannot carry once a park is clean:
1319
+ * a session that reached `safe` has nothing left to do, so no bucket names it,
1320
+ * and a reader in another window would fall back to whatever session it is
1321
+ * showing. An answer written before this field existed names no session here:
1322
+ * an absent address, never one to guess at.
1323
+ * @param status - the host's last whole-app answer.
1324
+ * @returns the session the park belongs to, or `undefined` when the answer names none.
1325
+ */
1326
+ function parkedAddress(status) {
1327
+ return status.parkedAt === void 0 ? void 0 : status.parkedAt;
1328
+ }
1329
+ /** Owns the published whole-app reading; nothing else writes that state. */
1330
+ var PauseGlobalControl = class {
1331
+ commands;
1332
+ lifetime;
1333
+ /** State source the whole-app contribution binds its selector hook to. */
1334
+ store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(INITIAL);
1335
+ /** The session the shell asked this control through; reads start here. */
1336
+ address;
1337
+ /**
1338
+ * The last session an answer reported as registered — the address a read
1339
+ * falls back to when the host's own answer names no parked session, which is
1340
+ * every reading written before that field existed.
1341
+ *
1342
+ * The buckets that feed it name only what is still moving, so they say
1343
+ * nothing about a park that finished cleanly; the park's own name is the live
1344
+ * fact, and this memory is what keeps the rows in the park's transcript for an
1345
+ * answer that predates it. An empty registry does not clear it: a park whose
1346
+ * sessions stopped moving has not moved out of its sessions.
1347
+ */
1348
+ lastRegistered;
1349
+ /**
1350
+ * The session an aggregate read was last served through. Later reads keep
1351
+ * going there: every read is two records in the address session's log, and
1352
+ * the shell's address follows whatever the user is looking at, which is not
1353
+ * where a row about the park belongs.
1354
+ */
1355
+ served;
1356
+ /** Highest decision-list read requested: an answer older than it is dropped. */
1357
+ latestTriage = 0;
1358
+ /** Highest read sequence requested: an answer older than it is dropped. */
1359
+ latest = 0;
1360
+ /**
1361
+ * Whether a registry change has arrived that no completed read pass has
1362
+ * answered. Set only for a change that is news — an absorbed echo of this
1363
+ * browser's own action is already answered by that action's read.
1364
+ */
1365
+ owed = false;
1366
+ /** The bounded burst of reads one transition buys; see `./burst.ts`. */
1367
+ burst;
1368
+ /**
1369
+ * @param commands - the Client-callable command namespace.
1370
+ * @param lifetime - aborts in-flight calls and the burst when the plugin unloads.
1371
+ * @param windowMs - how long the burst collects the registrations of one
1372
+ * transition before its first read; also the wait after a press.
1373
+ * @param policy - the burst waits and visibility source; defaults to the real page.
1374
+ */
1375
+ constructor(commands, lifetime, windowMs = 400, policy = DEFAULT_BURST_POLICY) {
1376
+ this.commands = commands;
1377
+ this.lifetime = lifetime;
1378
+ this.burst = new ReadBurst(() => {
1379
+ this.readAll();
1380
+ }, {
1381
+ ...policy,
1382
+ windowMs,
1383
+ waitsMs: burstWaits(windowMs)
1384
+ });
1385
+ }
1386
+ /**
1387
+ * The shell handed this control a session: the one the pill mounted on, or
1388
+ * the one the user has just moved to. Only a control with no answer yet reads
1389
+ * here — the view moving is not the park moving, and after the first answer
1390
+ * the forwarded registry change and the user's own press are what ask again.
1391
+ * A read that came back with nothing is not an answer, so moving to another
1392
+ * session is one more chance to reach the host rather than a control that
1393
+ * stays mute until something else happens to change. The address is kept
1394
+ * either way: it is where this control's presses and its decision list go, and
1395
+ * the fallback a remembered address falls back to.
1396
+ * @param sessionId - the session the shell asks through.
1397
+ * @returns after this read's answer is published or superseded by a newer one.
1398
+ */
1399
+ async refresh(sessionId) {
1400
+ this.address = sessionId;
1401
+ if (this.answered()) return;
1402
+ await this.readAggregate();
1403
+ this.follow();
1404
+ }
1405
+ /** Whether the host has answered: a state, not the absence of one it never sent. */
1406
+ answered() {
1407
+ return this.store.getSnapshot().global?.kind === "reported";
1408
+ }
1409
+ /**
1410
+ * The host's command registry changed: something may have moved, so a burst
1411
+ * is owed. The reads run on the burst's own schedule rather than here, which
1412
+ * is what coalesces a tick's worth of changes — parking N sessions announces
1413
+ * three registrations each, and a phase change re-registers one more — into
1414
+ * one burst. A change that is this browser's own action's echo owes nothing:
1415
+ * the action read the host as it ran.
1416
+ */
1417
+ refreshAll() {
1418
+ if (this.address === void 0 || this.lifetime.aborted) return;
1419
+ if (!this.burst.trigger()) return;
1420
+ this.owed = true;
1421
+ }
1422
+ /**
1423
+ * One read of the whole screen: the aggregate, plus the decision list while
1424
+ * the user has it open. A closed panel spends no command calls on rows nobody
1425
+ * is reading.
1426
+ */
1427
+ async readAll() {
1428
+ if (this.target() === void 0 || this.lifetime.aborted) {
1429
+ this.owed = false;
1430
+ this.burst.stop();
1431
+ return;
1432
+ }
1433
+ this.owed = false;
1434
+ await this.readAggregate();
1435
+ if (this.store.getSnapshot().panelOpen) await this.readTriage();
1436
+ this.follow();
1437
+ }
1438
+ /**
1439
+ * The session a read goes through: the one the park is still draining, else
1440
+ * the one the host names as the park's own, else the one a read was last
1441
+ * served through, else the session the shell is on now. The park's own address
1442
+ * comes first because a read's rows land in the transcript it was taken
1443
+ * through, and the park — not the reader — is what they are about; its name
1444
+ * survives the clean case, where every bucket above is empty and a park that
1445
+ * finished would otherwise have no address at all.
1446
+ * @param skip - an address this read already tried and could not reach. The
1447
+ * fallback is the next candidate rather than the same one again.
1448
+ */
1449
+ target(skip) {
1450
+ const current = this.store.getSnapshot().global;
1451
+ const named = current?.kind === "reported" ? current.status : void 0;
1452
+ return [
1453
+ named === void 0 ? void 0 : registeredAddress(named),
1454
+ named === void 0 ? void 0 : parkedAddress(named),
1455
+ this.lastRegistered,
1456
+ this.served,
1457
+ this.address
1458
+ ].find((candidate) => candidate !== void 0 && candidate !== skip);
1459
+ }
1460
+ /**
1461
+ * Ask the host for the aggregate through the session a read goes through,
1462
+ * with the one fallback an address that stopped answering is worth: the
1463
+ * remembered address is forgotten, and the read is taken again through the
1464
+ * next one — the session the shell is on at the latest. A memory of a session
1465
+ * this host no longer holds would otherwise leave the control unable to say
1466
+ * anything at all, and a single retry is the whole price of keeping it honest.
1467
+ */
1468
+ async readAggregate() {
1469
+ const address = this.target();
1470
+ if (address === void 0) return;
1471
+ if (!await this.readAt(address)) return;
1472
+ const fallback = this.target(address);
1473
+ if (fallback === void 0) return;
1474
+ await this.readAt(fallback);
1475
+ }
1476
+ /**
1477
+ * Publish one aggregate answer, unless a newer read was requested while this
1478
+ * one was in flight: that answer would show a state the host has moved past.
1479
+ * A served address is remembered, and one that did not answer is forgotten —
1480
+ * that is what makes the read fall back to the next address rather than ask
1481
+ * it again.
1482
+ * @param sessionId - the session this read goes through.
1483
+ * @returns whether this read came back without an answer; a superseded read is
1484
+ * not one, because the newer read already owns the outcome.
1485
+ */
1486
+ async readAt(sessionId) {
1487
+ const sequence = ++this.latest;
1488
+ const reading = await this.read(sessionId);
1489
+ if (this.latest !== sequence) return false;
1490
+ if (reading.kind === "reported") {
1491
+ this.served = sessionId;
1492
+ const registered = registeredSessions(reading.status)[0];
1493
+ if (registered !== void 0) this.lastRegistered = registered;
1494
+ } else {
1495
+ if (this.served === sessionId) this.served = void 0;
1496
+ if (this.lastRegistered === sessionId) this.lastRegistered = void 0;
1497
+ }
1498
+ this.publish(reading);
1499
+ return reading.kind === "unavailable";
1500
+ }
1501
+ /**
1502
+ * Open or close the decision list. Opening asks the host for the rows; there
1503
+ * is no state to show without that answer. Opening is also a client action
1504
+ * that reads, so the changes it sets off are absorbed rather than read back,
1505
+ * and whether the burst owes anything is what its own read decides.
1506
+ * @param open - whether the panel should be showing.
1507
+ */
1508
+ setPanel(open) {
1509
+ this.store.update((state) => {
1510
+ state.panelOpen = open;
1511
+ });
1512
+ if (open) {
1513
+ this.burst.absorb();
1514
+ this.refreshTriage();
1515
+ return;
1516
+ }
1517
+ this.follow();
1518
+ }
1519
+ /**
1520
+ * Ask the host for the decision list through the session the user is on. The
1521
+ * list is recorded per calling session and the decisions act only on rows
1522
+ * that session was shown, so this read — unlike the aggregate — stays on the
1523
+ * shell's own address.
1524
+ */
1525
+ async refreshTriage() {
1526
+ await this.readTriage();
1527
+ this.follow();
1528
+ }
1529
+ /**
1530
+ * Read the aggregate again because the user asked this control to act: the
1531
+ * address is the session the shell is on, since a press is a user action on
1532
+ * the session the user is looking at, and the host's answer is what the
1533
+ * control shows next.
1534
+ * @param sessionId - the session the shell is on.
1535
+ */
1536
+ async reread(sessionId) {
1537
+ this.address = sessionId;
1538
+ await this.readAggregate();
1539
+ this.follow();
1540
+ }
1541
+ /** One decision-list read, addressed at the session the user is on. */
1542
+ async readTriage() {
1543
+ const address = this.address;
1544
+ if (address === void 0) return;
1545
+ const sequence = ++this.latestTriage;
1546
+ const reading = await this.readTriageRows(address);
1547
+ if (this.latestTriage !== sequence) return;
1548
+ this.store.update((state) => {
1549
+ state.triage = reading;
1550
+ });
1551
+ }
1552
+ /**
1553
+ * Take one listed verb on one listed session, addressed through the session
1554
+ * the user is on. The list is read first, because the host acts only on rows
1555
+ * it has shown the calling session, and read again afterwards because the
1556
+ * host's answer — not this control's assumption — is what the next render
1557
+ * shows: a decision can release a session, and the goal switch changes what
1558
+ * the row itself says.
1559
+ * @param sessionId - the listed session to act on.
1560
+ * @param action - which listed verb was taken.
1561
+ */
1562
+ async decide(sessionId, action) {
1563
+ const address = this.address;
1564
+ if (address === void 0) return;
1565
+ this.burst.absorb();
1566
+ await this.refreshTriage();
1567
+ await this.commands.execute(address, `/${TRIAGE_DECISION_COMMAND[action]} ${sessionId}`, [], this.lifetime);
1568
+ await this.refreshTriage();
1569
+ await this.reread(address);
1570
+ }
1571
+ /**
1572
+ * Press the pill: park everything, or release everything the park owns.
1573
+ * Which one it is comes from the host's own last answer, never from a local
1574
+ * flip; the state shown afterwards is the host's answer again. A reading the
1575
+ * view does not turn into a verb — every registered session is one the host
1576
+ * will not wake — is not pressed at all.
1577
+ * @param sessionId - the session to address the verb to.
1578
+ * @returns after the control re-reads the host.
1579
+ */
1580
+ async toggle(sessionId) {
1581
+ const current = this.store.getSnapshot().global;
1582
+ if (current === void 0 || current.kind !== "reported") return;
1583
+ const action = globalPauseView(current).action;
1584
+ if (action === "none") return;
1585
+ const line = action === "pause-all" ? `/${PAUSE_ALL_COMMAND}` : `/${RESUME_ALL_COMMAND}`;
1586
+ this.burst.absorb();
1587
+ if (!(await this.commands.execute(sessionId, line, [], this.lifetime)).ok) {
1588
+ this.publish({ kind: "unavailable" });
1589
+ this.follow();
1590
+ return;
1591
+ }
1592
+ await this.reread(sessionId);
1593
+ }
1594
+ /** Ask the host for the decision list, in the browser's vocabulary. */
1595
+ async readTriageRows(sessionId) {
1596
+ const answered = await this.commands.execute(sessionId, `/${TRIAGE_COMMAND}`, [], this.lifetime);
1597
+ if (!answered.ok || answered.value === void 0) return { kind: "unavailable" };
1598
+ const text = answered.value.result.kind === "success" ? answered.value.result.text : void 0;
1599
+ const list = text === void 0 ? void 0 : decodeTriageList(text);
1600
+ return list === void 0 ? { kind: "unavailable" } : {
1601
+ kind: "reported",
1602
+ rows: list.rows
1603
+ };
1604
+ }
1605
+ /** Ask the host what the whole-app park state is, in the browser's vocabulary. */
1606
+ async read(sessionId) {
1607
+ const answered = await this.commands.execute(sessionId, `/${STATE_ALL_COMMAND}`, [], this.lifetime);
1608
+ if (!answered.ok || answered.value === void 0) return { kind: "unavailable" };
1609
+ const text = answered.value.result.kind === "success" ? answered.value.result.text : void 0;
1610
+ const status = text === void 0 ? void 0 : decodeGlobalStatus(text);
1611
+ if (status === void 0) return { kind: "unavailable" };
1612
+ return {
1613
+ kind: "reported",
1614
+ status
1615
+ };
1616
+ }
1617
+ publish(reading) {
1618
+ this.store.update((state) => {
1619
+ state.global = reading;
1620
+ });
1621
+ }
1622
+ /**
1623
+ * Decide what the burst owes after a read: another look while a change this
1624
+ * browser has not answered is outstanding, or while the open panel is still
1625
+ * owed rows, and nothing once neither holds.
1626
+ *
1627
+ * A registered session standing at the park point is left alone: the flush
1628
+ * that settles it announces itself on the command channel like every other
1629
+ * transition, so watching it from here would spend reads on a move that is
1630
+ * going to speak for itself. A verdict that stays `waiting` because a record
1631
+ * is stalled, unresumable or unread is not settling either: nothing in this
1632
+ * process will change it, and a session that starts moving again registers its
1633
+ * markers, which is what asks next.
1634
+ *
1635
+ * The one transition that cannot speak for itself is the one this browser's
1636
+ * own echo window swallowed: a press reads the host, the park reaches its
1637
+ * final phase while the window is still open, and the changes that would have
1638
+ * announced it look exactly like the press's own registrations. An action that
1639
+ * left the park moving therefore asks for one look when that window closes,
1640
+ * and only then. Once nothing is owed, the last reading stays published — the
1641
+ * verdict, the pill and the rows are exactly what the host last reported.
1642
+ */
1643
+ follow() {
1644
+ if (this.lifetime.aborted) {
1645
+ this.burst.stop();
1646
+ return;
1647
+ }
1648
+ if (!this.owed && !this.unansweredPanel()) {
1649
+ if (this.burst.absorbing && this.moving()) this.burst.recheckAfterEcho();
1650
+ this.burst.stop();
1651
+ return;
1652
+ }
1653
+ this.burst.next();
1654
+ }
1655
+ /**
1656
+ * Whether the park this control last read still has work on its way — the only
1657
+ * state an action's own echo could be hiding a transition behind. A record
1658
+ * that is stalled, unresumable or unread is not moving: nothing in this
1659
+ * process will finish it.
1660
+ */
1661
+ moving() {
1662
+ const current = this.store.getSnapshot().global;
1663
+ return current?.kind === "reported" && (current.status.draining.length > 0 || current.status.parking > 0);
1664
+ }
1665
+ /**
1666
+ * Whether the open panel is still owed its rows. Opening the panel reads them,
1667
+ * so this holds only after a read that did not answer — the one debt no change
1668
+ * created, and the one place a retry is the difference between an empty panel
1669
+ * and the facts the host has. It is bounded by the burst like everything else.
1670
+ */
1671
+ unansweredPanel() {
1672
+ const state = this.store.getSnapshot();
1673
+ return state.panelOpen && (state.triage === void 0 || state.triage.kind === "unavailable");
1674
+ }
1675
+ /**
1676
+ * Follow the page in and out of view. A hidden page is not being read — and
1677
+ * the read the burst was about to take is not lost, it waits for the page to
1678
+ * come back.
1679
+ */
1680
+ watchVisibility() {
1681
+ this.burst.checkVisibility();
1682
+ }
1683
+ };
1684
+ //#endregion
1685
+ //#region lib/types/client/icons.js
1686
+ /** 16px stroke icon (currentColor): the two bars of a pause. */
1687
+ function PauseIcon() {
1688
+ return (0, react_jsx_runtime.jsxs)("svg", {
1689
+ "aria-hidden": "true",
1690
+ width: 16,
1691
+ height: 16,
1692
+ viewBox: "0 0 24 24",
1693
+ fill: "none",
1694
+ stroke: "currentColor",
1695
+ strokeWidth: 2,
1696
+ strokeLinecap: "round",
1697
+ strokeLinejoin: "round",
1698
+ children: [(0, react_jsx_runtime.jsx)("path", { d: "M9 5v14" }), (0, react_jsx_runtime.jsx)("path", { d: "M15 5v14" })]
1699
+ });
1700
+ }
1701
+ /** 16px stroke icon (currentColor): the triangle of a resume. */
1702
+ function ResumeIcon() {
1703
+ return (0, react_jsx_runtime.jsx)("svg", {
1704
+ "aria-hidden": "true",
1705
+ width: 16,
1706
+ height: 16,
1707
+ viewBox: "0 0 24 24",
1708
+ fill: "none",
1709
+ stroke: "currentColor",
1710
+ strokeWidth: 2,
1711
+ strokeLinecap: "round",
1712
+ strokeLinejoin: "round",
1713
+ children: (0, react_jsx_runtime.jsx)("path", { d: "M7 4.5v15l12-7.5z" })
1714
+ });
1715
+ }
1716
+ /**
1717
+ * 16px stroke icon (currentColor): the rings of a target, for the goal switch
1718
+ * on the session header. Drawn at 16px like the pause and resume glyphs it sits
1719
+ * beside rather than the 14px marks the two reports use.
1720
+ */
1721
+ function GoalIcon() {
1722
+ return (0, react_jsx_runtime.jsxs)("svg", {
1723
+ "aria-hidden": "true",
1724
+ width: 16,
1725
+ height: 16,
1726
+ viewBox: "0 0 24 24",
1727
+ fill: "none",
1728
+ stroke: "currentColor",
1729
+ strokeWidth: 2,
1730
+ strokeLinecap: "round",
1731
+ strokeLinejoin: "round",
1732
+ children: [(0, react_jsx_runtime.jsx)("circle", {
1733
+ cx: "12",
1734
+ cy: "12",
1735
+ r: "8"
1736
+ }), (0, react_jsx_runtime.jsx)("circle", {
1737
+ cx: "12",
1738
+ cy: "12",
1739
+ r: "3"
1740
+ })]
1741
+ });
1742
+ }
1743
+ /**
1744
+ * 14px stroke icon (currentColor): the mark that a report carries a caveat.
1745
+ * A glyph rather than copy, so the facts it stands for stay in the locale
1746
+ * dictionaries beside everything else the user reads.
1747
+ */
1748
+ function CaveatIcon() {
1749
+ return (0, react_jsx_runtime.jsxs)("svg", {
1750
+ "aria-hidden": "true",
1751
+ width: 14,
1752
+ height: 14,
1753
+ viewBox: "0 0 24 24",
1754
+ fill: "none",
1755
+ stroke: "currentColor",
1756
+ strokeWidth: 2,
1757
+ strokeLinecap: "round",
1758
+ strokeLinejoin: "round",
1759
+ children: [
1760
+ (0, react_jsx_runtime.jsx)("circle", {
1761
+ cx: "12",
1762
+ cy: "12",
1763
+ r: "9"
1764
+ }),
1765
+ (0, react_jsx_runtime.jsx)("path", { d: "M12 7.5v5.5" }),
1766
+ (0, react_jsx_runtime.jsx)("path", { d: "M12 16.5v.5" })
1767
+ ]
1768
+ });
1769
+ }
1770
+ /** 14px stroke icon (currentColor): the chevron that opens the decision list. */
1771
+ function ChevronIcon() {
1772
+ return (0, react_jsx_runtime.jsx)("svg", {
1773
+ "aria-hidden": "true",
1774
+ width: 14,
1775
+ height: 14,
1776
+ viewBox: "0 0 24 24",
1777
+ fill: "none",
1778
+ stroke: "currentColor",
1779
+ strokeWidth: 2,
1780
+ strokeLinecap: "round",
1781
+ strokeLinejoin: "round",
1782
+ children: (0, react_jsx_runtime.jsx)("path", { d: "M6 15l6-6 6 6" })
1783
+ });
1784
+ }
1785
+ //#endregion
1786
+ //#region \0dsh-css:src/client/button.module.css.mjs
1787
+ const css = ".FCO1_q_dsp{align-items:center;display:inline-flex;position:relative}.FCO1_q_dspBtn{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;user-select:none;background:0 0;border:1px solid #0000;border-radius:6px;justify-content:center;align-items:center;padding:0;transition:color .12s,background .12s,border-color .12s;display:inline-flex}.FCO1_q_dspBtn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l1)}.FCO1_q_dspBtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.FCO1_q_dspBtn:disabled{cursor:default;opacity:.5}.FCO1_q_dspBtn:disabled:hover{color:var(--dsw-alias-label-secondary);background:0 0;border-color:#0000}.FCO1_q_dspGoal{width:22px;height:22px;color:var(--dsw-alias-label-tertiary);cursor:pointer;user-select:none;background:0 0;border:1px solid #0000;border-radius:6px;justify-content:center;align-items:center;margin-left:2px;padding:0;transition:color .12s,background .12s,border-color .12s;display:inline-flex}.FCO1_q_dspGoal[aria-pressed=true]{color:var(--dsw-alias-label-secondary)}.FCO1_q_dspGoal:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l1)}.FCO1_q_dspGoal:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}@media (prefers-reduced-motion:reduce){.FCO1_q_dspGoal{transition:none}}.FCO1_q_dspState{color:var(--dsw-alias-label-secondary);white-space:nowrap;user-select:none;padding-left:6px;font-size:12px;line-height:18px}.FCO1_q_dspTip{z-index:20;max-width:min(46ch,100vw - 48px);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:nowrap;text-overflow:ellipsis;opacity:0;pointer-events:none;border-radius:6px;padding:4px 10px;font-size:12px;line-height:18px;transition:opacity .12s,transform .12s;position:absolute;top:calc(100% + 6px);left:0;overflow:hidden;transform:translateY(-2px);box-shadow:0 4px 12px #0000002e}.FCO1_q_dsp:hover .FCO1_q_dspTip,.FCO1_q_dspBtn:focus-visible+.FCO1_q_dspTip{opacity:1;transform:translateY(0)}@media (prefers-reduced-motion:reduce){.FCO1_q_dspBtn,.FCO1_q_dspTip{transition:none}}.FCO1_q_dspPill{align-items:center;display:inline-flex;position:relative}.FCO1_q_dspPillBtn{max-width:min(38ch,100vw - 48px);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);cursor:pointer;user-select:none;border-radius:999px;align-items:center;gap:6px;padding:5px 10px;font-size:12px;line-height:18px;transition:color .12s,background .12s,border-color .12s;display:inline-flex;box-shadow:0 4px 12px #00000024}.FCO1_q_dspPillBtn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}.FCO1_q_dspPillIcon{justify-content:center;width:28px;height:28px;padding:0}.FCO1_q_dspPillBtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.FCO1_q_dspPillBtn:disabled{cursor:default;opacity:.5}.FCO1_q_dspPillText{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.FCO1_q_dspPillDetail{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary)}.FCO1_q_dspPillWarn{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary));font-weight:600}@media (prefers-reduced-motion:reduce){.FCO1_q_dspPillBtn{transition:none}}.FCO1_q_dspPillTriage{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary));background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);cursor:pointer;user-select:none;border-radius:999px;align-items:center;gap:2px;margin-right:4px;padding:4px 8px;font-size:12px;line-height:18px;display:inline-flex;box-shadow:0 4px 12px #00000024}.FCO1_q_dspPillTriage:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}.FCO1_q_dspPillTriage:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.FCO1_q_dspPillPanel{z-index:30;text-align:left;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:8px;width:min(52ch,100vw - 48px);max-height:min(60vh,420px);padding:10px 12px;display:flex;position:absolute;top:calc(100% + 6px);right:0;overflow-y:auto;box-shadow:0 8px 24px #00000038}.FCO1_q_dspPillPanelHead{justify-content:space-between;align-items:center;gap:8px;font-weight:600;display:flex}.FCO1_q_dspPillPanelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}.FCO1_q_dspPillRows{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.FCO1_q_dspPillRow{border:1px solid var(--dsw-alias-border-l1);border-radius:6px;flex-direction:column;gap:4px;padding:8px;display:flex}.FCO1_q_dspPillRowHead{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.FCO1_q_dspPillRowId{word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.FCO1_q_dspPillRowPhase{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary));flex:none;font-size:12px}.FCO1_q_dspPillRowFacts{color:var(--dsw-alias-label-secondary);flex-direction:column;gap:2px;font-size:12px;line-height:18px;display:flex}.FCO1_q_dspPillRowActions{flex-wrap:wrap;gap:6px;padding-top:2px;display:flex}.FCO1_q_dspPillAction{color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);cursor:pointer;user-select:none;background:0 0;border-radius:4px;padding:3px 8px;font-size:12px;line-height:16px}.FCO1_q_dspPillAction:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}.FCO1_q_dspPillAction:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}";
1788
+ const tagId = "dsh-pause-session/button.module.css";
1789
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1790
+ const tag = document.createElement("style");
1791
+ tag.dataset.plugin = "dsh-pause-session";
1792
+ tag.dataset.pluginCss = tagId;
1793
+ tag.textContent = css;
1794
+ document.head.appendChild(tag);
1795
+ }
1796
+ var button_module_css_default = {
1797
+ "dsp": "FCO1_q_dsp",
1798
+ "dspBtn": "FCO1_q_dspBtn",
1799
+ "dspGoal": "FCO1_q_dspGoal",
1800
+ "dspPill": "FCO1_q_dspPill",
1801
+ "dspPillAction": "FCO1_q_dspPillAction",
1802
+ "dspPillBtn": "FCO1_q_dspPillBtn",
1803
+ "dspPillDetail": "FCO1_q_dspPillDetail",
1804
+ "dspPillIcon": "FCO1_q_dspPillIcon",
1805
+ "dspPillPanel": "FCO1_q_dspPillPanel",
1806
+ "dspPillPanelHead": "FCO1_q_dspPillPanelHead",
1807
+ "dspPillPanelNote": "FCO1_q_dspPillPanelNote",
1808
+ "dspPillRow": "FCO1_q_dspPillRow",
1809
+ "dspPillRowActions": "FCO1_q_dspPillRowActions",
1810
+ "dspPillRowFacts": "FCO1_q_dspPillRowFacts",
1811
+ "dspPillRowHead": "FCO1_q_dspPillRowHead",
1812
+ "dspPillRowId": "FCO1_q_dspPillRowId",
1813
+ "dspPillRowPhase": "FCO1_q_dspPillRowPhase",
1814
+ "dspPillRows": "FCO1_q_dspPillRows",
1815
+ "dspPillText": "FCO1_q_dspPillText",
1816
+ "dspPillTriage": "FCO1_q_dspPillTriage",
1817
+ "dspPillWarn": "FCO1_q_dspPillWarn",
1818
+ "dspState": "FCO1_q_dspState",
1819
+ "dspTip": "FCO1_q_dspTip"
1820
+ };
1821
+ //#endregion
1822
+ //#region lib/types/client/pill.js
1823
+ /**
1824
+ * The whole-app control: one pill in the conversation header's utilities row
1825
+ * that answers "can I close the lid?", presses the park or the release, and —
1826
+ * when something did not stop cleanly — opens the morning decision list.
1827
+ *
1828
+ * The header's right-aligned utilities row is inside the conversation column,
1829
+ * which is where this control belongs: it answers for every session tree, but
1830
+ * it is read while looking at one, and a surface inside the layout is covered
1831
+ * by whatever covers that column — a sidebar or a drawer — instead of floating
1832
+ * above the whole frame. The decision list hangs from the control into the
1833
+ * conversation area below.
1834
+ *
1835
+ * The pill reads the host through the command channel only (`./global.ts`):
1836
+ * with no answer, or with no session to ask through, it renders nothing rather
1837
+ * than a state it would have to invent. The decision list is deliberately NOT
1838
+ * part of that first read — it costs two more command calls, and a user who
1839
+ * never opens it pays nothing; the panel asks for its own rows when it opens.
1840
+ * @module dsh-pause-session/client/pill
1841
+ */
1842
+ /** One listed session's row, with the four decisions. */
1843
+ function TriageSessionRow(props) {
1844
+ const { row, decide, t } = props;
1845
+ return (0, react_jsx_runtime.jsxs)("li", {
1846
+ className: button_module_css_default.dspPillRow,
1847
+ children: [
1848
+ (0, react_jsx_runtime.jsxs)("div", {
1849
+ className: button_module_css_default.dspPillRowHead,
1850
+ children: [(0, react_jsx_runtime.jsx)("span", {
1851
+ className: button_module_css_default.dspPillRowId,
1852
+ children: row.sessionId
1853
+ }), (0, react_jsx_runtime.jsx)("span", {
1854
+ className: button_module_css_default.dspPillRowPhase,
1855
+ children: t(row.phaseKey)
1856
+ })]
1857
+ }),
1858
+ (0, react_jsx_runtime.jsxs)("div", {
1859
+ className: button_module_css_default.dspPillRowFacts,
1860
+ children: [row.readable ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("span", { children: [
1861
+ t("triage.stopped"),
1862
+ ": ",
1863
+ row.stopped ?? "-"
1864
+ ] }), (0, react_jsx_runtime.jsx)("span", { children: row.unknownTools.length === 0 ? t("triage.toolsNone") : `${t("triage.tools")}: ${row.unknownTools.join(", ")}` })] }) : (0, react_jsx_runtime.jsx)("span", { children: t("triage.stoppedUnknown") }), row.readable && !row.wakeable ? (0, react_jsx_runtime.jsx)("span", { children: t("triage.noWake") }) : null]
1865
+ }),
1866
+ (0, react_jsx_runtime.jsxs)("div", {
1867
+ className: button_module_css_default.dspPillRowActions,
1868
+ children: [TRIAGE_ACTION_VIEWS.map((action) => (0, react_jsx_runtime.jsx)("button", {
1869
+ type: "button",
1870
+ className: button_module_css_default.dspPillAction,
1871
+ "aria-label": t(action.ariaKey),
1872
+ onClick: () => {
1873
+ decide(row.sessionId, action.action);
1874
+ },
1875
+ children: t(action.labelKey)
1876
+ }, action.action)), (0, react_jsx_runtime.jsx)("button", {
1877
+ type: "button",
1878
+ className: button_module_css_default.dspPillAction,
1879
+ "aria-label": t(TRIAGE_GOAL_VIEW.ariaKey),
1880
+ "aria-pressed": row.rearmGoal,
1881
+ onClick: () => {
1882
+ decide(row.sessionId, TRIAGE_GOAL_VIEW.action);
1883
+ },
1884
+ children: t(TRIAGE_GOAL_VIEW.labelKey)
1885
+ })]
1886
+ })
1887
+ ]
1888
+ });
1889
+ }
1890
+ /**
1891
+ * The whole-app control: what the host says about the whole park, the one
1892
+ * press that parks or releases all of it, and the decision list for whatever
1893
+ * did not stop cleanly.
1894
+ * @param props - session-header runtime, host operations, and localized copy.
1895
+ * @returns the control and its panel, or nothing while the host has not answered.
1896
+ */
1897
+ function PausePill(props) {
1898
+ const { refresh, toggle, setPanel, decide, t, useSessions, useSession } = props;
1899
+ const reading = props.usePauseGlobal((state) => state.global);
1900
+ const triage = props.usePauseGlobal((state) => state.triage);
1901
+ const open = props.usePauseGlobal((state) => state.panelOpen);
1902
+ const address = useSessions(addressingSession);
1903
+ const view = globalPauseView(reading);
1904
+ const list = triageView(triage);
1905
+ const visible = showsPauseControl(useSession((snapshot) => snapshot.subagent));
1906
+ (0, react.useEffect)(() => {
1907
+ if (visible && address !== void 0) refresh(address);
1908
+ }, [
1909
+ refresh,
1910
+ address,
1911
+ visible
1912
+ ]);
1913
+ if (!visible || !view.visible || address === void 0) return null;
1914
+ const tip = view.caveats.map((caveat) => caveat.names.length === 0 ? t(caveat.key) : `${t(caveat.key)} (${caveat.names.join(", ")})`).join("\n");
1915
+ return (0, react_jsx_runtime.jsxs)("span", {
1916
+ className: button_module_css_default.dspPill,
1917
+ children: [
1918
+ !open ? null : (0, react_jsx_runtime.jsxs)("div", {
1919
+ className: button_module_css_default.dspPillPanel,
1920
+ role: "dialog",
1921
+ "aria-label": t("triage.title"),
1922
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1923
+ className: button_module_css_default.dspPillPanelHead,
1924
+ children: [(0, react_jsx_runtime.jsx)("span", {
1925
+ className: button_module_css_default.dspPillText,
1926
+ children: t("triage.title")
1927
+ }), (0, react_jsx_runtime.jsx)("button", {
1928
+ type: "button",
1929
+ className: button_module_css_default.dspPillAction,
1930
+ "aria-label": t("triage.refresh.aria"),
1931
+ onClick: () => {
1932
+ setPanel(true);
1933
+ },
1934
+ children: t("triage.refresh.aria")
1935
+ })]
1936
+ }), !list.visible ? (0, react_jsx_runtime.jsx)("p", {
1937
+ className: button_module_css_default.dspPillPanelNote,
1938
+ children: t("triage.reading")
1939
+ }) : list.rows.length === 0 ? (0, react_jsx_runtime.jsx)("p", {
1940
+ className: button_module_css_default.dspPillPanelNote,
1941
+ children: t("triage.empty")
1942
+ }) : (0, react_jsx_runtime.jsx)("ul", {
1943
+ className: button_module_css_default.dspPillRows,
1944
+ children: list.rows.map((row) => (0, react_jsx_runtime.jsx)(TriageSessionRow, {
1945
+ row,
1946
+ decide,
1947
+ t
1948
+ }, row.sessionId))
1949
+ })]
1950
+ }),
1951
+ view.unclean === 0 ? null : (0, react_jsx_runtime.jsxs)("button", {
1952
+ type: "button",
1953
+ className: button_module_css_default.dspPillTriage,
1954
+ "aria-label": t("triage.open.aria"),
1955
+ "aria-expanded": open,
1956
+ title: `${String(view.unclean)} ${t("all.triage")}`,
1957
+ onClick: () => {
1958
+ setPanel(!open);
1959
+ },
1960
+ children: [(0, react_jsx_runtime.jsx)(ChevronIcon, {}), (0, react_jsx_runtime.jsx)("span", {
1961
+ className: button_module_css_default.dspPillDetail,
1962
+ children: view.unclean
1963
+ })]
1964
+ }),
1965
+ (0, react_jsx_runtime.jsxs)("button", {
1966
+ type: "button",
1967
+ className: view.labelKey === void 0 && view.detail.length === 0 ? `${button_module_css_default.dspPillBtn} ${button_module_css_default.dspPillIcon}` : button_module_css_default.dspPillBtn,
1968
+ "aria-label": t(view.ariaKey),
1969
+ "aria-pressed": view.action === "resume-all",
1970
+ disabled: !view.enabled,
1971
+ ...tip.length === 0 ? {} : { title: tip },
1972
+ onClick: () => {
1973
+ toggle(address);
1974
+ },
1975
+ children: [
1976
+ view.action === "resume-all" ? (0, react_jsx_runtime.jsx)(ResumeIcon, {}) : (0, react_jsx_runtime.jsx)(PauseIcon, {}),
1977
+ view.labelKey === void 0 ? null : (0, react_jsx_runtime.jsx)("span", {
1978
+ className: button_module_css_default.dspPillText,
1979
+ children: t(view.labelKey)
1980
+ }),
1981
+ view.detail.length === 0 ? null : (0, react_jsx_runtime.jsx)("span", {
1982
+ className: button_module_css_default.dspPillDetail,
1983
+ children: view.detail
1984
+ }),
1985
+ view.caveats.length === 0 ? null : (0, react_jsx_runtime.jsx)("span", {
1986
+ className: button_module_css_default.dspPillWarn,
1987
+ children: (0, react_jsx_runtime.jsx)(CaveatIcon, {})
1988
+ })
1989
+ ]
1990
+ })
1991
+ ]
1992
+ });
1993
+ }
1994
+ //#endregion
1995
+ //#region lib/types/client/locales.js
1996
+ /** `pause` namespace dictionaries. */
1997
+ /** Simplified Chinese dictionary (the key-set source of truth). */
1998
+ const zh = {
1999
+ "pause.aria": "暂停这个会话及其子代理:下一条模型请求不会发出",
2000
+ "pause.tip": "暂停这个会话及其子代理",
2001
+ "resume.aria": "恢复这个会话及其子代理",
2002
+ "resume.tip": "恢复这个会话及其子代理",
2003
+ "draining.aria": "正在暂停:还有工具在运行,工具运行完之后、下一条模型请求发出之前,会话会停住;按下可取消这次暂停",
2004
+ "parking.aria": "正在暂停:正在保存待发输入;按下可取消这次暂停",
2005
+ "safe.aria": "已暂停:待发输入已保存;按下可恢复",
2006
+ "flushFailed.aria": "已暂停,但保存待发输入失败;按下可恢复",
2007
+ "state.paused": "已暂停",
2008
+ "state.draining": "正在暂停",
2009
+ "state.parking": "正在保存",
2010
+ "state.safe": "已暂停",
2011
+ "state.flushFailed": "保存失败",
2012
+ "goal.aria": "恢复时重开目标:恢复这个会话时是否重新打开目标并让它继续运行;按下切换",
2013
+ "goal.on.tip": "恢复这个会话会重新打开目标,它会自己继续运行;按下改为不重开",
2014
+ "goal.off.tip": "恢复这个会话不会重新打开目标;按下改为重开",
2015
+ "checking.aria": "正在读取暂停状态",
2016
+ "checking.tip": "正在读取暂停状态",
2017
+ "unavailable.aria": "暂停状态不可用",
2018
+ "unavailable.tip": "暂停状态不可用",
2019
+ "all.pause.aria": "暂停所有运行中的会话及其子代理",
2020
+ "all.resume.aria": "恢复所有已暂停的会话",
2021
+ "all.idle": "暂停所有会话",
2022
+ "all.finishing": "正在暂停",
2023
+ "all.unclean": "有会话未暂停",
2024
+ "all.unclean.aria": "有会话未暂停,不能在这里恢复;打开清单逐行处理",
2025
+ "all.safe": "全部已暂停",
2026
+ "all.checking.aria": "正在读取暂停状态",
2027
+ "all.caveat.jobs": "后台任务仍在运行",
2028
+ "all.caveat.unresumable": "含无法恢复的子代理",
2029
+ "all.caveat.stalled": "未暂停,需要你决定",
2030
+ "all.caveat.unread": "子代理列表读不出来",
2031
+ "all.caveat.blindTrees": "子代理服务未加载,无法检查子代理",
2032
+ "all.caveat.blindJobs": "作业服务未加载,无法检查后台任务",
2033
+ "triage.open.aria": "查看未暂停的会话清单",
2034
+ "triage.title": "未暂停的会话",
2035
+ "triage.empty": "没有需要决定的会话",
2036
+ "triage.reading": "正在读取清单",
2037
+ "triage.unavailable": "清单暂不可读",
2038
+ "triage.refresh.aria": "重新读取清单",
2039
+ "triage.session.aria": "会话",
2040
+ "triage.stopped": "停在",
2041
+ "triage.stoppedUnknown": "读不到它的日志,停止位置未知",
2042
+ "triage.tools": "未知的工具结果",
2043
+ "triage.toolsNone": "没有缺少结果的工具调用",
2044
+ "triage.noWake": "这个会话没有待发的模型请求,继续不会发出请求",
2045
+ "triage.phase.draining": "工具还在运行时中断",
2046
+ "triage.phase.parking": "已暂停,但待发输入未保存",
2047
+ "triage.phase.flushFailed": "已暂停,但保存失败",
2048
+ "triage.continue": "继续",
2049
+ "triage.continue.aria": "继续:不添加消息,直接恢复",
2050
+ "triage.note": "继续并附说明",
2051
+ "triage.note.aria": "继续并附说明:告诉模型工具结果未知,不要盲目重试",
2052
+ "triage.note.hint": "会加入这句话:",
2053
+ "triage.review": "只看不运行",
2054
+ "triage.review.aria": "只看不运行:不启动会话、不发请求;需要查看时在会话历史里打开",
2055
+ "triage.leave": "保持暂停",
2056
+ "triage.leave.aria": "保持暂停:不恢复,也不写入任何内容",
2057
+ "triage.goal": "恢复时重开目标",
2058
+ "triage.goal.aria": "恢复时重开目标:恢复这个会话时是否重新打开目标并让它自己继续运行;按下切换",
2059
+ "all.triage": "有待决定的会话"
2060
+ };
2061
+ /** English dictionary, checked complete against the zh key set. */
2062
+ const en = {
2063
+ "pause.aria": "Pause this session and its subagents: their next model request will not be sent",
2064
+ "pause.tip": "Pause this session and its subagents",
2065
+ "resume.aria": "Resume this session and its subagents",
2066
+ "resume.tip": "Resume this session and its subagents",
2067
+ "draining.aria": "Pausing: a tool is still running. The session pauses after the tool finishes and before its next model request; press to cancel the pause",
2068
+ "parking.aria": "Pausing: saving pending input; press to cancel the pause",
2069
+ "safe.aria": "Paused: pending input is saved; press to resume",
2070
+ "flushFailed.aria": "Paused, but saving pending input failed; press to resume",
2071
+ "state.paused": "Paused",
2072
+ "state.draining": "Pausing",
2073
+ "state.parking": "Saving",
2074
+ "state.safe": "Paused",
2075
+ "state.flushFailed": "Save failed",
2076
+ "goal.aria": "Re-open goal on resume: whether resuming this session re-opens its goal and lets it run on its own; press to switch",
2077
+ "goal.on.tip": "Resuming re-opens the goal, which runs on its own; press to keep it closed",
2078
+ "goal.off.tip": "Resuming will not re-open the goal; press to re-open it",
2079
+ "checking.aria": "Reading pause state",
2080
+ "checking.tip": "Reading pause state",
2081
+ "unavailable.aria": "Pause state unavailable",
2082
+ "unavailable.tip": "Pause state unavailable",
2083
+ "all.pause.aria": "Pause all running sessions and their subagents",
2084
+ "all.resume.aria": "Resume all paused sessions",
2085
+ "all.idle": "Pause all sessions",
2086
+ "all.finishing": "Pausing",
2087
+ "all.unclean": "Some sessions are not paused",
2088
+ "all.unclean.aria": "Some sessions are not paused and cannot be resumed here; open the list to decide",
2089
+ "all.safe": "All Paused",
2090
+ "all.checking.aria": "Reading pause state",
2091
+ "all.caveat.jobs": "background jobs are still running",
2092
+ "all.caveat.unresumable": "contains a subagent that cannot be resumed",
2093
+ "all.caveat.stalled": "did not pause; needs your decision",
2094
+ "all.caveat.unread": "subagent list could not be read",
2095
+ "all.caveat.blindTrees": "subagent service is not loaded, so subagents cannot be checked",
2096
+ "all.caveat.blindJobs": "job service is not loaded, so background jobs cannot be checked",
2097
+ "triage.open.aria": "Review sessions that did not pause",
2098
+ "triage.title": "Sessions that did not pause",
2099
+ "triage.empty": "No sessions need a decision",
2100
+ "triage.reading": "Reading list",
2101
+ "triage.unavailable": "List unavailable",
2102
+ "triage.refresh.aria": "Read the list again",
2103
+ "triage.session.aria": "Session",
2104
+ "triage.stopped": "Stopped at",
2105
+ "triage.stoppedUnknown": "Its log cannot be read, so where it stopped is unknown",
2106
+ "triage.tools": "Unknown tool results",
2107
+ "triage.toolsNone": "No tool call is missing a result",
2108
+ "triage.noWake": "This session has no pending model request; continuing sends nothing",
2109
+ "triage.phase.draining": "stopped while a tool was still running",
2110
+ "triage.phase.parking": "paused but pending input was not saved",
2111
+ "triage.phase.flushFailed": "paused but saving failed",
2112
+ "triage.continue": "Continue",
2113
+ "triage.continue.aria": "Continue: resume without adding a message",
2114
+ "triage.note": "Continue with a note",
2115
+ "triage.note.aria": "Continue with a note: tell the model the tool result is unknown and not to retry blindly",
2116
+ "triage.note.hint": "This text is added:",
2117
+ "triage.review": "View only",
2118
+ "triage.review.aria": "View only: do not start the session or send a request; open it from your session history to read it",
2119
+ "triage.leave": "Leave it paused",
2120
+ "triage.leave.aria": "Leave it paused: keep the pause and write nothing",
2121
+ "triage.goal": "Re-open goal on resume",
2122
+ "triage.goal.aria": "Re-open goal on resume: whether resuming this session re-opens its goal, which then runs on its own; press to switch",
2123
+ "all.triage": "sessions need a decision"
2124
+ };
2125
+ //#endregion
2126
+ //#region lib/types/client/index.js
2127
+ /**
2128
+ * Browser half of dsh-pause-session.
2129
+ *
2130
+ * Registers two controls: one in the session header's title-adjacent action
2131
+ * group (`conversation.session.header.actions`), which pauses or resumes the
2132
+ * session it sits on, and one in the header's right-aligned utilities row
2133
+ * (`conversation.session.header.utilities`), which answers for the whole app.
2134
+ * Both seats are session-scoped and their props carry `sessionId`, so every
2135
+ * open session header renders its own pair and switching sessions follows
2136
+ * automatically; the whole-app control speaks for every session from the
2137
+ * header of the one on screen.
2138
+ *
2139
+ * The whole-app control sits inside the conversation column rather than in the
2140
+ * frame-wide overlay layer: it is read while looking at one conversation, and
2141
+ * a sidebar or drawer that covers that area covers the control with it.
2142
+ *
2143
+ * Both controls are host-authoritative: they read state from the host's command
2144
+ * registry and drive it through the host's session commands (`./control.ts` and
2145
+ * `./global.ts` document why the command registry is the only live channel a
2146
+ * third-party plugin can use). Each stays inert wherever that round-trip is
2147
+ * unavailable rather than rendering a control that cannot tell the truth. The
2148
+ * header control also reads the whole-app answer the whole-app control
2149
+ * publishes, so a park that holds nothing does not cost a host round-trip per
2150
+ * header.
2151
+ * @module dsh-pause-session/client
2152
+ */
2153
+ /** Dictionary namespace owned by this plugin. */
2154
+ const NS = "pause";
2155
+ /** Required services: the slot registry, the locale registry, and the host round-trip. */
2156
+ const inject = [
2157
+ "slots",
2158
+ "locale",
2159
+ "remote",
2160
+ "remote.commands"
2161
+ ];
2162
+ /**
2163
+ * The session's pause control: pause the session and its subagents before
2164
+ * their next model request, or resume them, with the host's own state on screen.
2165
+ * Once the host reports the park's goal opt-out, a second switch sits beside it
2166
+ * — the only place a session that stopped CLEANLY can be told not to re-open its
2167
+ * goal on the next resume, because the decision list shows only the sessions
2168
+ * that did not stop cleanly.
2169
+ *
2170
+ * A subagent session renders nothing: its work is its parent's, the parent's
2171
+ * control already covers it, and a button here would offer to pause "a session"
2172
+ * from inside a child.
2173
+ * @param props - session runtime, host operations, and localized copy.
2174
+ * @returns the header action, or nothing on a subagent session.
2175
+ */
2176
+ function PauseButton(props) {
2177
+ const { sessionId, refresh, toggle, toggleGoal, t, useSession } = props;
2178
+ const view = pauseView(props.usePause((state) => state.bySession[String(sessionId)]));
2179
+ const visible = showsPauseControl(useSession((snapshot) => snapshot.subagent));
2180
+ (0, react.useEffect)(() => {
2181
+ refresh(sessionId);
2182
+ }, [refresh, sessionId]);
2183
+ if (!visible) return null;
2184
+ return (0, react_jsx_runtime.jsxs)("span", {
2185
+ className: button_module_css_default.dsp,
2186
+ children: [
2187
+ (0, react_jsx_runtime.jsx)("button", {
2188
+ type: "button",
2189
+ className: button_module_css_default.dspBtn,
2190
+ "aria-label": t(view.ariaKey),
2191
+ "aria-pressed": view.paused,
2192
+ disabled: !view.enabled,
2193
+ onClick: () => {
2194
+ toggle(sessionId);
2195
+ },
2196
+ children: view.paused ? (0, react_jsx_runtime.jsx)(ResumeIcon, {}) : (0, react_jsx_runtime.jsx)(PauseIcon, {})
2197
+ }),
2198
+ view.rearmGoal === void 0 ? null : (0, react_jsx_runtime.jsx)("button", {
2199
+ type: "button",
2200
+ className: button_module_css_default.dspGoal,
2201
+ "aria-label": t("goal.aria"),
2202
+ "aria-pressed": view.rearmGoal,
2203
+ title: t(view.rearmGoal ? "goal.on.tip" : "goal.off.tip"),
2204
+ onClick: () => {
2205
+ toggleGoal(sessionId);
2206
+ },
2207
+ children: (0, react_jsx_runtime.jsx)(GoalIcon, {})
2208
+ }),
2209
+ view.labelKey === void 0 ? null : (0, react_jsx_runtime.jsxs)("span", {
2210
+ className: button_module_css_default.dspState,
2211
+ children: [t(view.labelKey), view.runningTools.length === 0 ? null : `: ${view.runningTools.join(", ")}`]
2212
+ })
2213
+ ]
2214
+ });
2215
+ }
2216
+ /**
2217
+ * Client plugin body: register the locale dictionaries, own both controllers,
2218
+ * follow the host's registry changes, and contribute the per-session control
2219
+ * and the whole-app control into the session header.
2220
+ * @param ctx - client context carrying slots, locale, and the Remote face.
2221
+ */
2222
+ function apply(ctx) {
2223
+ ctx.effect(() => ctx.locale.register(NS, {
2224
+ zh,
2225
+ en
2226
+ }), "pause: dictionaries");
2227
+ const lifetime = new AbortController();
2228
+ ctx.effect(() => () => {
2229
+ lifetime.abort(/* @__PURE__ */ new Error("pause: client unloaded"));
2230
+ }, "pause: lifetime");
2231
+ const globalControl = new PauseGlobalControl(ctx.remote.commands, lifetime.signal);
2232
+ const control = new PauseControl(ctx.remote.commands, lifetime.signal, void 0, void 0, () => globalControl.store.getSnapshot().global);
2233
+ ctx.effect(() => globalControl.store.subscribe(() => {
2234
+ control.reconsider();
2235
+ }), "pause: the whole-app answer serves the session headers");
2236
+ ctx.effect(() => ctx.remote.$on("commands/change", () => {
2237
+ control.refreshAll();
2238
+ globalControl.refreshAll();
2239
+ }), "pause: host command registry changes");
2240
+ ctx.effect(() => {
2241
+ if (typeof document === "undefined") return () => {};
2242
+ const onVisibility = () => {
2243
+ control.watchVisibility();
2244
+ globalControl.watchVisibility();
2245
+ };
2246
+ document.addEventListener("visibilitychange", onVisibility);
2247
+ return () => {
2248
+ document.removeEventListener("visibilitychange", onVisibility);
2249
+ };
2250
+ }, "pause: page visibility");
2251
+ ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
2252
+ name: "conversation.session.header.actions",
2253
+ id: NS,
2254
+ order: 40,
2255
+ locale: NS,
2256
+ inject: () => ({
2257
+ hooks: { pause: control.store },
2258
+ refresh: (sessionId) => {
2259
+ control.refresh(sessionId);
2260
+ },
2261
+ toggle: (sessionId) => {
2262
+ control.toggle(sessionId);
2263
+ },
2264
+ toggleGoal: (sessionId) => {
2265
+ control.toggleGoal(sessionId);
2266
+ }
2267
+ })
2268
+ }, PauseButton));
2269
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
2270
+ name: "conversation.session.header.utilities",
2271
+ id: "pause-all",
2272
+ order: 60,
2273
+ locale: NS,
2274
+ inject: () => ({
2275
+ hooks: { pauseGlobal: globalControl.store },
2276
+ refresh: (sessionId) => {
2277
+ globalControl.refresh(sessionId);
2278
+ },
2279
+ toggle: (sessionId) => {
2280
+ globalControl.toggle(sessionId);
2281
+ },
2282
+ setPanel: (open) => {
2283
+ globalControl.setPanel(open);
2284
+ },
2285
+ decide: (sessionId, action) => {
2286
+ globalControl.decide(sessionId, action);
2287
+ }
2288
+ })
2289
+ }, PausePill));
2290
+ }
2291
+ //#endregion
2292
+ exports.PauseButton = PauseButton;
2293
+ exports.apply = apply;
2294
+ exports.inject = inject;
2295
+ return module.exports;
2296
+ }
2297
+ });
2298
+
2299
+ //# sourceMappingURL=client.js.map