@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,279 @@
1
+ import { connect } from "node:net";
2
+ import { StringDecoder } from "node:string_decoder";
3
+ import { OPERATOR_PROTOCOL_VERSION, OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES, } from "../operator/protocol.js";
4
+ import { validateOperatorCommandResultForCommand, validateOperatorCursor } from "../operator/validation.js";
5
+ import { isPlainObject } from "../shared/guards.js";
6
+ import { TuiOperatorTransportController } from "./transport.js";
7
+ /** Bounds commands awaiting a result before the transport refuses more. */
8
+ const PENDING_COMMAND_LIMIT = 32;
9
+ /** Bounds observations buffered before a consumer attaches. */
10
+ const BUFFERED_OBSERVATION_LIMIT = 32;
11
+ /** Owns one cancellable loopback NDJSON session and exposes only model-guarded stream callbacks. */
12
+ export class TuiOperatorTcpClient {
13
+ url;
14
+ #socket;
15
+ #buffer = "";
16
+ #controller;
17
+ #initial;
18
+ #receive;
19
+ #unavailable;
20
+ #observations = [];
21
+ #generation = 0;
22
+ #failed = false;
23
+ #failure;
24
+ #pendingCommands = new Map();
25
+ /**
26
+ * Construct a client for the published numeric loopback TCP endpoint. No connection opens until a fetch or open.
27
+ * @param url - Operator endpoint the broker published, naming a numeric loopback host and port.
28
+ */
29
+ constructor(url) {
30
+ this.url = url;
31
+ }
32
+ /**
33
+ * Open cold hydration and resolve exactly one authoritative snapshot.
34
+ * @returns Snapshot request; cancelling it resets the session, and a second fetch while one is pending rejects.
35
+ */
36
+ fetch() {
37
+ if (this.#initial)
38
+ return { promise: Promise.reject(new Error("TUI snapshot is already pending")), cancel: () => undefined };
39
+ this.#reset(false);
40
+ let cancelled = false;
41
+ let reject;
42
+ const promise = new Promise((resolve, rejectRequest) => {
43
+ reject = rejectRequest;
44
+ this.#initial = { resolve, reject: rejectRequest };
45
+ this.#connect(null);
46
+ });
47
+ return {
48
+ promise,
49
+ cancel: () => {
50
+ if (cancelled)
51
+ return;
52
+ cancelled = true;
53
+ this.#reset(false);
54
+ reject(new Error("TUI snapshot cancelled"));
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Attach the model-owned ordered callback, opening a resumed session when no socket is live.
60
+ *
61
+ * After hydration the session opened by `fetch` is already subscribed, and the cursor it was
62
+ * subscribed at is the one the caller now holds. Only a caller resuming without hydrating first
63
+ * reaches the connect below, and the cursor it supplies is what makes the server replay rather
64
+ * than transfer a whole snapshot.
65
+ * @param cursor - Position the caller already holds, or null when it holds no snapshot.
66
+ * @param receive - Receives each validated observation in stream order, including any buffered before this call.
67
+ * @param unavailable - Called once when the session fails; a failure recorded before this call is reported on the next microtask.
68
+ * @returns Cancellation callback for this stream generation.
69
+ */
70
+ open(cursor, receive, unavailable) {
71
+ this.#receive = receive;
72
+ this.#unavailable = unavailable;
73
+ if (this.#failure) {
74
+ const generation = this.#generation;
75
+ this.#failure = undefined;
76
+ queueMicrotask(() => unavailable());
77
+ return () => {
78
+ if (generation === this.#generation)
79
+ this.#reset(false);
80
+ };
81
+ }
82
+ if (!this.#socket)
83
+ this.#connect(cursor);
84
+ const generation = this.#generation;
85
+ for (const observation of this.#observations.splice(0))
86
+ receive(observation);
87
+ return () => {
88
+ if (generation === this.#generation)
89
+ this.#reset(false);
90
+ };
91
+ }
92
+ /** Reset the current session. */
93
+ reconnect() {
94
+ this.#reset(false);
95
+ }
96
+ /**
97
+ * Submit one correlated non-authoritative command.
98
+ * @param command - Validated command envelope, correlated with its result by `commandId`.
99
+ * @returns Command request; it rejects at once without a live session, while hydration is pending, when
100
+ * too many commands are pending, or for a duplicate `commandId`, and rejects later if the session resets.
101
+ */
102
+ execute(command) {
103
+ if (!this.#socket || this.#initial || this.#pendingCommands.size >= PENDING_COMMAND_LIMIT)
104
+ return {
105
+ promise: Promise.reject(new Error("TUI operator command transport unavailable")),
106
+ cancel: () => undefined,
107
+ };
108
+ if (this.#pendingCommands.has(command.commandId))
109
+ return { promise: Promise.reject(new Error("TUI command is already pending")), cancel: () => undefined };
110
+ let reject;
111
+ const promise = new Promise((resolve, rejectRequest) => {
112
+ reject = rejectRequest;
113
+ this.#pendingCommands.set(command.commandId, { command, resolve, reject: rejectRequest });
114
+ this.#write({ type: "command", protocolVersion: OPERATOR_PROTOCOL_VERSION, command });
115
+ });
116
+ return {
117
+ promise,
118
+ cancel: () => {
119
+ if (this.#pendingCommands.delete(command.commandId))
120
+ reject(new Error("TUI command cancelled"));
121
+ },
122
+ };
123
+ }
124
+ /** Release socket, input buffers, and all pending work exactly once. */
125
+ dispose() {
126
+ this.#reset(false, new Error("TUI operator transport disposed"));
127
+ }
128
+ #connect(cursor) {
129
+ this.#failed = false;
130
+ let endpoint;
131
+ try {
132
+ endpoint = new URL(this.url);
133
+ const port = Number(endpoint.port);
134
+ if (endpoint.protocol !== "tcp:" ||
135
+ !/^127(?:\.\d{1,3}){3}$/.test(endpoint.hostname) ||
136
+ !Number.isInteger(port) ||
137
+ port < 1 ||
138
+ port > 65_535)
139
+ throw new Error("TUI operator endpoint must be a numeric loopback TCP URL");
140
+ }
141
+ catch (cause) {
142
+ this.#fail(cause instanceof Error ? cause : new Error("TUI operator endpoint is invalid"));
143
+ return;
144
+ }
145
+ const generation = ++this.#generation;
146
+ const socket = connect({ host: endpoint.hostname, port: Number(endpoint.port) });
147
+ this.#socket = socket;
148
+ this.#controller = new TuiOperatorTransportController({
149
+ replace: (snapshot, replacement) => {
150
+ if (this.#initial) {
151
+ const initial = this.#initial;
152
+ this.#initial = undefined;
153
+ initial.resolve(snapshot);
154
+ }
155
+ else
156
+ this.#deliver({ type: "snapshot", snapshot, replacement });
157
+ },
158
+ event: (event) => {
159
+ this.#deliver({ type: "event", event });
160
+ },
161
+ recover: () => this.#deliver({ type: "resync" }),
162
+ incompatible: () => this.#deliver({ type: "incompatible" }),
163
+ diagnostic: (diagnostic) => this.#deliver({ type: "diagnostic", diagnostic }),
164
+ live: () => this.#deliver({ type: "live" }),
165
+ awaitReplacement: () => this.#deliver({ type: "resync_pending" }),
166
+ replacementProgress: () => this.#deliver({ type: "resync_progress" }),
167
+ });
168
+ socket.setEncoding("utf8");
169
+ socket.on("connect", () => {
170
+ if (generation === this.#generation)
171
+ this.#write(cursor
172
+ ? { type: "subscribe", protocolVersion: OPERATOR_PROTOCOL_VERSION, cursor }
173
+ : { type: "subscribe", protocolVersion: OPERATOR_PROTOCOL_VERSION });
174
+ });
175
+ const decoder = new StringDecoder("utf8");
176
+ socket.on("data", (chunk) => {
177
+ if (generation === this.#generation)
178
+ this.#receiveChunk(typeof chunk === "string" ? chunk : decoder.write(chunk));
179
+ });
180
+ socket.on("error", () => this.#fail(new Error("TUI operator transport unavailable"), generation));
181
+ socket.on("close", () => this.#fail(new Error("TUI operator transport unavailable"), generation));
182
+ }
183
+ #receiveChunk(chunk) {
184
+ this.#buffer += chunk;
185
+ for (;;) {
186
+ const newline = this.#buffer.indexOf("\n");
187
+ if (newline < 0) {
188
+ if (Buffer.byteLength(this.#buffer) > OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES)
189
+ this.#fail(new Error("TUI operator frame exceeds limit"));
190
+ return;
191
+ }
192
+ const line = this.#buffer.slice(0, newline);
193
+ this.#buffer = this.#buffer.slice(newline + 1);
194
+ if (Buffer.byteLength(line) > OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES)
195
+ return this.#fail(new Error("TUI operator frame exceeds limit"));
196
+ try {
197
+ const frame = JSON.parse(line);
198
+ if (!isPlainObject(frame))
199
+ return this.#fail(new Error("TUI operator frame is invalid"));
200
+ if (frame.type === "command_result") {
201
+ if (Object.keys(frame).length !== 2 || !("result" in frame))
202
+ return this.#fail(new Error("TUI command result frame is invalid"));
203
+ this.#receiveCommandResult(frame.result);
204
+ }
205
+ else {
206
+ this.#controller?.receive(frame);
207
+ // Validated before it is echoed back in an ack.
208
+ if (frame.type === "heartbeat" && frame.cursor !== undefined)
209
+ this.#write({
210
+ type: "ack",
211
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
212
+ cursor: validateOperatorCursor(frame.cursor),
213
+ });
214
+ }
215
+ }
216
+ catch {
217
+ return this.#fail(new Error("TUI operator frame is invalid"));
218
+ }
219
+ }
220
+ }
221
+ #receiveCommandResult(value) {
222
+ if (typeof value !== "object" || value === null || !("commandId" in value) || typeof value.commandId !== "string")
223
+ return this.#fail(new Error("TUI command result is invalid"));
224
+ const pending = this.#pendingCommands.get(value.commandId);
225
+ if (!pending)
226
+ return this.#fail(new Error("TUI command result is unexpected"));
227
+ try {
228
+ const result = validateOperatorCommandResultForCommand(pending.command, value);
229
+ this.#pendingCommands.delete(value.commandId);
230
+ pending.resolve(result.result);
231
+ }
232
+ catch {
233
+ this.#fail(new Error("TUI command result is invalid"));
234
+ }
235
+ }
236
+ #write(value) {
237
+ this.#socket?.write(`${JSON.stringify(value)}\n`);
238
+ }
239
+ #fail(error, generation = this.#generation) {
240
+ if (generation !== this.#generation || this.#failed)
241
+ return;
242
+ this.#failed = true;
243
+ this.#failure = error;
244
+ const initial = this.#initial;
245
+ const unavailable = this.#unavailable;
246
+ this.#reset(true);
247
+ initial?.reject(error);
248
+ unavailable?.();
249
+ }
250
+ #reset(keepFailure, commandError = new Error("TUI operator transport unavailable")) {
251
+ ++this.#generation;
252
+ this.#socket?.destroy();
253
+ this.#socket = undefined;
254
+ this.#buffer = "";
255
+ this.#controller = undefined;
256
+ this.#initial = undefined;
257
+ this.#receive = undefined;
258
+ this.#unavailable = undefined;
259
+ this.#observations = [];
260
+ this.#rejectPendingCommands(commandError);
261
+ if (!keepFailure) {
262
+ this.#failed = false;
263
+ this.#failure = undefined;
264
+ }
265
+ }
266
+ #rejectPendingCommands(error) {
267
+ for (const pending of this.#pendingCommands.values())
268
+ pending.reject(error);
269
+ this.#pendingCommands.clear();
270
+ }
271
+ #deliver(observation) {
272
+ if (this.#receive)
273
+ this.#receive(observation);
274
+ else if (this.#observations.length < BUFFERED_OBSERVATION_LIMIT)
275
+ this.#observations.push(observation);
276
+ else
277
+ this.#fail(new Error("TUI operator observations exceed limit"));
278
+ }
279
+ }
@@ -0,0 +1,41 @@
1
+ import type { EventEmitter } from "node:events";
2
+ import { type CliRenderer, type CliRendererConfig } from "@opentui/core";
3
+ import type { ReactNode } from "react";
4
+ import { type TuiExitSignal, type TuiProcessPort, type TuiRuntime } from "./runtime.js";
5
+ /** Renderer settings that cannot override application-owned shutdown policy. */
6
+ export type OpenTuiRendererOptions = Omit<CliRendererConfig, "exitOnCtrlC" | "exitSignals" | "onDestroy" | "openConsoleOnError">;
7
+ /** Configuration accepted by the production OpenTUI composition root. */
8
+ export type OpenTuiRuntimeOptions = {
9
+ /** Non-lifecycle renderer configuration. */
10
+ renderer?: OpenTuiRendererOptions;
11
+ /** Signals routed through orderly renderer shutdown. */
12
+ exitSignals?: readonly TuiExitSignal[];
13
+ /** Process lifecycle adapter; defaults to the current Bun process. */
14
+ process?: TuiProcessPort;
15
+ };
16
+ /**
17
+ * Adapt a Bun process to the lifecycle port used by the TUI consumer.
18
+ * @param processObject - Bun-hosted process whose listeners and exit belong to this application.
19
+ * @returns Process port with removable listeners and conventional signal statuses.
20
+ */
21
+ export declare function createBunTuiProcessPort(processObject?: NodeJS.Process): TuiProcessPort;
22
+ /**
23
+ * Isolate failures from the listener set present when the renderer dispatches its destroy event.
24
+ * @param renderer - Event source whose destroy dispatch is guarded without changing other events.
25
+ * @param reportError - Sink receiving each dispatch failure without interrupting later subscribers.
26
+ */
27
+ export declare function installOpenTuiDestroyEmissionGuard(renderer: EventEmitter, reportError: (error: unknown) => void): void;
28
+ /**
29
+ * Compose the Bun-owned renderer, React reconciler, process lifecycle, and application node.
30
+ * @param node - React presentation tree mounted into the terminal renderer.
31
+ * @param options - Renderer tuning and configured application signals.
32
+ * @returns Running OpenTUI runtime with an orderly shutdown entry point.
33
+ */
34
+ export declare function startOpenTui(node: ReactNode, options?: OpenTuiRuntimeOptions): Promise<TuiRuntime<CliRenderer>>;
35
+ /**
36
+ * Compose a terminal tree after the runtime has supplied its single shutdown entry point.
37
+ * @param createNode - Factory receiving the idempotent orderly-shutdown action.
38
+ * @param options - Renderer tuning and configured application signals.
39
+ * @returns Running OpenTUI runtime with an orderly shutdown entry point.
40
+ */
41
+ export declare function startOpenTuiFactory(createNode: (shutdown: TuiRuntime<CliRenderer>["shutdown"]) => ReactNode, options?: OpenTuiRuntimeOptions): Promise<TuiRuntime<CliRenderer>>;
@@ -0,0 +1,146 @@
1
+ import { constants } from "node:os";
2
+ import { writeSync } from "node:fs";
3
+ import { createCliRenderer } from "@opentui/core";
4
+ import { createRoot, flushSync } from "@opentui/react";
5
+ import { DEFAULT_TUI_EXIT_SIGNALS, startTuiRuntime, } from "./runtime.js";
6
+ /** Offsets a signal number into the exit code a shell reports for a signalled process. */
7
+ const SIGNAL_EXIT_CODE_OFFSET = 128;
8
+ /**
9
+ * Adapt a Bun process to the lifecycle port used by the TUI consumer.
10
+ * @param processObject - Bun-hosted process whose listeners and exit belong to this application.
11
+ * @returns Process port with removable listeners and conventional signal statuses.
12
+ */
13
+ export function createBunTuiProcessPort(processObject = process) {
14
+ return {
15
+ onSignal(signal, listener) {
16
+ processObject.on(signal, listener);
17
+ return () => processObject.removeListener(signal, listener);
18
+ },
19
+ onUncaughtException(listener) {
20
+ const processListener = (error) => {
21
+ listener(error);
22
+ };
23
+ processObject.on("uncaughtException", processListener);
24
+ return () => processObject.removeListener("uncaughtException", processListener);
25
+ },
26
+ onUnhandledRejection(listener) {
27
+ const processListener = (error) => {
28
+ listener(error);
29
+ };
30
+ processObject.on("unhandledRejection", processListener);
31
+ return () => processObject.removeListener("unhandledRejection", processListener);
32
+ },
33
+ signalExitCode(signal) {
34
+ return SIGNAL_EXIT_CODE_OFFSET + constants.signals[signal];
35
+ },
36
+ report(line) {
37
+ // Process exit follows immediately, so this write must complete before returning.
38
+ try {
39
+ writeSync(processObject.stderr.fd, `${line}\n`);
40
+ }
41
+ catch {
42
+ processObject.stderr.write(`${line}\n`);
43
+ }
44
+ },
45
+ exit(exitCode) {
46
+ processObject.exit(exitCode);
47
+ },
48
+ };
49
+ }
50
+ /**
51
+ * Isolate failures from the listener set present when the renderer dispatches its destroy event.
52
+ * @param renderer - Event source whose destroy dispatch is guarded without changing other events.
53
+ * @param reportError - Sink receiving each dispatch failure without interrupting later subscribers.
54
+ */
55
+ export function installOpenTuiDestroyEmissionGuard(renderer, reportError) {
56
+ // OpenTUI has no destroy listeners when createCliRenderer resolves. This hook runs then, before
57
+ // React registers its cleanup through once(), so every production once wrapper is observed.
58
+ const onceWrappers = new WeakSet();
59
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- Borrowed EventEmitter calls require the caller's receiver.
60
+ const emit = renderer.emit;
61
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- Destroy snapshots belong to the caller's receiver.
62
+ const rawListenersFor = renderer.rawListeners;
63
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- Registration must retain the caller's receiver.
64
+ const once = renderer.once;
65
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- Registration must retain the caller's receiver.
66
+ const prependOnceListener = renderer.prependOnceListener;
67
+ const recordOnceWrapper = (receiver, event, index) => {
68
+ if (event !== "destroy")
69
+ return;
70
+ const rawListeners = Reflect.apply(rawListenersFor, receiver, [event]);
71
+ const rawListener = rawListeners.at(index);
72
+ if (rawListener)
73
+ onceWrappers.add(rawListener);
74
+ };
75
+ renderer.once = function (...parameters) {
76
+ const result = Reflect.apply(once, this, parameters);
77
+ recordOnceWrapper(this, parameters[0], -1);
78
+ return result;
79
+ };
80
+ renderer.prependOnceListener = function (...parameters) {
81
+ const result = Reflect.apply(prependOnceListener, this, parameters);
82
+ recordOnceWrapper(this, parameters[0], 0);
83
+ return result;
84
+ };
85
+ renderer.emit = function (event, ...arguments_) {
86
+ if (event !== "destroy")
87
+ return Reflect.apply(emit, this, [event, ...arguments_]);
88
+ const rawListeners = Reflect.apply(rawListenersFor, this, [event]);
89
+ if (rawListeners.length === 0)
90
+ return false;
91
+ for (const rawListener of rawListeners) {
92
+ try {
93
+ Reflect.apply(rawListener, this, arguments_);
94
+ }
95
+ catch (error) {
96
+ reportError(error);
97
+ if (onceWrappers.has(rawListener)) {
98
+ try {
99
+ Reflect.apply(rawListener, this, arguments_);
100
+ }
101
+ catch (retryError) {
102
+ reportError(retryError);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ return true;
108
+ };
109
+ }
110
+ /**
111
+ * Compose the Bun-owned renderer, React reconciler, process lifecycle, and application node.
112
+ * @param node - React presentation tree mounted into the terminal renderer.
113
+ * @param options - Renderer tuning and configured application signals.
114
+ * @returns Running OpenTUI runtime with an orderly shutdown entry point.
115
+ */
116
+ export function startOpenTui(node, options = {}) {
117
+ return startOpenTuiFactory(() => node, options);
118
+ }
119
+ /**
120
+ * Compose a terminal tree after the runtime has supplied its single shutdown entry point.
121
+ * @param createNode - Factory receiving the idempotent orderly-shutdown action.
122
+ * @param options - Renderer tuning and configured application signals.
123
+ * @returns Running OpenTUI runtime with an orderly shutdown entry point.
124
+ */
125
+ export function startOpenTuiFactory(createNode, options = {}) {
126
+ if (typeof Bun === "undefined")
127
+ throw new Error("The OpenTUI runtime requires Bun");
128
+ const rendererOptions = options.renderer ?? {};
129
+ return startTuiRuntime({
130
+ createRenderer: (lifecycleOptions) => createCliRenderer({ ...rendererOptions, ...lifecycleOptions }),
131
+ mount(renderer, shutdown) {
132
+ const root = createRoot(renderer);
133
+ root.render(createNode(shutdown));
134
+ return {
135
+ dispose() {
136
+ flushSync(() => {
137
+ root.unmount();
138
+ });
139
+ },
140
+ };
141
+ },
142
+ prepareRenderer: installOpenTuiDestroyEmissionGuard,
143
+ process: options.process ?? createBunTuiProcessPort(),
144
+ exitSignals: options.exitSignals ?? DEFAULT_TUI_EXIT_SIGNALS,
145
+ });
146
+ }
@@ -0,0 +1,94 @@
1
+ import { useRenderer } from "@opentui/react";
2
+ import { type OperatorAcceptedDelivery, type OperatorCommandResult, type OperatorTraffic } from "../operator/protocol.js";
3
+ import type { TuiOperatorModel } from "./model.js";
4
+ import type { TuiTheme } from "./theme.js";
5
+ /** Reads public full-body content without giving a renderer access to broker storage. */
6
+ export interface TuiMessageBodyPort {
7
+ read(message: OperatorAcceptedDelivery): TuiMessageBodyView;
8
+ }
9
+ /** Separates rendered public content from the exact raw-copy representation. */
10
+ export type TuiMessageBodyView = {
11
+ readonly display: string;
12
+ readonly rawCopy: string;
13
+ };
14
+ /** Reports whether a raw-copy request reached the terminal or a native fallback. */
15
+ export type TuiCopyResult = "osc52" | "copied" | "unavailable";
16
+ /** Handles a copy request the current terminal cannot safely perform through OSC52. */
17
+ export interface TuiCopyFallbackPort {
18
+ copyUnavailable(rawCopy: string): Extract<TuiCopyResult, "copied" | "unavailable">;
19
+ }
20
+ /** Creates unique command correlations and records command-result observation. */
21
+ export interface TuiMessageCommandPort {
22
+ commandId(): string;
23
+ /** Epoch milliseconds a command is stamped with when it is issued. */
24
+ issuedAt(): number;
25
+ observe(result: OperatorCommandResult): void;
26
+ }
27
+ /** Shared explicit dependencies for message presentation. */
28
+ export type MessageViewsProps = {
29
+ readonly model: TuiOperatorModel;
30
+ readonly theme: Readonly<TuiTheme>;
31
+ readonly body: TuiMessageBodyPort;
32
+ readonly copyFallback: TuiCopyFallbackPort;
33
+ readonly commands: TuiMessageCommandPort;
34
+ readonly colorEnabled?: boolean;
35
+ };
36
+ /**
37
+ * Render a distinct, renderer-neutral description for each traffic protocol member.
38
+ * @param entry - Traffic row of either member; identities are printed raw rather than as handles.
39
+ * @returns One line giving a delivery's state glyph, endpoints and message id, or an audit's glyph, target and the word ping.
40
+ */
41
+ export declare function formatTuiTraffic(entry: OperatorTraffic): string;
42
+ /**
43
+ * Move the agent-log selection one row without wrapping past either end.
44
+ * @param model - Model owning the local view state.
45
+ * @param direction - Row step the operator asked for.
46
+ * @returns Whether the log consumed the step.
47
+ */
48
+ export declare function stepTuiTrafficSelection(model: TuiOperatorModel, direction: "up" | "down"): boolean;
49
+ /**
50
+ * Select the newest row of the agent log for an operator arriving with nothing chosen.
51
+ *
52
+ * Follow is turned on rather than preserved: the selection lands on the tail, so the operator is at
53
+ * the position follow tracks and has not scrolled away from it.
54
+ * @param model - Operator model whose view selection is advanced.
55
+ * @returns Whether a row was available to select.
56
+ */
57
+ export declare function selectNewestTuiTraffic(model: TuiOperatorModel): boolean;
58
+ /**
59
+ * Render traffic rows and keep tail-follow state behind the model boundary.
60
+ * @param properties - Model, palette, whether agents are coloured, and the pane's columns.
61
+ * @returns A column heading over scrolling rows, or over a placeholder when the selection scopes no traffic.
62
+ */
63
+ export declare function AgentTrafficLog({ model, theme, colorEnabled, width, }: Pick<MessageViewsProps, "model" | "theme" | "colorEnabled"> & {
64
+ readonly width?: number;
65
+ }): import("react").ReactNode;
66
+ /**
67
+ * Render the selected accepted delivery's metadata, lifecycle, scrolling body, and full-body affordance.
68
+ * @param properties - Model, palette, body reader, whether agents are coloured, the pane's columns, and the rows the body block takes, including its subject and spacer rows.
69
+ * @returns The message log pane content, or a placeholder when no delivery is selected.
70
+ */
71
+ export declare function MessageDetail({ model, theme, body, colorEnabled, width: paneWidth, bodyRows, }: Pick<MessageViewsProps, "model" | "theme" | "body" | "colorEnabled"> & {
72
+ readonly width?: number;
73
+ readonly bodyRows?: number;
74
+ }): import("react").ReactNode;
75
+ /**
76
+ * Render the full-body, message context, and expire confirmation overlays at the terminal root.
77
+ * @param properties - Model whose overlay state chooses what renders, and the body, copy and command ports the overlays use.
78
+ * @returns The open message overlay, or nothing when the open overlay is not a message overlay or its message has left the projection.
79
+ */
80
+ export declare function MessageOverlays(properties: MessageViewsProps): import("react").ReactNode;
81
+ /**
82
+ * Actions the message context menu offers, in display order.
83
+ *
84
+ * An index that names none of them does nothing.
85
+ */
86
+ export declare const MESSAGE_CONTEXT_ACTIONS: readonly ["open", "copy", "expire"];
87
+ /**
88
+ * Guard OSC52 with OpenTUI's exact capability probe before emitting a clipboard sequence.
89
+ * @param renderer - Renderer whose capability probe and OSC52 write are used.
90
+ * @param rawCopy - Exact text to place on the clipboard.
91
+ * @param fallback - Native copy used when the terminal cannot take OSC52 or the write fails.
92
+ * @returns How the copy was performed, or that it could not be.
93
+ */
94
+ export declare function copyRaw(renderer: Pick<ReturnType<typeof useRenderer>, "isOsc52Supported" | "copyToClipboardOSC52">, rawCopy: string, fallback: TuiCopyFallbackPort): TuiCopyResult;