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.
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Browser-side reading and driving of the host's whole-app park state.
3
+ *
4
+ * The same one live channel the per-session control uses — a session command
5
+ * run through `ctx.remote.commands.execute`, plus the forwarded
6
+ * `commands/change` event as the only notification — carries the aggregate
7
+ * here. There is no push channel a third-party plugin can add, so the browser
8
+ * asks: `pause-state-all` is registered globally and its RESULT carries the
9
+ * whole-app status as of the read.
10
+ *
11
+ * The command channel is session-addressed, so the control needs one session
12
+ * to talk through. It picks a user session the host can serve (never a
13
+ * subagent's own session), preferring the one on screen; with no session at
14
+ * all there is nothing to ask, and the control claims nothing rather than
15
+ * inventing an answer. Once the host's own answer names the session the park
16
+ * belongs to, reads are addressed there instead: a read writes two records into
17
+ * the address session's transcript, and they belong in the session being parked
18
+ * rather than in whatever the user happens to be looking at. That name outlives
19
+ * the phases — a session that drained and parked cleanly names nothing else — so
20
+ * a clean park is addressed like a moving one. Failing that, the session the
21
+ * last answer reported as registered is remembered, then the one a read was last
22
+ * served through — an address change is the user looking somewhere else, not
23
+ * news about the park, and a park that has emptied out is no reason to start
24
+ * writing rows into every session the user opens next. An address that stops
25
+ * answering (its agent is gone) is forgotten and the read falls back to the next
26
+ * one, the session the shell asks with last, so a stale address costs an honest
27
+ * answer rather than a control that can never speak again.
28
+ *
29
+ * Every phase a session reaches announces itself through the registry — a
30
+ * transition re-registers that session's status verb — so a change is what buys
31
+ * the next read, and the flush that settles a park is announced like every other
32
+ * step (`./burst.ts`). A read that finds work still in flight is therefore the
33
+ * answer to the transition that produced it rather than a reason to keep asking;
34
+ * what the burst still owes is the delivery of a change, and the rows of a panel
35
+ * whose own read did not answer. A settled answer is left on screen unread: the
36
+ * panel stays readable and the pill keeps saying what it says, but the browser
37
+ * stops spending host round-trips and log records on a state that has stopped
38
+ * moving. What owes a read is the host's forwarded registry change, the user's
39
+ * own press or decision, and the first read at mount; once an answer is on
40
+ * screen, the shell handing this control another session to ask through is none
41
+ * of those. A press, a decision and an opened panel each read the host as they
42
+ * run, and the changes they set off are that action's own echo: those are
43
+ * absorbed for a short window (`./burst.ts`), so a forwarded change owes a read
44
+ * exactly when something moved that this browser did not do — with one look owed
45
+ * at the close of that window when the reading the action left is still moving,
46
+ * which is how a park that reaches its final phase inside the echo window is
47
+ * still read.
48
+ * @module dsh-pause-session/client/global
49
+ */
50
+ import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client';
51
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
52
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
53
+ import type { GlobalStatus, TriageAction, TriageRow } from '../shared.js';
54
+ import type { BurstPolicy } from './burst.js';
55
+ /** What the whole-app control knows: the host's answer, or that it could not ask. */
56
+ export type GlobalReading = {
57
+ readonly kind: 'reported';
58
+ readonly status: GlobalStatus;
59
+ } | {
60
+ readonly kind: 'unavailable';
61
+ };
62
+ /** What the decision-list panel knows: the host's rows, or that it could not ask. */
63
+ export type TriageReading = {
64
+ readonly kind: 'reported';
65
+ readonly rows: readonly TriageRow[];
66
+ } | {
67
+ readonly kind: 'unavailable';
68
+ };
69
+ /** Whole-app control state. */
70
+ export interface GlobalPauseState {
71
+ /** The host's answer; absent until one arrives. Writable: the store updates through a draft. */
72
+ global: GlobalReading | undefined;
73
+ /** Whether the user asked for the decision list; the panel reads only when it is open. */
74
+ panelOpen: boolean;
75
+ /** The host's decision list; absent until the panel asks for it. */
76
+ triage: TriageReading | undefined;
77
+ }
78
+ /** The command one listed verb runs, named from the one vocabulary the host registers. */
79
+ export declare const TRIAGE_DECISION_COMMAND: Readonly<Record<TriageAction, string>>;
80
+ /**
81
+ * The session a whole-app command is addressed to.
82
+ *
83
+ * A subagent's own session is never addressed: its agent belongs to a parent
84
+ * and is not the host's business to drive from the shell. Among the rest, the
85
+ * one on screen is the best address (its agent is live by construction), then
86
+ * any session the host reports as running, then any at all.
87
+ * @param state - the session list snapshot the shell provides.
88
+ * @returns the address, or `undefined` when the app has no session to ask through.
89
+ */
90
+ export declare function addressingSession(state: {
91
+ readonly ids: readonly SessionId[];
92
+ readonly byId: Readonly<Record<string, {
93
+ readonly running?: boolean;
94
+ readonly origin?: string;
95
+ readonly parentId?: unknown;
96
+ readonly retainedBy?: {
97
+ readonly mainView?: number;
98
+ };
99
+ } | undefined>>;
100
+ }): SessionId | undefined;
101
+ /**
102
+ * The session a registered park owns, when the host's answer names one that is
103
+ * still moving. This is how a read stops landing in whatever session the user
104
+ * happens to be looking at.
105
+ * @param status - the host's last whole-app answer.
106
+ * @returns a draining session, or `undefined` when the answer names none.
107
+ */
108
+ export declare function registeredAddress(status: GlobalStatus): SessionId | undefined;
109
+ /**
110
+ * The session the host names as the park's own, whatever phase the park
111
+ * reached — the address a whole-app read belongs at.
112
+ *
113
+ * This is the fact {@link registeredAddress} cannot carry once a park is clean:
114
+ * a session that reached `safe` has nothing left to do, so no bucket names it,
115
+ * and a reader in another window would fall back to whatever session it is
116
+ * showing. An answer written before this field existed names no session here:
117
+ * an absent address, never one to guess at.
118
+ * @param status - the host's last whole-app answer.
119
+ * @returns the session the park belongs to, or `undefined` when the answer names none.
120
+ */
121
+ export declare function parkedAddress(status: GlobalStatus): SessionId | undefined;
122
+ /** Owns the published whole-app reading; nothing else writes that state. */
123
+ export declare class PauseGlobalControl {
124
+ private readonly commands;
125
+ private readonly lifetime;
126
+ /** State source the whole-app contribution binds its selector hook to. */
127
+ readonly store: SnapshotStore<GlobalPauseState>;
128
+ /** The session the shell asked this control through; reads start here. */
129
+ private address;
130
+ /**
131
+ * The last session an answer reported as registered — the address a read
132
+ * falls back to when the host's own answer names no parked session, which is
133
+ * every reading written before that field existed.
134
+ *
135
+ * The buckets that feed it name only what is still moving, so they say
136
+ * nothing about a park that finished cleanly; the park's own name is the live
137
+ * fact, and this memory is what keeps the rows in the park's transcript for an
138
+ * answer that predates it. An empty registry does not clear it: a park whose
139
+ * sessions stopped moving has not moved out of its sessions.
140
+ */
141
+ private lastRegistered;
142
+ /**
143
+ * The session an aggregate read was last served through. Later reads keep
144
+ * going there: every read is two records in the address session's log, and
145
+ * the shell's address follows whatever the user is looking at, which is not
146
+ * where a row about the park belongs.
147
+ */
148
+ private served;
149
+ /** Highest decision-list read requested: an answer older than it is dropped. */
150
+ private latestTriage;
151
+ /** Highest read sequence requested: an answer older than it is dropped. */
152
+ private latest;
153
+ /**
154
+ * Whether a registry change has arrived that no completed read pass has
155
+ * answered. Set only for a change that is news — an absorbed echo of this
156
+ * browser's own action is already answered by that action's read.
157
+ */
158
+ private owed;
159
+ /** The bounded burst of reads one transition buys; see `./burst.ts`. */
160
+ private readonly burst;
161
+ /**
162
+ * @param commands - the Client-callable command namespace.
163
+ * @param lifetime - aborts in-flight calls and the burst when the plugin unloads.
164
+ * @param windowMs - how long the burst collects the registrations of one
165
+ * transition before its first read; also the wait after a press.
166
+ * @param policy - the burst waits and visibility source; defaults to the real page.
167
+ */
168
+ constructor(commands: ClientRemote['commands'], lifetime: AbortSignal, windowMs?: number, policy?: Omit<BurstPolicy, 'windowMs' | 'waitsMs'>);
169
+ /**
170
+ * The shell handed this control a session: the one the pill mounted on, or
171
+ * the one the user has just moved to. Only a control with no answer yet reads
172
+ * here — the view moving is not the park moving, and after the first answer
173
+ * the forwarded registry change and the user's own press are what ask again.
174
+ * A read that came back with nothing is not an answer, so moving to another
175
+ * session is one more chance to reach the host rather than a control that
176
+ * stays mute until something else happens to change. The address is kept
177
+ * either way: it is where this control's presses and its decision list go, and
178
+ * the fallback a remembered address falls back to.
179
+ * @param sessionId - the session the shell asks through.
180
+ * @returns after this read's answer is published or superseded by a newer one.
181
+ */
182
+ refresh(sessionId: SessionId): Promise<void>;
183
+ /** Whether the host has answered: a state, not the absence of one it never sent. */
184
+ private answered;
185
+ /**
186
+ * The host's command registry changed: something may have moved, so a burst
187
+ * is owed. The reads run on the burst's own schedule rather than here, which
188
+ * is what coalesces a tick's worth of changes — parking N sessions announces
189
+ * three registrations each, and a phase change re-registers one more — into
190
+ * one burst. A change that is this browser's own action's echo owes nothing:
191
+ * the action read the host as it ran.
192
+ */
193
+ refreshAll(): void;
194
+ /**
195
+ * One read of the whole screen: the aggregate, plus the decision list while
196
+ * the user has it open. A closed panel spends no command calls on rows nobody
197
+ * is reading.
198
+ */
199
+ private readAll;
200
+ /**
201
+ * The session a read goes through: the one the park is still draining, else
202
+ * the one the host names as the park's own, else the one a read was last
203
+ * served through, else the session the shell is on now. The park's own address
204
+ * comes first because a read's rows land in the transcript it was taken
205
+ * through, and the park — not the reader — is what they are about; its name
206
+ * survives the clean case, where every bucket above is empty and a park that
207
+ * finished would otherwise have no address at all.
208
+ * @param skip - an address this read already tried and could not reach. The
209
+ * fallback is the next candidate rather than the same one again.
210
+ */
211
+ private target;
212
+ /**
213
+ * Ask the host for the aggregate through the session a read goes through,
214
+ * with the one fallback an address that stopped answering is worth: the
215
+ * remembered address is forgotten, and the read is taken again through the
216
+ * next one — the session the shell is on at the latest. A memory of a session
217
+ * this host no longer holds would otherwise leave the control unable to say
218
+ * anything at all, and a single retry is the whole price of keeping it honest.
219
+ */
220
+ private readAggregate;
221
+ /**
222
+ * Publish one aggregate answer, unless a newer read was requested while this
223
+ * one was in flight: that answer would show a state the host has moved past.
224
+ * A served address is remembered, and one that did not answer is forgotten —
225
+ * that is what makes the read fall back to the next address rather than ask
226
+ * it again.
227
+ * @param sessionId - the session this read goes through.
228
+ * @returns whether this read came back without an answer; a superseded read is
229
+ * not one, because the newer read already owns the outcome.
230
+ */
231
+ private readAt;
232
+ /**
233
+ * Open or close the decision list. Opening asks the host for the rows; there
234
+ * is no state to show without that answer. Opening is also a client action
235
+ * that reads, so the changes it sets off are absorbed rather than read back,
236
+ * and whether the burst owes anything is what its own read decides.
237
+ * @param open - whether the panel should be showing.
238
+ */
239
+ setPanel(open: boolean): void;
240
+ /**
241
+ * Ask the host for the decision list through the session the user is on. The
242
+ * list is recorded per calling session and the decisions act only on rows
243
+ * that session was shown, so this read — unlike the aggregate — stays on the
244
+ * shell's own address.
245
+ */
246
+ refreshTriage(): Promise<void>;
247
+ /**
248
+ * Read the aggregate again because the user asked this control to act: the
249
+ * address is the session the shell is on, since a press is a user action on
250
+ * the session the user is looking at, and the host's answer is what the
251
+ * control shows next.
252
+ * @param sessionId - the session the shell is on.
253
+ */
254
+ private reread;
255
+ /** One decision-list read, addressed at the session the user is on. */
256
+ private readTriage;
257
+ /**
258
+ * Take one listed verb on one listed session, addressed through the session
259
+ * the user is on. The list is read first, because the host acts only on rows
260
+ * it has shown the calling session, and read again afterwards because the
261
+ * host's answer — not this control's assumption — is what the next render
262
+ * shows: a decision can release a session, and the goal switch changes what
263
+ * the row itself says.
264
+ * @param sessionId - the listed session to act on.
265
+ * @param action - which listed verb was taken.
266
+ */
267
+ decide(sessionId: string, action: TriageAction): Promise<void>;
268
+ /**
269
+ * Press the pill: park everything, or release everything the park owns.
270
+ * Which one it is comes from the host's own last answer, never from a local
271
+ * flip; the state shown afterwards is the host's answer again. A reading the
272
+ * view does not turn into a verb — every registered session is one the host
273
+ * will not wake — is not pressed at all.
274
+ * @param sessionId - the session to address the verb to.
275
+ * @returns after the control re-reads the host.
276
+ */
277
+ toggle(sessionId: SessionId): Promise<void>;
278
+ /** Ask the host for the decision list, in the browser's vocabulary. */
279
+ private readTriageRows;
280
+ /** Ask the host what the whole-app park state is, in the browser's vocabulary. */
281
+ private read;
282
+ private publish;
283
+ /**
284
+ * Decide what the burst owes after a read: another look while a change this
285
+ * browser has not answered is outstanding, or while the open panel is still
286
+ * owed rows, and nothing once neither holds.
287
+ *
288
+ * A registered session standing at the park point is left alone: the flush
289
+ * that settles it announces itself on the command channel like every other
290
+ * transition, so watching it from here would spend reads on a move that is
291
+ * going to speak for itself. A verdict that stays `waiting` because a record
292
+ * is stalled, unresumable or unread is not settling either: nothing in this
293
+ * process will change it, and a session that starts moving again registers its
294
+ * markers, which is what asks next.
295
+ *
296
+ * The one transition that cannot speak for itself is the one this browser's
297
+ * own echo window swallowed: a press reads the host, the park reaches its
298
+ * final phase while the window is still open, and the changes that would have
299
+ * announced it look exactly like the press's own registrations. An action that
300
+ * left the park moving therefore asks for one look when that window closes,
301
+ * and only then. Once nothing is owed, the last reading stays published — the
302
+ * verdict, the pill and the rows are exactly what the host last reported.
303
+ */
304
+ private follow;
305
+ /**
306
+ * Whether the park this control last read still has work on its way — the only
307
+ * state an action's own echo could be hiding a transition behind. A record
308
+ * that is stalled, unresumable or unread is not moving: nothing in this
309
+ * process will finish it.
310
+ */
311
+ private moving;
312
+ /**
313
+ * Whether the open panel is still owed its rows. Opening the panel reads them,
314
+ * so this holds only after a read that did not answer — the one debt no change
315
+ * created, and the one place a retry is the difference between an empty panel
316
+ * and the facts the host has. It is bounded by the burst like everything else.
317
+ */
318
+ private unansweredPanel;
319
+ /**
320
+ * Follow the page in and out of view. A hidden page is not being read — and
321
+ * the read the burst was about to take is not lost, it waits for the page to
322
+ * come back.
323
+ */
324
+ watchVisibility(): void;
325
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The glyphs both pause controls are drawn with, so the session-header
3
+ * control and the whole-app pill cannot drift apart.
4
+ * @module dsh-pause-session/client/icons
5
+ */
6
+ import type { ReactNode } from 'react';
7
+ /** 16px stroke icon (currentColor): the two bars of a pause. */
8
+ export declare function PauseIcon(): ReactNode;
9
+ /** 16px stroke icon (currentColor): the triangle of a resume. */
10
+ export declare function ResumeIcon(): ReactNode;
11
+ /**
12
+ * 16px stroke icon (currentColor): the rings of a target, for the goal switch
13
+ * on the session header. Drawn at 16px like the pause and resume glyphs it sits
14
+ * beside rather than the 14px marks the two reports use.
15
+ */
16
+ export declare function GoalIcon(): ReactNode;
17
+ /**
18
+ * 14px stroke icon (currentColor): the mark that a report carries a caveat.
19
+ * A glyph rather than copy, so the facts it stands for stay in the locale
20
+ * dictionaries beside everything else the user reads.
21
+ */
22
+ export declare function CaveatIcon(): ReactNode;
23
+ /** 14px stroke icon (currentColor): the chevron that opens the decision list. */
24
+ export declare function ChevronIcon(): ReactNode;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Browser half of dsh-pause-session.
3
+ *
4
+ * Registers two controls: one in the session header's title-adjacent action
5
+ * group (`conversation.session.header.actions`), which pauses or resumes the
6
+ * session it sits on, and one in the header's right-aligned utilities row
7
+ * (`conversation.session.header.utilities`), which answers for the whole app.
8
+ * Both seats are session-scoped and their props carry `sessionId`, so every
9
+ * open session header renders its own pair and switching sessions follows
10
+ * automatically; the whole-app control speaks for every session from the
11
+ * header of the one on screen.
12
+ *
13
+ * The whole-app control sits inside the conversation column rather than in the
14
+ * frame-wide overlay layer: it is read while looking at one conversation, and
15
+ * a sidebar or drawer that covers that area covers the control with it.
16
+ *
17
+ * Both controls are host-authoritative: they read state from the host's command
18
+ * registry and drive it through the host's session commands (`./control.ts` and
19
+ * `./global.ts` document why the command registry is the only live channel a
20
+ * third-party plugin can use). Each stays inert wherever that round-trip is
21
+ * unavailable rather than rendering a control that cannot tell the truth. The
22
+ * header control also reads the whole-app answer the whole-app control
23
+ * publishes, so a park that holds nothing does not cost a host round-trip per
24
+ * header.
25
+ * @module dsh-pause-session/client
26
+ */
27
+ import type { ReactNode } from 'react';
28
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
29
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
30
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
31
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
32
+ import type { PauseState } from './control.js';
33
+ import { type PauseKey } from './locales.js';
34
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
35
+ interface LocaleNamespaceMap {
36
+ /** The pause control's copy. */
37
+ 'pause': PauseKey;
38
+ }
39
+ }
40
+ /** Required services: the slot registry, the locale registry, and the host round-trip. */
41
+ export declare const inject: string[];
42
+ /** Browser state and operations injected into the Session-header contribution. */
43
+ export interface PauseInjected {
44
+ hooks: {
45
+ pause: ObservableSnapshot<PauseState>;
46
+ };
47
+ /** Read the host's state for one session (the control calls this once per mount). */
48
+ refresh: (sessionId: SessionId) => void;
49
+ /** Press the control for one session. */
50
+ toggle: (sessionId: SessionId) => void;
51
+ /** Flip one session's own goal opt-out. */
52
+ toggleGoal: (sessionId: SessionId) => void;
53
+ }
54
+ /** Full props of the Session-header pause control. */
55
+ export type PauseButtonProps = PropsRuntime<'conversation.session.header.actions'> & InjectFace<PauseInjected> & PropsLocale<'pause'>;
56
+ /**
57
+ * The session's pause control: pause the session and its subagents before
58
+ * their next model request, or resume them, with the host's own state on screen.
59
+ * Once the host reports the park's goal opt-out, a second switch sits beside it
60
+ * — the only place a session that stopped CLEANLY can be told not to re-open its
61
+ * goal on the next resume, because the decision list shows only the sessions
62
+ * that did not stop cleanly.
63
+ *
64
+ * A subagent session renders nothing: its work is its parent's, the parent's
65
+ * control already covers it, and a button here would offer to pause "a session"
66
+ * from inside a child.
67
+ * @param props - session runtime, host operations, and localized copy.
68
+ * @returns the header action, or nothing on a subagent session.
69
+ */
70
+ export declare function PauseButton(props: PauseButtonProps): ReactNode;
71
+ /**
72
+ * Client plugin body: register the locale dictionaries, own both controllers,
73
+ * follow the host's registry changes, and contribute the per-session control
74
+ * and the whole-app control into the session header.
75
+ * @param ctx - client context carrying slots, locale, and the Remote face.
76
+ */
77
+ export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,69 @@
1
+ /** `pause` namespace dictionaries. */
2
+ /** Simplified Chinese dictionary (the key-set source of truth). */
3
+ export declare const zh: {
4
+ 'pause.aria': string;
5
+ 'pause.tip': string;
6
+ 'resume.aria': string;
7
+ 'resume.tip': string;
8
+ 'draining.aria': string;
9
+ 'parking.aria': string;
10
+ 'safe.aria': string;
11
+ 'flushFailed.aria': string;
12
+ 'state.paused': string;
13
+ 'state.draining': string;
14
+ 'state.parking': string;
15
+ 'state.safe': string;
16
+ 'state.flushFailed': string;
17
+ 'goal.aria': string;
18
+ 'goal.on.tip': string;
19
+ 'goal.off.tip': string;
20
+ 'checking.aria': string;
21
+ 'checking.tip': string;
22
+ 'unavailable.aria': string;
23
+ 'unavailable.tip': string;
24
+ 'all.pause.aria': string;
25
+ 'all.resume.aria': string;
26
+ 'all.idle': string;
27
+ 'all.finishing': string;
28
+ 'all.unclean': string;
29
+ 'all.unclean.aria': string;
30
+ 'all.safe': string;
31
+ 'all.checking.aria': string;
32
+ 'all.caveat.jobs': string;
33
+ 'all.caveat.unresumable': string;
34
+ 'all.caveat.stalled': string;
35
+ 'all.caveat.unread': string;
36
+ 'all.caveat.blindTrees': string;
37
+ 'all.caveat.blindJobs': string;
38
+ 'triage.open.aria': string;
39
+ 'triage.title': string;
40
+ 'triage.empty': string;
41
+ 'triage.reading': string;
42
+ 'triage.unavailable': string;
43
+ 'triage.refresh.aria': string;
44
+ 'triage.session.aria': string;
45
+ 'triage.stopped': string;
46
+ 'triage.stoppedUnknown': string;
47
+ 'triage.tools': string;
48
+ 'triage.toolsNone': string;
49
+ 'triage.noWake': string;
50
+ 'triage.phase.draining': string;
51
+ 'triage.phase.parking': string;
52
+ 'triage.phase.flushFailed': string;
53
+ 'triage.continue': string;
54
+ 'triage.continue.aria': string;
55
+ 'triage.note': string;
56
+ 'triage.note.aria': string;
57
+ 'triage.note.hint': string;
58
+ 'triage.review': string;
59
+ 'triage.review.aria': string;
60
+ 'triage.leave': string;
61
+ 'triage.leave.aria': string;
62
+ 'triage.goal': string;
63
+ 'triage.goal.aria': string;
64
+ 'all.triage': string;
65
+ };
66
+ /** The pause namespace key union. */
67
+ export type PauseKey = keyof typeof zh;
68
+ /** English dictionary, checked complete against the zh key set. */
69
+ export declare const en: Record<PauseKey, string>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The whole-app control: one pill in the conversation header's utilities row
3
+ * that answers "can I close the lid?", presses the park or the release, and —
4
+ * when something did not stop cleanly — opens the morning decision list.
5
+ *
6
+ * The header's right-aligned utilities row is inside the conversation column,
7
+ * which is where this control belongs: it answers for every session tree, but
8
+ * it is read while looking at one, and a surface inside the layout is covered
9
+ * by whatever covers that column — a sidebar or a drawer — instead of floating
10
+ * above the whole frame. The decision list hangs from the control into the
11
+ * conversation area below.
12
+ *
13
+ * The pill reads the host through the command channel only (`./global.ts`):
14
+ * with no answer, or with no session to ask through, it renders nothing rather
15
+ * than a state it would have to invent. The decision list is deliberately NOT
16
+ * part of that first read — it costs two more command calls, and a user who
17
+ * never opens it pays nothing; the panel asks for its own rows when it opens.
18
+ * @module dsh-pause-session/client/pill
19
+ */
20
+ import type { ReactNode } from 'react';
21
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
22
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
23
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
24
+ import type { TriageAction } from '../shared.js';
25
+ import type { GlobalPauseState } from './global.js';
26
+ /** Browser state and operations injected into the whole-app pill. */
27
+ export interface PausePillInjected {
28
+ hooks: {
29
+ pauseGlobal: ObservableSnapshot<GlobalPauseState>;
30
+ };
31
+ /** Hand the control the session the shell is on; it reads there until it has an answer. */
32
+ refresh: (sessionId: SessionId) => void;
33
+ /** Press the pill for one session. */
34
+ toggle: (sessionId: SessionId) => void;
35
+ /** Show or hide the decision list; opening it also reads its rows. */
36
+ setPanel: (open: boolean) => void;
37
+ /** Take one of the four decisions on one listed session. */
38
+ decide: (sessionId: string, action: TriageAction) => void;
39
+ }
40
+ /** Full props of the whole-app pause control. */
41
+ export type PausePillProps = PropsRuntime<'conversation.session.header.utilities'> & InjectFace<PausePillInjected> & PropsLocale<'pause'>;
42
+ /**
43
+ * The whole-app control: what the host says about the whole park, the one
44
+ * press that parks or releases all of it, and the decision list for whatever
45
+ * did not stop cleanly.
46
+ * @param props - session-header runtime, host operations, and localized copy.
47
+ * @returns the control and its panel, or nothing while the host has not answered.
48
+ */
49
+ export declare function PausePill(props: PausePillProps): ReactNode;