@haywoodmarx/agent-pager 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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/src/broker/address.d.ts +49 -0
  4. package/dist/src/broker/address.js +110 -0
  5. package/dist/src/broker/broker.d.ts +665 -0
  6. package/dist/src/broker/broker.js +2270 -0
  7. package/dist/src/broker/database.d.ts +81 -0
  8. package/dist/src/broker/database.js +69 -0
  9. package/dist/src/broker/operator-projection.d.ts +291 -0
  10. package/dist/src/broker/operator-projection.js +722 -0
  11. package/dist/src/broker/protocol.d.ts +237 -0
  12. package/dist/src/broker/protocol.js +311 -0
  13. package/dist/src/broker/schema.d.ts +94 -0
  14. package/dist/src/broker/schema.js +454 -0
  15. package/dist/src/broker/snapshot.d.ts +171 -0
  16. package/dist/src/broker/snapshot.js +78 -0
  17. package/dist/src/configuration-defaults.d.ts +64 -0
  18. package/dist/src/configuration-defaults.js +48 -0
  19. package/dist/src/configuration.d.ts +76 -0
  20. package/dist/src/configuration.js +156 -0
  21. package/dist/src/implementations/claude/environment.d.ts +8 -0
  22. package/dist/src/implementations/claude/environment.js +9 -0
  23. package/dist/src/implementations/claude/gateway-cli.d.ts +2 -0
  24. package/dist/src/implementations/claude/gateway-cli.js +136 -0
  25. package/dist/src/implementations/claude/gateway-identity.d.ts +134 -0
  26. package/dist/src/implementations/claude/gateway-identity.js +103 -0
  27. package/dist/src/implementations/claude/gateway.d.ts +148 -0
  28. package/dist/src/implementations/claude/gateway.js +236 -0
  29. package/dist/src/implementations/claude/instructions.d.ts +33 -0
  30. package/dist/src/implementations/claude/instructions.js +64 -0
  31. package/dist/src/implementations/claude/manifest.d.ts +33 -0
  32. package/dist/src/implementations/claude/manifest.js +108 -0
  33. package/dist/src/implementations/claude/plugin-directory.d.ts +80 -0
  34. package/dist/src/implementations/claude/plugin-directory.js +137 -0
  35. package/dist/src/implementations/claude/session-continuation.d.ts +25 -0
  36. package/dist/src/implementations/claude/session-continuation.js +125 -0
  37. package/dist/src/implementations/claude/session-identity.d.ts +25 -0
  38. package/dist/src/implementations/claude/session-identity.js +31 -0
  39. package/dist/src/implementations/claude/session-start-cli.d.ts +2 -0
  40. package/dist/src/implementations/claude/session-start-cli.js +16 -0
  41. package/dist/src/implementations/claude/socket-delivery.d.ts +52 -0
  42. package/dist/src/implementations/claude/socket-delivery.js +159 -0
  43. package/dist/src/implementations/model.d.ts +162 -0
  44. package/dist/src/implementations/model.js +1 -0
  45. package/dist/src/implementations/opencode/adapter.d.ts +71 -0
  46. package/dist/src/implementations/opencode/adapter.js +94 -0
  47. package/dist/src/implementations/opencode/environment.d.ts +8 -0
  48. package/dist/src/implementations/opencode/environment.js +9 -0
  49. package/dist/src/implementations/opencode/manifest.d.ts +30 -0
  50. package/dist/src/implementations/opencode/manifest.js +93 -0
  51. package/dist/src/implementations/opencode/plugin-api.d.ts +120 -0
  52. package/dist/src/implementations/opencode/plugin-api.js +1 -0
  53. package/dist/src/implementations/opencode/plugin.d.ts +38 -0
  54. package/dist/src/implementations/opencode/plugin.js +303 -0
  55. package/dist/src/implementations/opencode/self-close-tool.d.ts +9 -0
  56. package/dist/src/implementations/opencode/self-close-tool.js +31 -0
  57. package/dist/src/implementations/orca/terminal.d.ts +48 -0
  58. package/dist/src/implementations/orca/terminal.js +154 -0
  59. package/dist/src/implementations/pi/broker-client.d.ts +56 -0
  60. package/dist/src/implementations/pi/broker-client.js +156 -0
  61. package/dist/src/implementations/pi/environment.d.ts +8 -0
  62. package/dist/src/implementations/pi/environment.js +9 -0
  63. package/dist/src/implementations/pi/extension.d.ts +57 -0
  64. package/dist/src/implementations/pi/extension.js +253 -0
  65. package/dist/src/implementations/pi/host-slots.d.ts +18 -0
  66. package/dist/src/implementations/pi/host-slots.js +27 -0
  67. package/dist/src/implementations/pi/manifest.d.ts +11 -0
  68. package/dist/src/implementations/pi/manifest.js +61 -0
  69. package/dist/src/implementations/pi/self-close-tool.d.ts +7 -0
  70. package/dist/src/implementations/pi/self-close-tool.js +27 -0
  71. package/dist/src/implementations/registry.d.ts +47 -0
  72. package/dist/src/implementations/registry.js +157 -0
  73. package/dist/src/implementations/session-key.d.ts +13 -0
  74. package/dist/src/implementations/session-key.js +19 -0
  75. package/dist/src/index.d.ts +27 -0
  76. package/dist/src/index.js +26 -0
  77. package/dist/src/launch/broker-lock.d.ts +257 -0
  78. package/dist/src/launch/broker-lock.js +644 -0
  79. package/dist/src/launch/cli.d.ts +37 -0
  80. package/dist/src/launch/cli.js +317 -0
  81. package/dist/src/launch/environment.d.ts +45 -0
  82. package/dist/src/launch/environment.js +48 -0
  83. package/dist/src/launch/launch.d.ts +235 -0
  84. package/dist/src/launch/launch.js +223 -0
  85. package/dist/src/launch/on-demand.d.ts +202 -0
  86. package/dist/src/launch/on-demand.js +435 -0
  87. package/dist/src/launch/runtime-composition.d.ts +88 -0
  88. package/dist/src/launch/runtime-composition.js +253 -0
  89. package/dist/src/launch/runtime.d.ts +79 -0
  90. package/dist/src/launch/runtime.js +196 -0
  91. package/dist/src/launch/service-system.d.ts +11 -0
  92. package/dist/src/launch/service-system.js +39 -0
  93. package/dist/src/launch/service.d.ts +173 -0
  94. package/dist/src/launch/service.js +492 -0
  95. package/dist/src/launch/skill.d.ts +57 -0
  96. package/dist/src/launch/skill.js +81 -0
  97. package/dist/src/launch/terminal.d.ts +26 -0
  98. package/dist/src/launch/terminal.js +43 -0
  99. package/dist/src/launch/workspace-branch.d.ts +10 -0
  100. package/dist/src/launch/workspace-branch.js +29 -0
  101. package/dist/src/messaging/delivery-client.d.ts +144 -0
  102. package/dist/src/messaging/delivery-client.js +261 -0
  103. package/dist/src/messaging/delivery.d.ts +75 -0
  104. package/dist/src/messaging/delivery.js +22 -0
  105. package/dist/src/messaging/inbox-socket.d.ts +98 -0
  106. package/dist/src/messaging/inbox-socket.js +278 -0
  107. package/dist/src/messaging/mcp.d.ts +53 -0
  108. package/dist/src/messaging/mcp.js +501 -0
  109. package/dist/src/messaging/notifications.d.ts +231 -0
  110. package/dist/src/messaging/notifications.js +489 -0
  111. package/dist/src/messaging/push-delivery.d.ts +77 -0
  112. package/dist/src/messaging/push-delivery.js +187 -0
  113. package/dist/src/messaging/tools.d.ts +11 -0
  114. package/dist/src/messaging/tools.js +102 -0
  115. package/dist/src/messaging/wake.d.ts +23 -0
  116. package/dist/src/messaging/wake.js +39 -0
  117. package/dist/src/operator/agent-projection.d.ts +102 -0
  118. package/dist/src/operator/agent-projection.js +249 -0
  119. package/dist/src/operator/commands.d.ts +50 -0
  120. package/dist/src/operator/commands.js +199 -0
  121. package/dist/src/operator/diagnostics.d.ts +21 -0
  122. package/dist/src/operator/diagnostics.js +22 -0
  123. package/dist/src/operator/protocol.d.ts +511 -0
  124. package/dist/src/operator/protocol.js +49 -0
  125. package/dist/src/operator/repository-system.d.ts +14 -0
  126. package/dist/src/operator/repository-system.js +150 -0
  127. package/dist/src/operator/repository.d.ts +79 -0
  128. package/dist/src/operator/repository.js +72 -0
  129. package/dist/src/operator/traffic-projection.d.ts +19 -0
  130. package/dist/src/operator/traffic-projection.js +229 -0
  131. package/dist/src/operator/transport.d.ts +87 -0
  132. package/dist/src/operator/transport.js +549 -0
  133. package/dist/src/operator/update-store.d.ts +53 -0
  134. package/dist/src/operator/update-store.js +162 -0
  135. package/dist/src/operator/updates.d.ts +94 -0
  136. package/dist/src/operator/updates.js +280 -0
  137. package/dist/src/operator/validation.d.ts +114 -0
  138. package/dist/src/operator/validation.js +916 -0
  139. package/dist/src/shared/guards.d.ts +59 -0
  140. package/dist/src/shared/guards.js +119 -0
  141. package/dist/src/shared/http.d.ts +30 -0
  142. package/dist/src/shared/http.js +50 -0
  143. package/dist/src/shared/row.d.ts +2 -0
  144. package/dist/src/shared/row.js +1 -0
  145. package/dist/src/shared/shell.d.ts +6 -0
  146. package/dist/src/shared/shell.js +8 -0
  147. package/dist/src/tui/actions.d.ts +166 -0
  148. package/dist/src/tui/actions.js +253 -0
  149. package/dist/src/tui/agent-tree.d.ts +125 -0
  150. package/dist/src/tui/agent-tree.js +663 -0
  151. package/dist/src/tui/app.d.ts +38 -0
  152. package/dist/src/tui/app.js +378 -0
  153. package/dist/src/tui/chrome.d.ts +86 -0
  154. package/dist/src/tui/chrome.js +85 -0
  155. package/dist/src/tui/client.d.ts +41 -0
  156. package/dist/src/tui/client.js +279 -0
  157. package/dist/src/tui/composition.d.ts +41 -0
  158. package/dist/src/tui/composition.js +146 -0
  159. package/dist/src/tui/message-views.d.ts +94 -0
  160. package/dist/src/tui/message-views.js +515 -0
  161. package/dist/src/tui/model.d.ts +414 -0
  162. package/dist/src/tui/model.js +758 -0
  163. package/dist/src/tui/presentation.d.ts +350 -0
  164. package/dist/src/tui/presentation.js +541 -0
  165. package/dist/src/tui/production.d.ts +79 -0
  166. package/dist/src/tui/production.js +253 -0
  167. package/dist/src/tui/run-terminal.d.ts +32 -0
  168. package/dist/src/tui/run-terminal.js +54 -0
  169. package/dist/src/tui/runtime.d.ts +113 -0
  170. package/dist/src/tui/runtime.js +189 -0
  171. package/dist/src/tui/theme.d.ts +49 -0
  172. package/dist/src/tui/theme.js +55 -0
  173. package/dist/src/tui/transport.d.ts +31 -0
  174. package/dist/src/tui/transport.js +255 -0
  175. package/dist/src/version.d.ts +11 -0
  176. package/dist/src/version.js +52 -0
  177. package/package.json +142 -0
  178. package/skills/pager/SKILL.md +112 -0
@@ -0,0 +1,253 @@
1
+ import { createElement } from "react";
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { isPlainObject } from "../shared/guards.js";
6
+ import { OperatorTui } from "./app.js";
7
+ import { TuiOperatorTcpClient } from "./client.js";
8
+ import { startOpenTuiFactory } from "./composition.js";
9
+ import { TuiOperatorModel } from "./model.js";
10
+ import { messageBodyDisplayText } from "./presentation.js";
11
+ import { defineTuiTheme } from "./theme.js";
12
+ /** Canonical semantic palette selected by the production terminal command. */
13
+ export const DEFAULT_OPERATOR_TUI_THEME = defineTuiTheme({
14
+ structure: {
15
+ background: "#0c0e0f",
16
+ selection: { background: "#6d7275", text: "#0c0e0f" },
17
+ dimBackdrop: "#4a5053",
18
+ rulesAndLabels: "#6d7275",
19
+ bodyText: "#c2c0b8",
20
+ emphasis: "#e6e4dc",
21
+ },
22
+ agents: ["#7fa8a8", "#b090c0", "#8fae7f", "#7f9ec2", "#c39a6b", "#c2857f"],
23
+ states: { complete: "#7fa87f", inProgress: "#c2a05f", failed: "#c25f5f", expired: "#6d7275" },
24
+ });
25
+ /**
26
+ * Start the production terminal surface from explicitly composed client and presentation ports.
27
+ * @param options - Model, presentation ports, and optional renderer configuration.
28
+ * @param start - Starts the renderer, supplying the shutdown action to the factory that builds the root element; the OpenTUI starter by default.
29
+ * @returns Running renderer lifecycle whose caller owns the final shutdown request.
30
+ */
31
+ export function startOperatorTui(options, start = startOpenTuiFactory) {
32
+ return start((shutdown) => createElement(OperatorTui, {
33
+ ...options,
34
+ onShutdown: shutdown,
35
+ }), options.runtime);
36
+ }
37
+ /**
38
+ * Build the production model and retained-body adapter from the published operator state endpoint.
39
+ * @param options - Endpoint, palette, and optional clipboard, local-command, and layout ports.
40
+ * @returns The shell's model, palette, body reader, copy fallback and command port, without its shutdown action or action catalogue.
41
+ */
42
+ export function createOperatorTuiClient({ operatorUrl, theme, clipboard = systemNativeClipboard(), localCommand = systemLocalCommand(), layout, }) {
43
+ const client = new TuiOperatorTcpClient(operatorUrl);
44
+ const model = new TuiOperatorModel({
45
+ snapshot: { fetch: () => client.fetch() },
46
+ stream: { open: (cursor, receive, unavailable) => client.open(cursor, receive, unavailable) },
47
+ reconnect: { reconnect: () => client.reconnect() },
48
+ command: {
49
+ execute: (command) => client.execute(command),
50
+ },
51
+ scheduler: createTimerScheduler(setTimeout, clearTimeout),
52
+ localCommand,
53
+ disposal: { dispose: () => client.dispose() },
54
+ ...(layout ? { layout } : {}),
55
+ now: () => Date.now(),
56
+ });
57
+ return {
58
+ model,
59
+ theme,
60
+ body: retainedOperatorBody,
61
+ copyFallback: { copyUnavailable: (rawCopy) => (clipboard.copy(rawCopy) ? "copied" : "unavailable") },
62
+ commands: { commandId: () => crypto.randomUUID(), issuedAt: () => Date.now(), observe: () => undefined },
63
+ };
64
+ }
65
+ /**
66
+ * Keep dragged pane widths in a file beside the operator's other local state.
67
+ *
68
+ * Every failure resolves to "no stored layout": the file is a convenience, and an operator locked out
69
+ * of the surface because a width could not be parsed would be worse off than one whose panes reset.
70
+ * @param storeDirectory - Directory the operator's local state lives in.
71
+ * @returns - Port reading and writing that directory's layout file.
72
+ */
73
+ export function fileBackedTuiLayout(storeDirectory) {
74
+ const path = join(storeDirectory, "tui-layout.json");
75
+ return {
76
+ read() {
77
+ try {
78
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
79
+ if (!isPlainObject(parsed))
80
+ return null;
81
+ return {
82
+ treeColumns: paneColumns(parsed.treeColumns),
83
+ messageLogColumns: paneColumns(parsed.messageLogColumns),
84
+ };
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ },
90
+ write(layout) {
91
+ try {
92
+ mkdirSync(dirname(path), { recursive: true });
93
+ writeFileSync(path, JSON.stringify(layout));
94
+ }
95
+ catch {
96
+ return;
97
+ }
98
+ },
99
+ };
100
+ }
101
+ function paneColumns(value) {
102
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
103
+ }
104
+ /**
105
+ * Run a local command attached to a pseudo-terminal, streaming its raw bytes to the model.
106
+ *
107
+ * Output is captured rather than inherited: the renderer owns the terminal, so a child writing to it
108
+ * directly would overwrite the frame.
109
+ *
110
+ * The child is given a pseudo-terminal rather than pipes. Seeing pipes, a child reports no terminal
111
+ * and takes whatever silent non-interactive path it has, so an operator could never answer a question
112
+ * it asks.
113
+ * @returns - Port that spawns through the operating system.
114
+ */
115
+ export function systemLocalCommand() {
116
+ return {
117
+ run(command, args, size, observe) {
118
+ let settled = false;
119
+ const settle = (event) => {
120
+ if (settled)
121
+ return;
122
+ settled = true;
123
+ observe(event);
124
+ };
125
+ // Every byte is reported before the exit whichever of the process's end and the terminal's end of
126
+ // file Bun dispatches first, since the exit waits for both.
127
+ let exitCode = null;
128
+ let drained = null;
129
+ const finish = () => {
130
+ if (exitCode === null || drained === null)
131
+ return;
132
+ if (!drained.closed)
133
+ drained.close();
134
+ settle({ type: "exit", code: exitCode });
135
+ };
136
+ let child;
137
+ try {
138
+ // Given inline, the terminal's `exit` is the end-of-file signal `finish` waits for.
139
+ child = Bun.spawn([command, ...args], {
140
+ terminal: {
141
+ cols: size.columns,
142
+ rows: size.rows,
143
+ data(_terminal, chunk) {
144
+ observe({ type: "output", bytes: chunk });
145
+ },
146
+ exit(terminal) {
147
+ drained = terminal;
148
+ finish();
149
+ },
150
+ },
151
+ });
152
+ }
153
+ catch (error) {
154
+ settle({ type: "failed", reason: error instanceof Error ? error.message : String(error) });
155
+ return null;
156
+ }
157
+ const pty = child.terminal;
158
+ if (pty === undefined) {
159
+ child.kill();
160
+ settle({ type: "failed", reason: "the command was started without a terminal" });
161
+ return null;
162
+ }
163
+ void child.exited.then((code) => {
164
+ exitCode = code;
165
+ finish();
166
+ }, (error) => {
167
+ pty.close();
168
+ settle({ type: "failed", reason: error instanceof Error ? error.message : String(error) });
169
+ });
170
+ return {
171
+ write(bytes) {
172
+ if (!settled && !pty.closed)
173
+ pty.write(bytes);
174
+ },
175
+ resize(columns, rows) {
176
+ if (!settled && !pty.closed)
177
+ pty.resize(columns, rows);
178
+ },
179
+ stop() {
180
+ if (settled)
181
+ return;
182
+ const spawned = child;
183
+ spawned.kill();
184
+ if (!pty.closed)
185
+ pty.close();
186
+ },
187
+ };
188
+ },
189
+ };
190
+ }
191
+ /**
192
+ * Build the reconnect scheduler over an explicit timer, so the delay the model asks for is observable.
193
+ * @param set - Arms one timer for the requested delay and returns the handle that cancels it.
194
+ * @param clear - Cancels a handle `set` returned, whether or not it has already fired.
195
+ * @returns Scheduler port whose cancellation callback clears the handle it armed.
196
+ */
197
+ export function createTimerScheduler(set, clear) {
198
+ return {
199
+ schedule: (callback, delayMs) => {
200
+ const handle = set(callback, delayMs);
201
+ return () => clear(handle);
202
+ },
203
+ };
204
+ }
205
+ function systemNativeClipboard() {
206
+ return createNativeClipboardPort(process.platform, (command, arguments_, rawCopy, timeout) => {
207
+ const result = spawnSync(command, [...arguments_], {
208
+ input: rawCopy,
209
+ stdio: ["pipe", "ignore", "ignore"],
210
+ timeout,
211
+ });
212
+ return !result.error && result.status === 0;
213
+ });
214
+ }
215
+ /**
216
+ * Build a bounded native clipboard port from explicit platform command execution.
217
+ * @param platform - Operating system whose clipboard commands are tried, in order; any other copies nothing.
218
+ * @param run - Runs one command with the raw text on standard input, reporting success; a throw counts as failure.
219
+ * @returns Port that reports whether any command copied the text within its time limit.
220
+ */
221
+ export function createNativeClipboardPort(platform, run) {
222
+ const commands = platform === "darwin"
223
+ ? [["pbcopy", []]]
224
+ : platform === "linux"
225
+ ? [
226
+ ["wl-copy", []],
227
+ ["xclip", ["-selection", "clipboard"]],
228
+ ]
229
+ : platform === "win32"
230
+ ? [["clip.exe", []]]
231
+ : [];
232
+ return {
233
+ copy(rawCopy) {
234
+ for (const [command, arguments_] of commands) {
235
+ try {
236
+ if (run(command, arguments_, rawCopy, NATIVE_CLIPBOARD_TIMEOUT_MS))
237
+ return true;
238
+ }
239
+ catch {
240
+ continue;
241
+ }
242
+ }
243
+ return false;
244
+ },
245
+ };
246
+ }
247
+ const NATIVE_CLIPBOARD_TIMEOUT_MS = 500;
248
+ const retainedOperatorBody = {
249
+ read: (message) => ({
250
+ display: messageBodyDisplayText(message.body.content),
251
+ rawCopy: message.body.rawCopy,
252
+ }),
253
+ };
@@ -0,0 +1,32 @@
1
+ import { EmbeddedTerminalRenderable } from "@opentui/core";
2
+ declare module "@opentui/react" {
3
+ interface OpenTUIComponents {
4
+ embeddedTerminal: typeof EmbeddedTerminalRenderable;
5
+ }
6
+ }
7
+ /**
8
+ * Render a child's terminal output as the child itself drew it.
9
+ *
10
+ * The preamble is written first, so the command sits in the terminal as though it had been typed there
11
+ * and the child's output follows it. The chunks are then replayed from the first byte on every mount,
12
+ * because the emulator's screen is derived from the whole sequence: a chunk applied to a fresh screen
13
+ * lands in the wrong place.
14
+ *
15
+ * Focus is what routes the operator's keys here, and it also tells the emulator to encode them for the
16
+ * keyboard mode the child has asked for, which no caller can know.
17
+ * @param properties - Command to show before anything runs, byte stream and how many chunks it has
18
+ * ever held, viewport size, whether the child is still taking input, whether the run is spent, and the
19
+ * sinks for what the emulator sends back.
20
+ * @returns - Embedded terminal element sized to the given viewport.
21
+ */
22
+ export declare function RunTerminal({ preamble, chunks, received, columns, rows, live, dimmed, onInput, onResize, }: {
23
+ readonly preamble: string;
24
+ readonly chunks: readonly Uint8Array[];
25
+ readonly received: number;
26
+ readonly columns: number;
27
+ readonly rows: number;
28
+ readonly live: boolean;
29
+ readonly dimmed: boolean;
30
+ readonly onInput: (bytes: Uint8Array) => void;
31
+ readonly onResize: (columns: number, rows: number) => void;
32
+ }): import("react").ReactNode;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { EmbeddedTerminalRenderable } from "@opentui/core";
3
+ import { extend } from "@opentui/react";
4
+ import { useEffect, useRef } from "react";
5
+ import { TUI_RUN_OUTPUT_BYTE_LIMIT } from "./model.js";
6
+ /** Opacity a finished run's terminal takes, so it reads as spent rather than still live. */
7
+ const FINISHED_RUN_OPACITY = 0.55;
8
+ extend({ embeddedTerminal: EmbeddedTerminalRenderable });
9
+ /**
10
+ * Render a child's terminal output as the child itself drew it.
11
+ *
12
+ * The preamble is written first, so the command sits in the terminal as though it had been typed there
13
+ * and the child's output follows it. The chunks are then replayed from the first byte on every mount,
14
+ * because the emulator's screen is derived from the whole sequence: a chunk applied to a fresh screen
15
+ * lands in the wrong place.
16
+ *
17
+ * Focus is what routes the operator's keys here, and it also tells the emulator to encode them for the
18
+ * keyboard mode the child has asked for, which no caller can know.
19
+ * @param properties - Command to show before anything runs, byte stream and how many chunks it has
20
+ * ever held, viewport size, whether the child is still taking input, whether the run is spent, and the
21
+ * sinks for what the emulator sends back.
22
+ * @returns - Embedded terminal element sized to the given viewport.
23
+ */
24
+ export function RunTerminal({ preamble, chunks, received, columns, rows, live, dimmed, onInput, onResize, }) {
25
+ const terminal = useRef(null);
26
+ const written = useRef(0);
27
+ const primed = useRef(false);
28
+ useEffect(() => {
29
+ const current = terminal.current;
30
+ if (!current)
31
+ return;
32
+ if (!primed.current) {
33
+ current.write(preamble);
34
+ primed.current = true;
35
+ }
36
+ // Counted from the newest end, because the oldest chunks are dropped as the buffer fills and an
37
+ // index from the front would address a different chunk each time that happens.
38
+ const pending = Math.min(received - written.current, chunks.length);
39
+ if (pending > 0)
40
+ for (const chunk of chunks.slice(chunks.length - pending))
41
+ current.write(chunk);
42
+ written.current = received;
43
+ }, [chunks, received, preamble]);
44
+ useEffect(() => {
45
+ const current = terminal.current;
46
+ if (!current)
47
+ return;
48
+ if (live)
49
+ current.focus();
50
+ else
51
+ current.blur();
52
+ }, [live]);
53
+ return (_jsx("embeddedTerminal", { ref: terminal, cols: columns, rows: rows, width: columns, height: rows, maxScrollback: TUI_RUN_OUTPUT_BYTE_LIMIT, opacity: dimmed ? FINISHED_RUN_OPACITY : 1, onData: onInput, onTerminalResize: onResize }));
54
+ }
@@ -0,0 +1,113 @@
1
+ /** Signals whose default terminal action is replaced by orderly TUI shutdown. */
2
+ export declare const DEFAULT_TUI_EXIT_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGQUIT", "SIGHUP"];
3
+ /** A signal owned by the TUI runtime boundary. */
4
+ export type TuiExitSignal = (typeof DEFAULT_TUI_EXIT_SIGNALS)[number];
5
+ /** The first event that initiated terminal shutdown. */
6
+ export type TuiExitReason = {
7
+ /** Identifies an operator-requested orderly exit. */
8
+ kind: "normal";
9
+ /** Process status used after renderer finalization. */
10
+ exitCode: number;
11
+ } | {
12
+ /** Identifies shutdown requested by an operating-system signal. */
13
+ kind: "signal";
14
+ /** Signal delivered to the runtime boundary. */
15
+ signal: TuiExitSignal;
16
+ /** Process status used after renderer finalization. */
17
+ exitCode: number;
18
+ } | {
19
+ /** Identifies a renderer frame failure. */
20
+ kind: "renderer-error";
21
+ /** Error reported by the renderer. */
22
+ error: unknown;
23
+ /** Process status used after renderer finalization. */
24
+ exitCode: number;
25
+ } | {
26
+ /** Identifies an exception that escaped application code. */
27
+ kind: "uncaught-exception";
28
+ /** Uncaught value reported by the runtime. */
29
+ error: unknown;
30
+ /** Process status used after renderer finalization. */
31
+ exitCode: number;
32
+ } | {
33
+ /** Identifies a rejected promise without a consumer. */
34
+ kind: "unhandled-rejection";
35
+ /** Rejection value reported by the runtime. */
36
+ error: unknown;
37
+ /** Process status used after renderer finalization. */
38
+ exitCode: number;
39
+ };
40
+ /** Renderer options controlled by the TUI lifecycle boundary. */
41
+ export type TuiRendererLifecycleOptions = {
42
+ /** Prevents the renderer from bypassing application-owned Ctrl+C handling. */
43
+ exitOnCtrlC: false;
44
+ /** Prevents the renderer from installing its own signal policy. */
45
+ exitSignals: [];
46
+ /** Prevents fatal errors from replacing the production screen with a debug overlay. */
47
+ openConsoleOnError: false;
48
+ /** Runs only after the renderer has completed native finalization. */
49
+ onDestroy: () => void;
50
+ };
51
+ /** Minimal renderer surface needed by the lifecycle owner. */
52
+ export interface TuiRendererPort {
53
+ /** Requests terminal cleanup; completion is reported through the configured callback. */
54
+ destroy(): void;
55
+ /** Subscribes to one renderer event. */
56
+ on(event: "render:error", listener: (event: {
57
+ error: unknown;
58
+ }) => void): this;
59
+ /** Removes one renderer event subscription. */
60
+ off(event: "render:error", listener: (event: {
61
+ error: unknown;
62
+ }) => void): this;
63
+ }
64
+ /** Process lifecycle operations used by the TUI runtime. */
65
+ export interface TuiProcessPort {
66
+ /** Registers a signal listener and returns its cleanup operation. */
67
+ onSignal(signal: TuiExitSignal, listener: () => void): () => void;
68
+ /** Registers an uncaught-exception listener and returns its cleanup operation. */
69
+ onUncaughtException(listener: (error: unknown) => void): () => void;
70
+ /** Registers an unhandled-rejection listener and returns its cleanup operation. */
71
+ onUnhandledRejection(listener: (error: unknown) => void): () => void;
72
+ /** Resolves the conventional process status for one configured signal. */
73
+ signalExitCode(signal: TuiExitSignal): number;
74
+ /** Writes one operator-visible line once the terminal has been restored. */
75
+ report(line: string): void;
76
+ /** Terminates the process after renderer finalization. */
77
+ exit(exitCode: number): void;
78
+ }
79
+ /** Mounted presentation resources released before renderer destruction begins. */
80
+ export interface MountedTuiRoot {
81
+ /** Releases presentation resources without owning native terminal cleanup. */
82
+ dispose(): void;
83
+ }
84
+ /** Dependencies supplied to the TUI runtime consumer. */
85
+ export type TuiRuntimeDependencies<Renderer extends TuiRendererPort> = {
86
+ /** Creates the renderer with lifecycle policy supplied by the consumer. */
87
+ createRenderer(options: TuiRendererLifecycleOptions): Promise<Renderer>;
88
+ /** Mounts the presentation tree and optionally returns its cleanup operation. */
89
+ mount(renderer: Renderer, shutdown: (reason?: TuiExitReason) => void): MountedTuiRoot | undefined;
90
+ /** Installs renderer event isolation before application subscriptions or presentation mount. */
91
+ prepareRenderer(renderer: Renderer, reportError: (error: unknown) => void): void;
92
+ /** Supplies process event and termination operations. */
93
+ process: TuiProcessPort;
94
+ /** Signals owned by this runtime instance. */
95
+ exitSignals: readonly TuiExitSignal[];
96
+ };
97
+ /** A running TUI together with its single shutdown entry point. */
98
+ export interface TuiRuntime<Renderer extends TuiRendererPort> {
99
+ /** Renderer owned by this runtime instance. */
100
+ readonly renderer: Renderer;
101
+ /** First shutdown reason, or undefined while the runtime is active. */
102
+ readonly exitReason: TuiExitReason | undefined;
103
+ /** Cleanup failures retained without preventing later cleanup operations. */
104
+ readonly cleanupErrors: readonly unknown[];
105
+ /** Requests idempotent renderer shutdown while retaining the first reason. */
106
+ shutdown(reason?: TuiExitReason): void;
107
+ }
108
+ /**
109
+ * Start a TUI from explicit renderer, process, and presentation dependencies.
110
+ * @param dependencies - Collaborators and signal policy owned by the composition root.
111
+ * @returns Running TUI whose process exit remains gated on renderer finalization.
112
+ */
113
+ export declare function startTuiRuntime<Renderer extends TuiRendererPort>(dependencies: TuiRuntimeDependencies<Renderer>): Promise<TuiRuntime<Renderer>>;
@@ -0,0 +1,189 @@
1
+ /** Signals whose default terminal action is replaced by orderly TUI shutdown. */
2
+ export const DEFAULT_TUI_EXIT_SIGNALS = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGHUP"];
3
+ /**
4
+ * Start a TUI from explicit renderer, process, and presentation dependencies.
5
+ * @param dependencies - Collaborators and signal policy owned by the composition root.
6
+ * @returns Running TUI whose process exit remains gated on renderer finalization.
7
+ */
8
+ export async function startTuiRuntime(dependencies) {
9
+ const rendererReference = {};
10
+ let mountedRoot;
11
+ let exitReason;
12
+ let creating = true;
13
+ let mounting = false;
14
+ let shutdownPending = false;
15
+ let presentationDisposed = false;
16
+ let destroyRequested = false;
17
+ let finalizationObserved = false;
18
+ let finalizationCompleted = false;
19
+ let rendererSubscriptionAttempted = false;
20
+ const removeProcessListeners = [];
21
+ const cleanupErrors = [];
22
+ const finalizationWasObserved = () => finalizationObserved;
23
+ const shutdownWasRequested = () => shutdownPending;
24
+ const attemptCleanup = (operation) => {
25
+ try {
26
+ operation();
27
+ }
28
+ catch (error) {
29
+ cleanupErrors.push(error);
30
+ }
31
+ };
32
+ const removeListeners = () => {
33
+ if (rendererSubscriptionAttempted) {
34
+ rendererSubscriptionAttempted = false;
35
+ attemptCleanup(() => {
36
+ rendererReference.current?.off("render:error", onRendererError);
37
+ });
38
+ }
39
+ for (const remove of removeProcessListeners.splice(0))
40
+ attemptCleanup(remove);
41
+ };
42
+ const disposePresentation = () => {
43
+ if (presentationDisposed || !mountedRoot)
44
+ return;
45
+ presentationDisposed = true;
46
+ const root = mountedRoot;
47
+ attemptCleanup(() => {
48
+ root.dispose();
49
+ });
50
+ };
51
+ const completeFinalization = () => {
52
+ finalizationObserved = true;
53
+ if (creating || mounting || finalizationCompleted)
54
+ return;
55
+ finalizationCompleted = true;
56
+ const retainedReason = exitReason ?? normalExit();
57
+ exitReason = retainedReason;
58
+ try {
59
+ disposePresentation();
60
+ removeListeners();
61
+ }
62
+ finally {
63
+ attemptCleanup(() => {
64
+ for (const line of describeAbnormalExit(retainedReason, cleanupErrors))
65
+ dependencies.process.report(line);
66
+ });
67
+ dependencies.process.exit(retainedReason.exitCode);
68
+ }
69
+ };
70
+ const requestDestroy = () => {
71
+ if (destroyRequested || finalizationObserved)
72
+ return;
73
+ destroyRequested = true;
74
+ disposePresentation();
75
+ const renderer = rendererReference.current;
76
+ if (!renderer)
77
+ return;
78
+ attemptCleanup(() => {
79
+ renderer.destroy();
80
+ });
81
+ };
82
+ const rollbackStartup = () => {
83
+ removeListeners();
84
+ requestDestroy();
85
+ };
86
+ const shutdown = (reason = normalExit()) => {
87
+ exitReason ??= reason;
88
+ shutdownPending = true;
89
+ if (mounting)
90
+ return;
91
+ requestDestroy();
92
+ };
93
+ const onRendererError = (event) => {
94
+ shutdown({ kind: "renderer-error", error: event.error, exitCode: 1 });
95
+ };
96
+ let renderer;
97
+ try {
98
+ renderer = await dependencies.createRenderer({
99
+ exitOnCtrlC: false,
100
+ exitSignals: [],
101
+ openConsoleOnError: false,
102
+ onDestroy: completeFinalization,
103
+ });
104
+ rendererReference.current = renderer;
105
+ creating = false;
106
+ }
107
+ catch (error) {
108
+ creating = false;
109
+ exitReason = { kind: "uncaught-exception", error, exitCode: 1 };
110
+ shutdownPending = true;
111
+ if (finalizationWasObserved())
112
+ completeFinalization();
113
+ throw error;
114
+ }
115
+ if (!finalizationWasObserved()) {
116
+ try {
117
+ dependencies.prepareRenderer(renderer, (error) => cleanupErrors.push(error));
118
+ rendererSubscriptionAttempted = true;
119
+ renderer.on("render:error", onRendererError);
120
+ for (const signal of dependencies.exitSignals) {
121
+ removeProcessListeners.push(dependencies.process.onSignal(signal, () => {
122
+ shutdown({ kind: "signal", signal, exitCode: dependencies.process.signalExitCode(signal) });
123
+ }));
124
+ }
125
+ removeProcessListeners.push(dependencies.process.onUncaughtException((error) => {
126
+ shutdown({ kind: "uncaught-exception", error, exitCode: 1 });
127
+ }));
128
+ removeProcessListeners.push(dependencies.process.onUnhandledRejection((error) => {
129
+ shutdown({ kind: "unhandled-rejection", error, exitCode: 1 });
130
+ }));
131
+ mounting = true;
132
+ mountedRoot = dependencies.mount(renderer, shutdown) ?? undefined;
133
+ mounting = false;
134
+ }
135
+ catch (error) {
136
+ mounting = false;
137
+ exitReason ??= { kind: "uncaught-exception", error, exitCode: 1 };
138
+ shutdownPending = true;
139
+ rollbackStartup();
140
+ if (finalizationWasObserved())
141
+ completeFinalization();
142
+ throw error;
143
+ }
144
+ }
145
+ if (finalizationWasObserved())
146
+ completeFinalization();
147
+ else if (shutdownWasRequested())
148
+ requestDestroy();
149
+ return {
150
+ renderer,
151
+ get exitReason() {
152
+ return exitReason;
153
+ },
154
+ get cleanupErrors() {
155
+ return cleanupErrors;
156
+ },
157
+ shutdown,
158
+ };
159
+ }
160
+ function normalExit() {
161
+ return { kind: "normal", exitCode: 0 };
162
+ }
163
+ /**
164
+ * Describe an exit that is neither normal nor a signal, so the cause survives the process.
165
+ * @param reason - Retained first shutdown reason.
166
+ * @param cleanupErrors - Failures collected while releasing runtime resources; reported only alongside a described exit.
167
+ * @returns Lines to write after terminal restoration; empty for a normal exit or a signal.
168
+ */
169
+ function describeAbnormalExit(reason, cleanupErrors) {
170
+ if (reason.kind === "normal" || reason.kind === "signal")
171
+ return [];
172
+ return [
173
+ `pager tui exited: ${reason.kind}`,
174
+ describeThrown(reason.error),
175
+ ...cleanupErrors.map((error) => `cleanup failed: ${describeThrown(error)}`),
176
+ ];
177
+ }
178
+ function describeThrown(value) {
179
+ if (value instanceof Error)
180
+ return value.stack ?? `${value.name}: ${value.message}`;
181
+ if (typeof value !== "object" || value === null)
182
+ return String(value);
183
+ try {
184
+ return JSON.stringify(value);
185
+ }
186
+ catch {
187
+ return "[unserializable value]";
188
+ }
189
+ }