dsh-pause-session 0.0.0-stage → 0.2.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/DEVELOPMENT.md +78 -0
  3. package/LICENSE +21 -0
  4. package/README.md +140 -2
  5. package/README.zh.md +101 -0
  6. package/REHEARSAL.md +90 -0
  7. package/cordis.patch.yml +3 -0
  8. package/docs/images/pause-controls.webp +0 -0
  9. package/icon.svg +5 -0
  10. package/lib/client.js +2024 -0
  11. package/lib/client.js.map +1 -0
  12. package/lib/index.js +2996 -0
  13. package/lib/index.js.map +1 -0
  14. package/lib/typert.host.d.ts +3 -0
  15. package/lib/typert.host.js +339 -0
  16. package/lib/typert.remote-client.d.ts +38 -0
  17. package/lib/typert.remote-client.d.ts.map +1 -0
  18. package/lib/typert.remote-client.js +244 -0
  19. package/lib/types/client/control.d.ts +99 -0
  20. package/lib/types/client/global.d.ts +125 -0
  21. package/lib/types/client/icons.d.ts +24 -0
  22. package/lib/types/client/index.d.ts +78 -0
  23. package/lib/types/client/locales.d.ts +67 -0
  24. package/lib/types/client/pill.d.ts +49 -0
  25. package/lib/types/client/remote.d.ts +71 -0
  26. package/lib/types/client/view.d.ts +151 -0
  27. package/lib/types/coldlog.d.ts +111 -0
  28. package/lib/types/composition.d.ts +94 -0
  29. package/lib/types/control.d.ts +95 -0
  30. package/lib/types/gate.d.ts +110 -0
  31. package/lib/types/global.d.ts +103 -0
  32. package/lib/types/history.d.ts +47 -0
  33. package/lib/types/index.d.ts +64 -0
  34. package/lib/types/park-state.d.ts +51 -0
  35. package/lib/types/park.d.ts +50 -0
  36. package/lib/types/registry.d.ts +117 -0
  37. package/lib/types/release-order.d.ts +21 -0
  38. package/lib/types/remote-adapter.d.ts +22 -0
  39. package/lib/types/remote.d.ts +158 -0
  40. package/lib/types/resume-activation.d.ts +27 -0
  41. package/lib/types/resume-text.d.ts +67 -0
  42. package/lib/types/resume.d.ts +126 -0
  43. package/lib/types/shared.d.ts +221 -0
  44. package/lib/types/snapshot.d.ts +29 -0
  45. package/lib/types/tree.d.ts +71 -0
  46. package/lib/types/triage.d.ts +120 -0
  47. package/locale/en.json +6 -0
  48. package/locale/zh.json +6 -0
  49. package/package.json +166 -3
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Browser-side reading and driving of the host's park state for one session.
3
+ *
4
+ * The browser reads the native `pauseSession` stream; actions travel through
5
+ * the same Remote namespace. No slash command is involved. The stream is
6
+ * host-authoritative, so this module never derives a state from its own press.
7
+ * The snapshot also carries the record's goal opt-out, so the switch beside the
8
+ * pause button can show it without a second host round-trip.
9
+ *
10
+ * The stream is the change feed. A pushed snapshot triggers one cached read per
11
+ * shown header; a press or goal switch reads the host's answer once after the
12
+ * action.
13
+ * @module dsh-pause-session/client/control
14
+ */
15
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
16
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
17
+ import type { PauseTransport } from './remote.js';
18
+ /**
19
+ * What the Session-header control knows about one session: what the host
20
+ * reported, or that no answer has arrived (`undefined`) or the last read
21
+ * failed. Neither of the latter claims a state the host never reported.
22
+ *
23
+ * `draining` and `parking` are the host's own two answers on the way to
24
+ * `safe`; the control shows them rather than rounding them up, because only
25
+ * `safe` means the pending input is durably stored.
26
+ */
27
+ export type PausePhase = 'active' | 'draining' | 'parking' | 'safe' | 'flush-failed' | 'unavailable';
28
+ /** One host answer about one session. */
29
+ export interface PauseReading {
30
+ readonly phase: PausePhase;
31
+ /** Tools the host reported in flight; empty unless the phase is `draining`. */
32
+ readonly runningTools: readonly string[];
33
+ /**
34
+ * Whether the host's own record lets the next resume re-open this session's
35
+ * goal. Absent — not `true` — when the host did not say: the control shows no
36
+ * switch rather than the product default, because a default here would tell
37
+ * the user their rounds are safe when nobody has answered.
38
+ */
39
+ readonly rearmGoal?: boolean;
40
+ }
41
+ /** Per-session control state, keyed by Session id so a slot selector can watch one session. */
42
+ export interface PauseState {
43
+ /** Reading per Session id; absent means no host answer yet. Writable: the store updates through a draft. */
44
+ bySession: Record<string, PauseReading | undefined>;
45
+ }
46
+ /** Owns the published reading per session; nothing else writes that state. */
47
+ export declare class PauseControl {
48
+ private readonly transport;
49
+ private readonly lifetime;
50
+ /** State source the Session-header contribution binds its selector hook to. */
51
+ readonly store: SnapshotStore<PauseState>;
52
+ /** Sessions this control has shown; a pushed snapshot applies to every one. */
53
+ private readonly shown;
54
+ /** Highest read sequence requested per session: an answer older than it is dropped. */
55
+ private readonly latest;
56
+ private reads;
57
+ /**
58
+ * @param transport - the native namespace source and actions.
59
+ * @param lifetime - aborts in-flight reads when the plugin unloads.
60
+ */
61
+ constructor(transport: PauseTransport, lifetime: AbortSignal);
62
+ /**
63
+ * Read the host's state for one session.
64
+ * @param sessionId - session whose control was rendered.
65
+ * @returns after this read's answer is published or superseded by a newer one.
66
+ */
67
+ refresh(sessionId: SessionId): Promise<void>;
68
+ /** Apply one pushed snapshot without spending a read per shown header. */
69
+ apply(snapshot: {
70
+ readonly sessions: Readonly<Record<string, {
71
+ readonly phase: PausePhase;
72
+ readonly runningTools: readonly string[];
73
+ readonly rearmGoal?: boolean;
74
+ }>>;
75
+ } | undefined): void;
76
+ /** A new pushed snapshot may have moved every header; read them once. */
77
+ refreshAll(): void;
78
+ private readAll;
79
+ /** One session's read, published unless a newer read of that session supersedes it. */
80
+ private readOne;
81
+ /**
82
+ * Press the control: pause an active session, or take back a pause that is
83
+ * still on its way in (or already settled). The phase shown afterwards is the
84
+ * host's answer, never an assumed flip.
85
+ * @param sessionId - session whose control was pressed.
86
+ * @returns after the control re-reads the host.
87
+ */
88
+ toggle(sessionId: SessionId): Promise<void>;
89
+ /**
90
+ * Flip this session's goal opt-out. The verb belongs to the session it
91
+ * changes; the switch shows the host's next answer, never a local flip.
92
+ * @param sessionId - session whose switch was pressed.
93
+ * @returns after the control re-reads the host.
94
+ */
95
+ toggleGoal(sessionId: SessionId): Promise<void>;
96
+ /** Ask the native stream what this session's park state is. */
97
+ private read;
98
+ private publish;
99
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Browser-side reading and driving of the host's whole-app park state.
3
+ *
4
+ * The aggregate arrives from the native `pauseSession` stream. The stream is
5
+ * process-wide and writes nothing into any Session; actions travel through the
6
+ * same Remote namespace. A pushed snapshot is applied directly; a settled park
7
+ * costs nothing while its last answer stays on screen.
8
+ * @module dsh-pause-session/client/global
9
+ */
10
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
11
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
12
+ import type { GlobalStatus, TriageAction, TriageRow } from '../shared.js';
13
+ import type { PauseTransport } from './remote.js';
14
+ /** What the whole-app control knows: the host's answer, or that it could not ask. */
15
+ export type GlobalReading = {
16
+ readonly kind: 'reported';
17
+ readonly status: GlobalStatus;
18
+ } | {
19
+ readonly kind: 'unavailable';
20
+ };
21
+ /** What the decision-list panel knows: the host's rows, or that it could not ask. */
22
+ export type TriageReading = {
23
+ readonly kind: 'reported';
24
+ readonly rows: readonly TriageRow[];
25
+ } | {
26
+ readonly kind: 'unavailable';
27
+ };
28
+ /** Whole-app control state. */
29
+ export interface GlobalPauseState {
30
+ /** The host's answer; absent until one arrives. Writable: the store updates through a draft. */
31
+ global: GlobalReading | undefined;
32
+ /** Whether the user asked for the decision list; the panel reads only when it is open. */
33
+ panelOpen: boolean;
34
+ /** The host's decision list; absent until the panel asks for it. */
35
+ triage: TriageReading | undefined;
36
+ /** The last action or read refusal, shown verbatim in the panel. */
37
+ error?: string;
38
+ /** The last snapshot's per-session details, for the draining progress copy. */
39
+ sessions?: Readonly<Record<string, {
40
+ readonly runningTools: readonly string[];
41
+ }>>;
42
+ }
43
+ /**
44
+ * The session a whole-app action is addressed to.
45
+ *
46
+ * A subagent's own session is never addressed: its agent belongs to a parent
47
+ * and is not the host's business to drive from the shell. Among the rest, the
48
+ * one on screen is the best address (its agent is live by construction), then
49
+ * any session the host reports as running, then any at all.
50
+ * @param state - the session list snapshot the shell provides.
51
+ * @returns the address, or `undefined` when the app has no session to ask through.
52
+ */
53
+ export declare function addressingSession(state: {
54
+ readonly ids: readonly SessionId[];
55
+ readonly byId: Readonly<Record<string, {
56
+ readonly running?: boolean;
57
+ readonly origin?: string;
58
+ readonly parentId?: unknown;
59
+ readonly retainedBy?: {
60
+ readonly mainView?: number;
61
+ };
62
+ } | undefined>>;
63
+ }): SessionId | undefined;
64
+ /** Owns the published whole-app reading; nothing else writes that state. */
65
+ export declare class PauseGlobalControl {
66
+ private readonly transport;
67
+ private readonly lifetime;
68
+ /** State source the whole-app contribution binds its selector hook to. */
69
+ readonly store: SnapshotStore<GlobalPauseState>;
70
+ /** The session the shell asked this control through; list reads use it. */
71
+ private address;
72
+ /** Highest decision-list read requested: an answer older than it is dropped. */
73
+ private latestTriage;
74
+ /**
75
+ * @param transport - the native namespace source and actions.
76
+ * @param lifetime - aborts in-flight reads when the plugin unloads.
77
+ */
78
+ constructor(transport: PauseTransport, lifetime: AbortSignal);
79
+ /**
80
+ * The shell handed this control a session. The first call reads the aggregate;
81
+ * later calls only move where list reads and actions are addressed.
82
+ * @param sessionId - the session the shell asks through.
83
+ */
84
+ refresh(sessionId: SessionId): Promise<void>;
85
+ /** Apply one pushed snapshot directly. */
86
+ apply(snapshot: {
87
+ readonly global: GlobalStatus;
88
+ readonly sessions: Readonly<Record<string, {
89
+ readonly runningTools: readonly string[];
90
+ }>>;
91
+ } | undefined): void;
92
+ /** A pushed snapshot may have moved the aggregate and the open list. */
93
+ refreshAll(): void;
94
+ /**
95
+ * Open or close the decision list. Opening asks for the rows; there is no
96
+ * state to show without that answer.
97
+ * @param open - whether the panel should be showing.
98
+ */
99
+ setPanel(open: boolean): void;
100
+ /** Ask the host for the decision list for the session the user is on. */
101
+ refreshTriage(): Promise<void>;
102
+ /**
103
+ * Take one listed verb on one listed session, addressed through the session
104
+ * the user is on. A refusal is published verbatim rather than silently
105
+ * leaving the previous rows in place.
106
+ * @param sessionId - the listed session to act on.
107
+ * @param action - which listed verb was taken.
108
+ */
109
+ decide(sessionId: string, action: TriageAction): Promise<void>;
110
+ /**
111
+ * Press the pill: park everything, or release everything the park owns.
112
+ * Which one it is comes from the host's own last answer, never from a local
113
+ * flip; the state shown afterwards is the host's answer again.
114
+ * @param sessionId - the session to address the verb to.
115
+ */
116
+ toggle(sessionId: SessionId): Promise<void>;
117
+ /** Read the aggregate once and publish it. */
118
+ private readAggregate;
119
+ private reread;
120
+ /** One decision-list read, addressed at the session the user is on. */
121
+ private readTriage;
122
+ /** Ask the native namespace for the decision list. */
123
+ private readTriageRows;
124
+ private publish;
125
+ }
@@ -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,78 @@
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. On a browser Host they read the
18
+ * native `pauseSession` stream, which keeps status reads out of the Session
19
+ * log. The same path serves Web and Desktop.
20
+ * User actions and status both use the generated native Remote namespace;
21
+ * no slash command is involved. Each control stays inert wherever no
22
+ * round-trip is available rather than rendering a state it cannot support. The
23
+ * header control also reads the whole-app answer the whole-app control
24
+ * publishes, so a park that holds nothing does not cost a host round-trip per
25
+ * header.
26
+ * @module dsh-pause-session/client
27
+ */
28
+ import type { ReactNode } from 'react';
29
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
30
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
31
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
32
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
33
+ import type { PauseState } from './control.js';
34
+ import { type PauseKey } from './locales.js';
35
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
36
+ interface LocaleNamespaceMap {
37
+ /** The pause control's copy. */
38
+ 'pause': PauseKey;
39
+ }
40
+ }
41
+ /** Required services: the slot registry, the locale registry, and the host round-trip. */
42
+ export declare const inject: string[];
43
+ /** Browser state and operations injected into the Session-header contribution. */
44
+ export interface PauseInjected {
45
+ hooks: {
46
+ pause: ObservableSnapshot<PauseState>;
47
+ };
48
+ /** Read the host's state for one session (the control calls this once per mount). */
49
+ refresh: (sessionId: SessionId) => void;
50
+ /** Press the control for one session. */
51
+ toggle: (sessionId: SessionId) => void;
52
+ /** Flip one session's own goal opt-out. */
53
+ toggleGoal: (sessionId: SessionId) => void;
54
+ }
55
+ /** Full props of the Session-header pause control. */
56
+ export type PauseButtonProps = PropsRuntime<'conversation.session.header.actions'> & InjectFace<PauseInjected> & PropsLocale<'pause'>;
57
+ /**
58
+ * The session's pause control: pause the session and its subagents before
59
+ * their next model request, or resume them, with the host's own state on screen.
60
+ * Once the host reports the park's goal opt-out, a second switch sits beside it
61
+ * — the only place a session that stopped CLEANLY can be told not to re-open its
62
+ * goal on the next resume, because the decision list shows only the sessions
63
+ * that did not stop cleanly.
64
+ *
65
+ * A subagent session renders nothing: its work is its parent's, the parent's
66
+ * control already covers it, and a button here would offer to pause "a session"
67
+ * from inside a child.
68
+ * @param props - session runtime, host operations, and localized copy.
69
+ * @returns the header action, or nothing on a subagent session.
70
+ */
71
+ export declare function PauseButton(props: PauseButtonProps): ReactNode;
72
+ /**
73
+ * Client plugin body: register the locale dictionaries, own both controllers,
74
+ * follow the host's registry changes, and contribute the per-session control
75
+ * and the whole-app control into the session header.
76
+ * @param ctx - client context carrying slots, locale, and the Remote face.
77
+ */
78
+ export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,67 @@
1
+ /** `pause` namespace dictionaries. */
2
+ /** Simplified Chinese dictionary (the key-set source of truth). */
3
+ export declare const zh: {
4
+ 'pause.aria': string;
5
+ 'resume.tip': string;
6
+ 'draining.aria': string;
7
+ 'parking.aria': string;
8
+ 'safe.aria': string;
9
+ 'flushFailed.aria': string;
10
+ 'state.draining': string;
11
+ 'state.parking': string;
12
+ 'state.safe': string;
13
+ 'state.flushFailed': string;
14
+ 'goal.aria': string;
15
+ 'goal.on.tip': string;
16
+ 'goal.off.tip': string;
17
+ 'checking.aria': string;
18
+ 'checking.tip': string;
19
+ 'unavailable.aria': string;
20
+ 'unavailable.tip': string;
21
+ 'all.pause.aria': string;
22
+ 'all.resume.aria': string;
23
+ 'all.idle': string;
24
+ 'all.finishing': string;
25
+ 'all.cancel': string;
26
+ 'all.cancel.aria': string;
27
+ 'all.unclean': string;
28
+ 'all.unclean.aria': string;
29
+ 'all.safe': string;
30
+ 'all.checking.aria': string;
31
+ 'all.caveat.jobs': string;
32
+ 'all.caveat.unresumable': string;
33
+ 'all.caveat.stalled': string;
34
+ 'all.caveat.unread': string;
35
+ 'all.caveat.blindTrees': string;
36
+ 'all.caveat.blindJobs': string;
37
+ 'triage.open.aria': string;
38
+ 'triage.title': string;
39
+ 'triage.empty': string;
40
+ 'triage.reading': string;
41
+ 'triage.unavailable': string;
42
+ 'triage.refresh.aria': string;
43
+ 'triage.session.aria': string;
44
+ 'triage.stopped': string;
45
+ 'triage.stoppedUnknown': string;
46
+ 'triage.tools': string;
47
+ 'triage.toolsNone': string;
48
+ 'triage.noWake': string;
49
+ 'triage.phase.draining': string;
50
+ 'triage.phase.parking': string;
51
+ 'triage.phase.flushFailed': string;
52
+ 'triage.continue': string;
53
+ 'triage.continue.aria': string;
54
+ 'triage.note': string;
55
+ 'triage.note.aria': string;
56
+ 'triage.review': string;
57
+ 'triage.review.aria': string;
58
+ 'triage.leave': string;
59
+ 'triage.leave.aria': string;
60
+ 'triage.goal': string;
61
+ 'triage.goal.aria': string;
62
+ 'all.triage': string;
63
+ };
64
+ /** The pause namespace key union. */
65
+ export type PauseKey = keyof typeof zh;
66
+ /** English dictionary, checked complete against the zh key set. */
67
+ 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 native status stream through `./global.ts`. With no
14
+ * answer, or with no session to address an action through, it renders nothing
15
+ * rather than a state it would have to invent. The decision list is
16
+ * deliberately NOT part of that first read; the panel asks for its own rows
17
+ * 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;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Client-side bridge from the generated `pauseSession` Remote namespace to the
3
+ * control state machines.
4
+ *
5
+ * The native namespace is the only transport: the stream supplies snapshots and
6
+ * the action methods drive every button. No slash command is involved.
7
+ * @module dsh-pause-session/client/remote
8
+ */
9
+ import type { PauseActionResultWire, PauseDecisionRequest, PauseSnapshotWire, TriageRowWire } from '../remote.js';
10
+ import type { GlobalStatus, ParkStatus } from '../shared.js';
11
+ /** The generated namespace shape this bridge needs. */
12
+ export interface RemotePauseNamespace {
13
+ observe(signal?: AbortSignal): AsyncIterable<PauseSnapshotWire>;
14
+ act(request: {
15
+ readonly action: 'pause' | 'resume' | 'pause-all' | 'resume-all' | 'cancel-all' | 'goal';
16
+ readonly sessionId: string;
17
+ }, signal?: AbortSignal): Promise<{
18
+ readonly ok: boolean;
19
+ readonly value?: PauseActionResultWire;
20
+ readonly error?: {
21
+ readonly code: string;
22
+ };
23
+ }>;
24
+ triage(request: {
25
+ readonly caller: string;
26
+ }, signal?: AbortSignal): Promise<{
27
+ readonly ok: boolean;
28
+ readonly value?: readonly TriageRowWire[];
29
+ }>;
30
+ decide(request: PauseDecisionRequest, signal?: AbortSignal): Promise<{
31
+ readonly ok: boolean;
32
+ readonly value?: PauseActionResultWire;
33
+ }>;
34
+ }
35
+ /** The status shape shared by the controls, with branded ids erased at this boundary. */
36
+ export interface PauseSnapshotLike {
37
+ readonly global: GlobalStatus;
38
+ readonly sessions: Readonly<Record<string, ParkStatus>>;
39
+ }
40
+ /** A remote-backed status source with a change subscription. */
41
+ export interface RemoteStatusSource {
42
+ /** Read the latest host snapshot; undefined before the first answer or after failure. */
43
+ read(signal?: AbortSignal): Promise<PauseSnapshotLike | undefined>;
44
+ onChange(listener: () => void): () => void;
45
+ /** Receive each pushed snapshot directly; `undefined` means the stream failed. */
46
+ onSnapshot(listener: (snapshot: PauseSnapshotLike | undefined) => void): () => void;
47
+ }
48
+ /**
49
+ * Open the native status stream and expose its latest snapshot to both controls.
50
+ * @param remote - the generated Remote namespace.
51
+ * @param lifetime - client lifetime; aborting it releases the stream.
52
+ * @returns the cached source and its change subscription.
53
+ */
54
+ export declare function openRemoteSource(remote: RemotePauseNamespace, lifetime: AbortSignal): RemoteStatusSource;
55
+ /** Direct browser transport over the generated namespace. */
56
+ export interface PauseTransport {
57
+ readonly source: RemoteStatusSource;
58
+ act(request: {
59
+ readonly action: 'pause' | 'resume' | 'pause-all' | 'resume-all' | 'cancel-all' | 'goal';
60
+ readonly sessionId: string;
61
+ }, signal?: AbortSignal): Promise<PauseActionResultWire>;
62
+ triage(caller: string, signal?: AbortSignal): Promise<readonly TriageRowWire[] | undefined>;
63
+ decide(caller: string, target: string, decision: 'continue' | 'note' | 'review' | 'leave' | 'goal', signal?: AbortSignal): Promise<PauseActionResultWire>;
64
+ }
65
+ /**
66
+ * Build the direct transport over one mounted Remote namespace.
67
+ * @param remote - the generated `pauseSession` namespace.
68
+ * @param lifetime - client lifetime.
69
+ * @returns the source and action methods the controls use.
70
+ */
71
+ export declare function remoteTransport(remote: RemotePauseNamespace, lifetime: AbortSignal): PauseTransport;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The controls' phase → view mapping.
3
+ *
4
+ * Kept pure functions of the published reading so the rendering decision — what
5
+ * the button says, whether it accepts a press, and which state label is visible
6
+ * — is asserted without a browser renderer. Labels are KEYS, not copy: the
7
+ * visible sentence is localized, and the host's own names join it as data.
8
+ * @module dsh-pause-session/client/view
9
+ */
10
+ import type { GlobalReading, TriageReading } from './global.js';
11
+ import type { PauseReading } from './control.js';
12
+ import type { PauseKey } from './locales.js';
13
+ import type { TriageAction } from '../shared.js';
14
+ /** What one reading renders as. */
15
+ export interface PauseView {
16
+ /** Whether the host reported this session paused. */
17
+ readonly paused: boolean;
18
+ /** Whether the control accepts a press; an unknown or failed reading does not. */
19
+ readonly enabled: boolean;
20
+ /** Accessible name of the button. */
21
+ readonly ariaKey: PauseKey;
22
+ /** Label shown beside the button while the control has something to say. */
23
+ readonly labelKey?: PauseKey;
24
+ /** Tools in flight, taken verbatim from the host; rendered only with `labelKey`. */
25
+ readonly runningTools: readonly string[];
26
+ /**
27
+ * The host-reported goal opt-out, or `undefined` when the host did not answer
28
+ * it. Absent means the goal switch is not rendered at all: a switch has to
29
+ * state a fact, and the only fact available is the one the record holds.
30
+ */
31
+ readonly rearmGoal?: boolean;
32
+ }
33
+ /**
34
+ * Whether the pause control renders on one session at all.
35
+ *
36
+ * The control is the user's button for a session's work, and that work includes
37
+ * the subagents under it — which is why the press covers the tree and why the
38
+ * button does not belong inside the tree. The client can tell: a Session
39
+ * snapshot carries a `subagent` field, the same signal the read-only composer
40
+ * reads. A subagent's service is not the user's to pause: its parent's control
41
+ * already speaks for it, and a second button here would offer to stop "a
42
+ * session" from inside a child.
43
+ * @param subagent - the session snapshot's own subagent fact, absent when unknown.
44
+ * @returns whether this session's header renders the control.
45
+ */
46
+ export declare function showsPauseControl(subagent: {
47
+ readonly address: unknown;
48
+ } | null | undefined): boolean;
49
+ /**
50
+ * Map one reading to its view.
51
+ * @param reading - the host-reported reading, or `undefined` before the first answer.
52
+ * @returns the view for that reading.
53
+ */
54
+ export declare function pauseView(reading: PauseReading | undefined, running?: boolean): PauseView;
55
+ /** One named fact from the whole-app report, with the copy key that introduces it. */
56
+ export interface GlobalCaveat {
57
+ /** What this list of names is. */
58
+ readonly key: PauseKey;
59
+ /** Host names, rendered verbatim. */
60
+ readonly names: readonly string[];
61
+ }
62
+ /** Which verb a whole-app press runs. */
63
+ export type GlobalAction = 'pause-all' | 'resume-all' | 'cancel-all' | 'none';
64
+ /** One decision as the list renders it. */
65
+ export interface TriageActionView {
66
+ /** The decision this button takes. */
67
+ readonly action: TriageAction;
68
+ /** Visible label. */
69
+ readonly labelKey: PauseKey;
70
+ /** What it does, for the accessible name. */
71
+ readonly ariaKey: PauseKey;
72
+ }
73
+ /** The four decisions, in the order a user reads them: least to most cautious. */
74
+ export declare const TRIAGE_ACTION_VIEWS: readonly TriageActionView[];
75
+ /**
76
+ * The decisions one row may offer. Continue and note both deliver the owed
77
+ * request, so they are absent when the log is unreadable or the host says it
78
+ * cannot wake the entry; the remaining decisions still give the user a way out.
79
+ * @param row - the rendered row.
80
+ * @returns the actions that can be taken on it.
81
+ */
82
+ export declare function triageActionsFor(row: TriageRowView): readonly TriageActionView[];
83
+ /**
84
+ * The fifth listed verb, rendered apart from the four decisions: it is a
85
+ * switch on what the NEXT resume may spend, not a way to decide this row.
86
+ */
87
+ export declare const TRIAGE_GOAL_VIEW: TriageActionView;
88
+ /** One listed session, as the panel renders it. */
89
+ export interface TriageRowView {
90
+ /** Host data, rendered verbatim. */
91
+ readonly sessionId: string;
92
+ /** The durable outcome, as a word. */
93
+ readonly phaseKey: PauseKey;
94
+ /** Stop position: the event the log ends on and the step it belongs to. */
95
+ readonly stopped?: string;
96
+ /** The tools whose outcomes were never recorded. */
97
+ readonly unknownTools: readonly string[];
98
+ /** Whether the host said it can hand this session the request it owes. */
99
+ readonly wakeable: boolean;
100
+ /** Whether the log could be read at all. */
101
+ readonly readable: boolean;
102
+ /** Whether resuming this session may re-open its goal and spend its rounds. */
103
+ readonly rearmGoal: boolean;
104
+ }
105
+ /** What the decision-list panel renders. */
106
+ export interface TriageView {
107
+ /** Whether the host answered the list read at all. */
108
+ readonly visible: boolean;
109
+ /** Whether the last list read failed rather than returning rows. */
110
+ readonly unavailable: boolean;
111
+ /** The rows the host reported. */
112
+ readonly rows: readonly TriageRowView[];
113
+ }
114
+ /**
115
+ * Map one decision-list reading to its view.
116
+ * @param reading - the host-reported list, or `undefined` before the panel asked.
117
+ * @returns the rows as the panel renders them.
118
+ */
119
+ export declare function triageView(reading: TriageReading | undefined): TriageView;
120
+ /** What the whole-app reading renders as. */
121
+ export interface GlobalPauseView {
122
+ /** Whether the control renders at all; a control with no host answer claims nothing. */
123
+ readonly visible: boolean;
124
+ /** Whether the control accepts a press. */
125
+ readonly enabled: boolean;
126
+ /** Accessible name of the button. */
127
+ readonly ariaKey: PauseKey;
128
+ /**
129
+ * Headline shown beside the button. Absent while there is nothing to report:
130
+ * an untouched park renders the bare affordance, not a sentence about itself.
131
+ */
132
+ readonly labelKey?: PauseKey;
133
+ /** Verb the press runs. */
134
+ readonly action: GlobalAction;
135
+ /** Host numbers, rendered verbatim beside the label. */
136
+ readonly detail: string;
137
+ /** How many decisions are waiting; shown on the panel's own entry point. */
138
+ readonly unclean: number;
139
+ /** Named facts the user must not miss; rendered on the tooltip. */
140
+ readonly caveats: readonly GlobalCaveat[];
141
+ /** Draining sessions and what each is running, when the host still has work. */
142
+ readonly running?: string;
143
+ }
144
+ /**
145
+ * Map one whole-app reading to its view.
146
+ * @param reading - the host-reported reading, or `undefined` before the first answer.
147
+ * @returns the view for that reading.
148
+ */
149
+ export declare function globalPauseView(reading: GlobalReading | undefined, sessions?: Readonly<Record<string, {
150
+ readonly runningTools: readonly string[];
151
+ }>>): GlobalPauseView;