@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,541 @@
1
+ import { MESSAGE_TYPE_LIMIT_CHARACTERS } from "../broker/protocol.js";
2
+ import { isOperatorAgentArchived } from "../operator/agent-projection.js";
3
+ /** Column width the type column occupies when there is room to show it in full. */
4
+ const TRAFFIC_TYPE_COLUMN_WIDTH = MESSAGE_TYPE_LIMIT_CHARACTERS + 1;
5
+ /** Column widths of the agent-log list when a workspace scopes the traffic. */
6
+ export const WORKSPACE_TRAFFIC_COLUMNS = Object.freeze({
7
+ state: 3,
8
+ time: 6,
9
+ from: 15,
10
+ arrow: 3,
11
+ to: 14,
12
+ type: TRAFFIC_TYPE_COLUMN_WIDTH,
13
+ });
14
+ /**
15
+ * Column widths of the agent-log list when a single agent scopes the traffic.
16
+ *
17
+ * One peer stands where the workspace list shows two, so it takes half of what those two endpoints
18
+ * occupy there; the arrow between them belongs to neither and is excluded before halving.
19
+ */
20
+ export const AGENT_TRAFFIC_COLUMNS = Object.freeze({
21
+ state: 3,
22
+ time: 6,
23
+ direction: 6,
24
+ peer: Math.floor((WORKSPACE_TRAFFIC_COLUMNS.from + WORKSPACE_TRAFFIC_COLUMNS.to) / 2),
25
+ type: TRAFFIC_TYPE_COLUMN_WIDTH,
26
+ });
27
+ /** Label gutter shared by the message-log metadata and lifecycle sections. */
28
+ export const MESSAGE_LABEL_GUTTER = 11;
29
+ /** Label gutter used by the agent identity modal. */
30
+ export const IDENTITY_LABEL_GUTTER = 14;
31
+ /** Share of the message-log pane the body block occupies, whatever the message holds. */
32
+ const MESSAGE_BODY_SHARE = 0.4;
33
+ /** Rows the message-log body block never falls below while the pane can afford them. */
34
+ export const MINIMUM_MESSAGE_BODY_ROWS = 3;
35
+ /** Rows the lifecycle section keeps: its label, its blank row, and one observation. */
36
+ const MINIMUM_LIFECYCLE_ROWS = 3;
37
+ /** Rows the message-log metadata block and its closing rule always occupy. */
38
+ const MESSAGE_METADATA_ROWS = 4;
39
+ /**
40
+ * Size the message-log body block from the pane rather than the message.
41
+ *
42
+ * The block is fixed for any one pane height so the panel cannot jump as the operator moves through
43
+ * the list, and it scales with the pane's height.
44
+ * @param contentRows - Rows the message-log pane gives its content, excluding header, footer, and rules.
45
+ * @returns Rows reserved for the subject and clipped body.
46
+ */
47
+ export function selectMessageBodyRows(contentRows) {
48
+ const available = contentRows - MESSAGE_METADATA_ROWS - 1;
49
+ if (available <= MINIMUM_MESSAGE_BODY_ROWS)
50
+ return Math.max(1, available);
51
+ const share = Math.round(contentRows * MESSAGE_BODY_SHARE);
52
+ return Math.min(Math.max(share, MINIMUM_MESSAGE_BODY_ROWS), available - MINIMUM_LIFECYCLE_ROWS);
53
+ }
54
+ /**
55
+ * Marks the selected row in the gutter cell that sits outside its fill.
56
+ *
57
+ * The marker is not part of the highlight, so it keeps one shape whether or not the row is filled.
58
+ */
59
+ export const SELECTION_GUTTER_GLYPH = "┃";
60
+ /** Default column count of the agent tree pane, including its own padding; see {@link sidePaneWidths} for its draggable range. */
61
+ export const AGENT_TREE_COLUMNS = 52;
62
+ /** Default column count of the message log pane, including its own padding; see {@link sidePaneWidths} for its draggable range. */
63
+ export const MESSAGE_LOG_COLUMNS = 42;
64
+ /** Narrowest the agent tree may be dragged before its own content stops being legible. */
65
+ export const MINIMUM_TREE_COLUMNS = 29;
66
+ /** Narrowest the message log may be dragged before its own content stops being legible. */
67
+ export const MINIMUM_MESSAGE_LOG_COLUMNS = 41;
68
+ /** Columns the agent log keeps for itself, bounding how far either side pane may be dragged open. */
69
+ export const MINIMUM_AGENT_LOG_COLUMNS = 55;
70
+ /** Cells the window border and the two rules between panes take before any pane is drawn. */
71
+ export const PANE_DECORATION_COLUMNS = 4;
72
+ /**
73
+ * Resolve both side panes' widths against the operator's drags and the room actually available.
74
+ *
75
+ * Each pane holds the width it was dragged to while the room allows, so widening one does not move
76
+ * the other. They yield only together: when the two of them plus the agent log's own minimum exceed
77
+ * the room, they give up the shortfall in proportion to how far each holds above its floor. A null
78
+ * request is a pane the operator has never dragged, which holds its default.
79
+ * @param request - Both panes' drag state, their fallbacks, and the room available to them.
80
+ * @returns Columns each side pane occupies; the agent log takes what is left.
81
+ */
82
+ export function sidePaneWidths({ tree, treeFallback, messageLog, messageLogFallback, available, }) {
83
+ const wanted = {
84
+ tree: Math.max(MINIMUM_TREE_COLUMNS, tree ?? treeFallback),
85
+ messageLog: Math.max(MINIMUM_MESSAGE_LOG_COLUMNS, messageLog ?? messageLogFallback),
86
+ };
87
+ const overflow = wanted.tree + wanted.messageLog + MINIMUM_AGENT_LOG_COLUMNS - available;
88
+ const slack = {
89
+ tree: wanted.tree - MINIMUM_TREE_COLUMNS,
90
+ messageLog: wanted.messageLog - MINIMUM_MESSAGE_LOG_COLUMNS,
91
+ };
92
+ const total = slack.tree + slack.messageLog;
93
+ if (overflow <= 0 || total <= 0)
94
+ return wanted;
95
+ const take = Math.min(overflow, total);
96
+ const fromTree = Math.min(slack.tree, Math.max(take - slack.messageLog, Math.round((take * slack.tree) / total)));
97
+ return { tree: wanted.tree - fromTree, messageLog: wanted.messageLog - (take - fromTree) };
98
+ }
99
+ /**
100
+ * Terminal width below which the operator surface becomes a single-pane stack.
101
+ *
102
+ * The three panes cannot all be shown below the sum of the widths each needs to stay legible, so that
103
+ * sum plus the decoration between and around them is exactly where the surface stops trying.
104
+ */
105
+ export const PORTRAIT_BREAKPOINT_COLUMNS = MINIMUM_TREE_COLUMNS + MINIMUM_AGENT_LOG_COLUMNS + MINIMUM_MESSAGE_LOG_COLUMNS + PANE_DECORATION_COLUMNS;
106
+ /**
107
+ * Decide how wide a traffic row's type column should render.
108
+ *
109
+ * The type column outranks the subject as a row narrows: a type is a whole word, while a subject
110
+ * clipped to its first few characters is worth less than the category beside it. The subject
111
+ * therefore absorbs the loss down to nothing, and only a row too narrow for the type column itself
112
+ * clips that.
113
+ * @param fixedColumnsWithoutType - The row's other fixed columns for its scope, excluding type.
114
+ * @param maxWidth - The type column's width when there is room to show it in full.
115
+ * @param rowWidth - Character cells available to the whole row, subject included.
116
+ * @returns `maxWidth`, or what remains of the row when even that does not fit.
117
+ */
118
+ export function trafficTypeColumnWidth(fixedColumnsWithoutType, maxWidth, rowWidth) {
119
+ return Math.max(0, Math.min(maxWidth, rowWidth - fixedColumnsWithoutType));
120
+ }
121
+ /** Rows the diagnostics log always occupies beneath the panes, however many entries it holds. */
122
+ export const COLLAPSED_LOG_ROWS = 3;
123
+ /**
124
+ * Render one diagnostic as a single log row.
125
+ * @param diagnostic - Content-free condition recorded by the broker or the client.
126
+ * @param width - Character cells available to the row.
127
+ * @returns The row text, clipped to the width.
128
+ */
129
+ export function formatDiagnostic(diagnostic, width) {
130
+ return truncateCells(`${formatClockSeconds(diagnostic.at)} ${diagnostic.scope} — ${diagnostic.summary}`, width);
131
+ }
132
+ /**
133
+ * Word one operator command's outcome for the diagnostics log.
134
+ *
135
+ * An answer is admission, not the resulting state change, so an accepted command is worded as
136
+ * accepted. A command that got no answer at all is worded as unknown, because the broker may have
137
+ * applied it.
138
+ * @param state - Current projection, which names the agent a command targeted.
139
+ * @param command - Command the operator submitted.
140
+ * @param result - The broker's admission answer, or null when none came back.
141
+ * @returns Severity and content-free summary for the log row.
142
+ */
143
+ export function describeCommandOutcome(state, command, result) {
144
+ const verb = { ping_agent: "ping", archive_agent: "archive", expire_message: "expire" }[command.type];
145
+ if (result === null)
146
+ return { severity: "error", summary: `${verb} outcome unknown: no answer came back from the broker` };
147
+ if (result.status === "rejected")
148
+ return { severity: "warning", summary: `${verb} refused: ${result.reason}` };
149
+ const target = command.type === "expire_message" ? command.messageId : agentHandle(state, command.targetAgentId);
150
+ return { severity: "info", summary: `${verb} accepted for ${target}` };
151
+ }
152
+ /**
153
+ * Select the colour role one diagnostic severity carries.
154
+ * @param severity - Attention the condition asks for.
155
+ * @param theme - Semantic palette in use.
156
+ * @returns The colour the row is drawn in.
157
+ */
158
+ export function diagnosticColor(severity, theme) {
159
+ switch (severity) {
160
+ case "error":
161
+ return theme.states.failed;
162
+ case "warning":
163
+ return theme.states.inProgress;
164
+ case "info":
165
+ return theme.structure.rulesAndLabels;
166
+ }
167
+ }
168
+ /**
169
+ * Render a wall-clock time without seconds for list columns.
170
+ * @param at - Epoch milliseconds recorded by the broker.
171
+ * @returns Local `HH:MM` rendering.
172
+ */
173
+ export function formatClock(at) {
174
+ const value = new Date(at);
175
+ return `${pad2(value.getHours())}:${pad2(value.getMinutes())}`;
176
+ }
177
+ /**
178
+ * Render a wall-clock time with seconds for lifecycle rows.
179
+ * @param at - Epoch milliseconds recorded by the broker.
180
+ * @returns Local `HH:MM:SS` rendering.
181
+ */
182
+ export function formatClockSeconds(at) {
183
+ const value = new Date(at);
184
+ return `${pad2(value.getHours())}:${pad2(value.getMinutes())}:${pad2(value.getSeconds())}`;
185
+ }
186
+ /**
187
+ * Fit a value into a fixed column count, marking loss with an ellipsis.
188
+ * @param value - Text to place in the column.
189
+ * @param width - Character cells available to the column.
190
+ * @returns Text no wider than the column.
191
+ */
192
+ export function truncateCells(value, width) {
193
+ if (width <= 0)
194
+ return "";
195
+ return Array.from(value).length > width
196
+ ? `${Array.from(value)
197
+ .slice(0, Math.max(0, width - 1))
198
+ .join("")}…`
199
+ : value;
200
+ }
201
+ /** Cells a pane bar leaves unused at its two edges. */
202
+ export const PANE_BAR_INSET_COLUMNS = 2;
203
+ /**
204
+ * Fit the leading text of a pane bar so the trailing text keeps its place beside it.
205
+ *
206
+ * A bar row is one cell high, so text wider than the room it is given wraps and the row shows only its
207
+ * first line, cut at a hyphen with no ellipsis. This shortens the leading text before that happens.
208
+ * @param leading - Text at the bar's left edge, given whatever the trailing text leaves.
209
+ * @param barWidth - Cells the whole bar occupies, including its inset.
210
+ * @param trailing - Text at the bar's right edge, kept whole; empty when the bar has none.
211
+ * @returns The leading text, ending in an ellipsis when it was shortened.
212
+ */
213
+ export function fitBarLeading(leading, barWidth, trailing = "") {
214
+ const gap = trailing === "" ? 0 : Array.from(trailing).length + 1;
215
+ return truncateCells(leading, barWidth - PANE_BAR_INSET_COLUMNS - gap);
216
+ }
217
+ /**
218
+ * Left-align a value in a fixed column.
219
+ * @param value - Text to place in the column.
220
+ * @param width - Character cells available to the column.
221
+ * @returns Truncated text padded to the column width.
222
+ */
223
+ export function padCells(value, width) {
224
+ const fitted = truncateCells(value, width);
225
+ return fitted + " ".repeat(Math.max(0, width - Array.from(fitted).length));
226
+ }
227
+ /**
228
+ * Left-align a value in a fixed column, reserving its final cell so the next column cannot abut.
229
+ * @param value - Text to place in the column.
230
+ * @param width - Character cells available to the column, including the reserved separator.
231
+ * @returns Text truncated one cell short of the column and padded to its full width.
232
+ */
233
+ export function padCellsBeforeColumn(value, width) {
234
+ return padCells(truncateCells(value, Math.max(0, width - 1)), width);
235
+ }
236
+ /**
237
+ * Right-align a value in a fixed column.
238
+ * @param value - Text to place in the column.
239
+ * @param width - Character cells available to the column.
240
+ * @returns Truncated text padded to the column width.
241
+ */
242
+ export function padCellsEnd(value, width) {
243
+ const fitted = truncateCells(value, width);
244
+ return " ".repeat(Math.max(0, width - Array.from(fitted).length)) + fitted;
245
+ }
246
+ /**
247
+ * Assign one stable palette slot to an agent identity.
248
+ * @param agentId - Durable broker agent identity.
249
+ * @param theme - Semantic palette in use.
250
+ * @returns The agent's colour, stable everywhere its handle appears.
251
+ */
252
+ export function agentColor(agentId, theme) {
253
+ let hash = 0;
254
+ for (const character of agentId)
255
+ hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
256
+ return theme.agents[hash % theme.agents.length] ?? theme.structure.bodyText;
257
+ }
258
+ /**
259
+ * Render the handle an operator reads for an agent identity.
260
+ * @param state - Current projection and view state.
261
+ * @param agentId - Durable broker agent identity.
262
+ * @returns The routable handle, or the raw identity when the agent is not projected.
263
+ */
264
+ export function agentHandle(state, agentId) {
265
+ const agent = state.snapshot?.agents.find((entry) => entry.id === agentId);
266
+ return agent ? agentHandleText(agent.alias) : agentId;
267
+ }
268
+ /**
269
+ * Render one alias as the handle an operator reads.
270
+ *
271
+ * The projection delivers aliases without their at-sign, and this adds the one; a leading at-sign
272
+ * already present is kept rather than doubled.
273
+ * @param alias - Projected agent alias.
274
+ * @returns The alias with exactly one leading at-sign.
275
+ */
276
+ export function agentHandleText(alias) {
277
+ return alias.startsWith("@") ? alias : `@${alias}`;
278
+ }
279
+ /**
280
+ * Render one agent as the full address that reaches it from any workspace.
281
+ * @param state - Current operator model state supplying the projected agents.
282
+ * @param agentId - Durable agent identity to name.
283
+ * @returns The handle followed by an at-sign and the agent's workspace name.
284
+ */
285
+ export function agentAddress(state, agentId) {
286
+ const agent = state.snapshot?.agents.find((entry) => entry.id === agentId);
287
+ return agent ? `${agentHandleText(agent.alias)}@${agent.workspace}` : agentId;
288
+ }
289
+ /**
290
+ * Name the implementation and worktree detail shown beside an agent handle.
291
+ * @param agent - Projected agent.
292
+ * @returns Secondary detail text, without the handle.
293
+ */
294
+ export function agentDetail(agent) {
295
+ const implementation = agent.implementation.available
296
+ ? agent.implementation.displayName
297
+ : (agent.implementation.id ?? "unavailable");
298
+ return agent.repository.worktree.kind === "worktree"
299
+ ? `${implementation} ⑂ ${agent.repository.worktree.path}`
300
+ : implementation;
301
+ }
302
+ /**
303
+ * Select the glyph carried by a delivery state in the tree and traffic lists.
304
+ * @param state - Durable delivery state.
305
+ * @returns The one-cell state glyph.
306
+ */
307
+ export function deliveryGlyph(state) {
308
+ switch (state) {
309
+ case "queued":
310
+ case "claimed":
311
+ return "◐";
312
+ case "acknowledged":
313
+ return "●";
314
+ case "expired":
315
+ return "⊖";
316
+ case "dead_lettered":
317
+ case "failed":
318
+ return "✕";
319
+ }
320
+ }
321
+ /**
322
+ * Select the colour carried by a delivery state.
323
+ * @param state - Durable delivery state, or null for an observation that established none.
324
+ * @param theme - Semantic palette in use.
325
+ * @returns The state colour, or secondary text for a stateless observation.
326
+ */
327
+ export function deliveryColor(state, theme) {
328
+ switch (state) {
329
+ case "acknowledged":
330
+ return theme.states.complete;
331
+ case "queued":
332
+ case "claimed":
333
+ return theme.states.inProgress;
334
+ case "failed":
335
+ case "dead_lettered":
336
+ return theme.states.failed;
337
+ case "expired":
338
+ return theme.states.expired;
339
+ case null:
340
+ return theme.structure.rulesAndLabels;
341
+ }
342
+ }
343
+ /**
344
+ * Read the headline an operator scans a traffic row by.
345
+ * @param entry - Traffic row from the authoritative projection.
346
+ * @returns The durable subject for a delivery, and nothing for an audit, which carries no subject.
347
+ */
348
+ export function trafficSubject(entry) {
349
+ switch (entry.kind) {
350
+ case "ping_audit":
351
+ return "";
352
+ case "accepted_delivery":
353
+ return entry.subject;
354
+ }
355
+ }
356
+ /**
357
+ * Read the type of a traffic row, apart from its subject.
358
+ * @param entry - Traffic row from the authoritative projection.
359
+ * @returns The sender-supplied type of a delivery, or the control word for an audit.
360
+ */
361
+ export function trafficType(entry) {
362
+ return entry.kind === "accepted_delivery" ? entry.type : "ping";
363
+ }
364
+ /**
365
+ * Render a retained message body as the sentence an operator reads, not its wire JSON.
366
+ *
367
+ * A bare string body renders as itself. A peer sending a `{ type, detail }` envelope falls back to
368
+ * its `detail`. Every other shape the broker's opaque body admits — an array, an object
369
+ * with no string `detail` — has no defined readable subset, so it falls back to the full body rather
370
+ * than inventing a partial rendering. Exact wire-byte access does not depend on this: the raw-copy
371
+ * action reads the body's own retained representation, untouched by this function.
372
+ * @param content - Retained message body content.
373
+ * @returns Text for the message-log pane and full-message overlay.
374
+ */
375
+ export function messageBodyDisplayText(content) {
376
+ if (typeof content === "string")
377
+ return content;
378
+ if (content !== null && typeof content === "object" && !Array.isArray(content) && typeof content.detail === "string")
379
+ return content.detail;
380
+ return JSON.stringify(content);
381
+ }
382
+ /**
383
+ * Describe the scope heading the agent log presents for the current tree selection.
384
+ * @param state - Current projection and view state.
385
+ * @returns Heading title, its trailing detail, and the scope kind, with the agent's identity when one agent scopes the log.
386
+ */
387
+ export function agentLogScope(state) {
388
+ const count = selectTuiTraffic(state).length;
389
+ const selection = state.view.selection;
390
+ if (selection?.kind === "agent") {
391
+ const agent = state.snapshot?.agents.find((entry) => entry.id === selection.agentId);
392
+ const repository = state.snapshot?.repositories.find((entry) => entry.id === agent?.repository.repositoryId);
393
+ const workspace = repository ? repository.name : "unknown workspace";
394
+ return {
395
+ kind: "agent",
396
+ title: agent
397
+ ? `${agentHandle(state, selection.agentId)} · ${agentDetail(agent)}`
398
+ : agentHandle(state, selection.agentId),
399
+ detail: `${workspace} · ${String(count)} msgs`,
400
+ agentId: selection.agentId,
401
+ };
402
+ }
403
+ const repository = selection === null || selection.kind === "hidden"
404
+ ? undefined
405
+ : state.snapshot?.repositories.find((entry) => entry.id === selection.repositoryId);
406
+ return { kind: "workspace", title: repository ? repository.name : "messages", detail: `${String(count)} msgs` };
407
+ }
408
+ /**
409
+ * Count the agents the operator is being asked to keep an eye on.
410
+ *
411
+ * Archived identities are excluded: they are kept for their history and their conversations may
412
+ * return, but they are not part of what is running now. So are the agents of a hidden workspace,
413
+ * which the operator has said they do not want to see.
414
+ * @param state - Current authoritative projection.
415
+ * @returns - Number of agents that are neither archived nor in a hidden workspace.
416
+ */
417
+ export function activeAgentCount(state) {
418
+ return (state.snapshot?.agents ?? []).filter((agent) => !isOperatorAgentArchived(agent) && !state.view.hiddenRepositories.has(agent.repository.repositoryId)).length;
419
+ }
420
+ /**
421
+ * Report the broker indicator and its colour role.
422
+ * @param connection - Current transport condition held by the model.
423
+ * @param theme - Semantic palette in use.
424
+ * @returns Indicator glyph and colour.
425
+ */
426
+ export function brokerIndicator(connection, theme) {
427
+ switch (connection) {
428
+ case "healthy":
429
+ return { glyph: "●", color: theme.states.complete };
430
+ case "recovering":
431
+ return { glyph: "◐", color: theme.states.inProgress };
432
+ case "incompatible":
433
+ return { glyph: "✕", color: theme.states.failed };
434
+ case "cold":
435
+ case "disposed":
436
+ return { glyph: "○", color: theme.structure.rulesAndLabels };
437
+ }
438
+ }
439
+ /**
440
+ * Move a menu selection one entry, wrapping at both ends.
441
+ * @param index - Current selection.
442
+ * @param direction - Requested movement.
443
+ * @param count - Number of entries to move within.
444
+ * @returns - Next selection, zero for a menu with no entries.
445
+ */
446
+ export function stepWrappingIndex(index, direction, count) {
447
+ if (count < 1)
448
+ return 0;
449
+ return (index + (direction === "up" ? count - 1 : 1)) % count;
450
+ }
451
+ /**
452
+ * Select traffic that belongs to the local tree selection without treating audits as deliveries.
453
+ * @param state - Current projection and view state.
454
+ * @returns Traffic rows in projection order, empty where the selection scopes none.
455
+ */
456
+ export function selectTuiTraffic(state) {
457
+ const traffic = state.snapshot?.traffic ?? [];
458
+ const selection = state.view.selection;
459
+ // Traffic is read through a selection. Without one there is nothing in scope, rather than
460
+ // everything: an unscoped list belongs to no workspace and answers no question the operator asked.
461
+ if (!selection || selection.kind === "hidden")
462
+ return [];
463
+ if (selection.kind === "agent")
464
+ return traffic.filter((entry) => trafficTouchesAgent(entry, selection.agentId));
465
+ const agentIds = new Set((state.snapshot?.agents ?? [])
466
+ .filter((agent) => agent.repository.repositoryId === selection.repositoryId)
467
+ .map((agent) => agent.id));
468
+ return traffic.filter((entry) => trafficTouchesAny(entry, agentIds));
469
+ }
470
+ /**
471
+ * Read the traffic row the agent log has selected.
472
+ * @param state - Current projection and view state.
473
+ * @returns The selected row, or undefined when nothing is selected or it has left the projection.
474
+ */
475
+ export function selectedTraffic(state) {
476
+ const trafficId = state.view.trafficId;
477
+ return trafficId === null ? undefined : state.snapshot?.traffic.find((entry) => entry.trafficId === trafficId);
478
+ }
479
+ /**
480
+ * Read the accepted delivery the message log presents.
481
+ * @param state - Current projection and view state.
482
+ * @returns The selected delivery, or undefined when the selected row is not one.
483
+ */
484
+ export function selectedMessage(state) {
485
+ const entry = selectedTraffic(state);
486
+ return entry?.kind === "accepted_delivery" ? entry : undefined;
487
+ }
488
+ /**
489
+ * Find one accepted delivery by its durable message identity.
490
+ * @param state - Current projection and view state.
491
+ * @param messageId - Durable message the caller holds an identity for.
492
+ * @returns The delivery, or undefined when the projection no longer carries it.
493
+ */
494
+ export function findTuiMessage(state, messageId) {
495
+ const entry = state.snapshot?.traffic.find((traffic) => traffic.kind === "accepted_delivery" && traffic.messageId === messageId);
496
+ return entry?.kind === "accepted_delivery" ? entry : undefined;
497
+ }
498
+ /**
499
+ * Report whether one traffic row involves a single agent.
500
+ * @param entry - Traffic row from the authoritative projection.
501
+ * @param agentId - Durable broker agent identity.
502
+ * @returns Whether the row belongs to the agent's filtered log.
503
+ */
504
+ export function trafficTouchesAgent(entry, agentId) {
505
+ switch (entry.kind) {
506
+ case "accepted_delivery":
507
+ return entry.fromAgentId === agentId || entry.toAgentId === agentId;
508
+ case "ping_audit":
509
+ return entry.targetAgentId === agentId;
510
+ }
511
+ }
512
+ /**
513
+ * Report whether one traffic row involves any agent of a workspace.
514
+ * @param entry - Traffic row from the authoritative projection.
515
+ * @param agentIds - Durable identities grouped under one operator repository.
516
+ * @returns Whether the row belongs to the workspace log.
517
+ */
518
+ export function trafficTouchesAny(entry, agentIds) {
519
+ switch (entry.kind) {
520
+ case "accepted_delivery":
521
+ return agentIds.has(entry.fromAgentId) || agentIds.has(entry.toAgentId);
522
+ case "ping_audit":
523
+ return agentIds.has(entry.targetAgentId);
524
+ }
525
+ }
526
+ /**
527
+ * Read the time a traffic row is ordered and listed by.
528
+ * @param entry - Traffic row from the authoritative projection.
529
+ * @returns Epoch milliseconds shown in the time column.
530
+ */
531
+ export function trafficTime(entry) {
532
+ switch (entry.kind) {
533
+ case "accepted_delivery":
534
+ return entry.acceptedAt;
535
+ case "ping_audit":
536
+ return entry.acceptedAt;
537
+ }
538
+ }
539
+ function pad2(value) {
540
+ return String(value).padStart(2, "0");
541
+ }
@@ -0,0 +1,79 @@
1
+ import { type OperatorTuiProps } from "./app.js";
2
+ import { startOpenTuiFactory, type OpenTuiRuntimeOptions } from "./composition.js";
3
+ import { type TuiLayoutPort, type TuiLocalCommandPort, type TuiSchedulerPort } from "./model.js";
4
+ import type { TuiTheme } from "./theme.js";
5
+ export type { TuiHostRegistration } from "./actions.js";
6
+ /** Canonical semantic palette selected by the production terminal command. */
7
+ export declare const DEFAULT_OPERATOR_TUI_THEME: Readonly<TuiTheme>;
8
+ /**
9
+ * Start the production terminal surface from explicitly composed client and presentation ports.
10
+ * @param options - Model, presentation ports, and optional renderer configuration.
11
+ * @param start - Starts the renderer, supplying the shutdown action to the factory that builds the root element; the OpenTUI starter by default.
12
+ * @returns Running renderer lifecycle whose caller owns the final shutdown request.
13
+ */
14
+ export declare function startOperatorTui(options: Omit<OperatorTuiProps, "onShutdown"> & {
15
+ readonly runtime?: OpenTuiRuntimeOptions;
16
+ }, start?: typeof startOpenTuiFactory): Promise<import("./runtime.ts").TuiRuntime<import("@opentui/core").CliRenderer>>;
17
+ /** Options for {@link createOperatorTuiClient}. */
18
+ export type CreateOperatorTuiClientOptions = {
19
+ /** Operator endpoint the broker published; no connection opens until the model starts. */
20
+ operatorUrl: string;
21
+ /** Palette every view receives; the dimmed backdrop for overlays is derived from it. */
22
+ theme: Readonly<TuiTheme>;
23
+ /** Copies raw text where the terminal cannot; the operating system's commands unless injected. */
24
+ clipboard?: TuiNativeClipboardPort;
25
+ /** Runs operator actions on this machine; a pseudo-terminal spawn unless injected. */
26
+ localCommand?: TuiLocalCommandPort;
27
+ /** Carries dragged pane widths between runs, or undefined to keep them for this run only. */
28
+ layout?: TuiLayoutPort;
29
+ };
30
+ /**
31
+ * Build the production model and retained-body adapter from the published operator state endpoint.
32
+ * @param options - Endpoint, palette, and optional clipboard, local-command, and layout ports.
33
+ * @returns The shell's model, palette, body reader, copy fallback and command port, without its shutdown action or action catalogue.
34
+ */
35
+ export declare function createOperatorTuiClient({ operatorUrl, theme, clipboard, localCommand, layout, }: CreateOperatorTuiClientOptions): Omit<OperatorTuiProps, "onShutdown" | "actions">;
36
+ /**
37
+ * Keep dragged pane widths in a file beside the operator's other local state.
38
+ *
39
+ * Every failure resolves to "no stored layout": the file is a convenience, and an operator locked out
40
+ * of the surface because a width could not be parsed would be worse off than one whose panes reset.
41
+ * @param storeDirectory - Directory the operator's local state lives in.
42
+ * @returns - Port reading and writing that directory's layout file.
43
+ */
44
+ export declare function fileBackedTuiLayout(storeDirectory: string): TuiLayoutPort;
45
+ /** Copies raw message content through a native mechanism when OSC52 is unavailable. */
46
+ export interface TuiNativeClipboardPort {
47
+ /**
48
+ * Attempts one local clipboard write without reporting message content.
49
+ * @param rawCopy - Exact content to copy.
50
+ * @returns Whether the platform reported success.
51
+ */
52
+ copy(rawCopy: string): boolean;
53
+ }
54
+ /**
55
+ * Run a local command attached to a pseudo-terminal, streaming its raw bytes to the model.
56
+ *
57
+ * Output is captured rather than inherited: the renderer owns the terminal, so a child writing to it
58
+ * directly would overwrite the frame.
59
+ *
60
+ * The child is given a pseudo-terminal rather than pipes. Seeing pipes, a child reports no terminal
61
+ * and takes whatever silent non-interactive path it has, so an operator could never answer a question
62
+ * it asks.
63
+ * @returns - Port that spawns through the operating system.
64
+ */
65
+ export declare function systemLocalCommand(): TuiLocalCommandPort;
66
+ /**
67
+ * Build the reconnect scheduler over an explicit timer, so the delay the model asks for is observable.
68
+ * @param set - Arms one timer for the requested delay and returns the handle that cancels it.
69
+ * @param clear - Cancels a handle `set` returned, whether or not it has already fired.
70
+ * @returns Scheduler port whose cancellation callback clears the handle it armed.
71
+ */
72
+ export declare function createTimerScheduler<Handle>(set: (callback: () => void, delayMs: number) => Handle, clear: (handle: Handle) => void): TuiSchedulerPort;
73
+ /**
74
+ * Build a bounded native clipboard port from explicit platform command execution.
75
+ * @param platform - Operating system whose clipboard commands are tried, in order; any other copies nothing.
76
+ * @param run - Runs one command with the raw text on standard input, reporting success; a throw counts as failure.
77
+ * @returns Port that reports whether any command copied the text within its time limit.
78
+ */
79
+ export declare function createNativeClipboardPort(platform: NodeJS.Platform, run: (command: string, arguments_: readonly string[], rawCopy: string, timeoutMs: number) => boolean): TuiNativeClipboardPort;