@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,515 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
2
+ import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
3
+ import { useState } from "react";
4
+ import { isTuiExpirable } from "../operator/agent-projection.js";
5
+ import { OPERATOR_PROTOCOL_VERSION, } from "../operator/protocol.js";
6
+ import { Line, Overlay, PaneBar, Rule, SCROLLBAR_RESERVED_COLUMNS, VERTICAL_SCROLL_STYLE, WRAPPED_TEXT_SCROLL_STYLE, fitOverlayColumns, useTuiModel, } from "./chrome.js";
7
+ import { AGENT_TRAFFIC_COLUMNS, MESSAGE_LABEL_GUTTER, MINIMUM_MESSAGE_BODY_ROWS, MESSAGE_LOG_COLUMNS, SELECTION_GUTTER_GLYPH, WORKSPACE_TRAFFIC_COLUMNS, agentColor, agentHandle, agentAddress, agentLogScope, deliveryColor, deliveryGlyph, describeCommandOutcome, findTuiMessage, fitBarLeading, formatClock, formatClockSeconds, PANE_BAR_INSET_COLUMNS, padCells, padCellsBeforeColumn, padCellsEnd, selectedMessage, selectTuiTraffic, stepWrappingIndex, trafficSubject, trafficTime, trafficType, trafficTypeColumnWidth, truncateCells, } from "./presentation.js";
8
+ /* eslint-enable jsdoc/require-jsdoc -- The port and property declarations end here. */
9
+ /**
10
+ * Render a distinct, renderer-neutral description for each traffic protocol member.
11
+ * @param entry - Traffic row of either member; identities are printed raw rather than as handles.
12
+ * @returns One line giving a delivery's state glyph, endpoints and message id, or an audit's glyph, target and the word ping.
13
+ */
14
+ export function formatTuiTraffic(entry) {
15
+ switch (entry.kind) {
16
+ case "accepted_delivery":
17
+ return `${deliveryGlyph(entry.state)} ${entry.fromAgentId} → ${entry.toAgentId} ${entry.messageId}`;
18
+ case "ping_audit":
19
+ return `⍾ broker → ${entry.targetAgentId} ping`;
20
+ }
21
+ }
22
+ /**
23
+ * Move the agent-log selection one row without wrapping past either end.
24
+ * @param model - Model owning the local view state.
25
+ * @param direction - Row step the operator asked for.
26
+ * @returns Whether the log consumed the step.
27
+ */
28
+ export function stepTuiTrafficSelection(model, direction) {
29
+ const rows = selectTuiTraffic(model.state);
30
+ if (rows.length === 0)
31
+ return false;
32
+ const current = rows.findIndex((entry) => entry.trafficId === model.state.view.trafficId);
33
+ const step = current < 0 ? 0 : current + (direction === "up" ? -1 : 1);
34
+ if (step < 0 || step > rows.length - 1)
35
+ return false;
36
+ const next = rows[step];
37
+ if (!next)
38
+ return false;
39
+ model.viewTraffic(next);
40
+ model.updateView((view) => ({ ...view, focus: "agent_log", followAgentLog: false }));
41
+ return true;
42
+ }
43
+ /**
44
+ * Select the newest row of the agent log for an operator arriving with nothing chosen.
45
+ *
46
+ * Follow is turned on rather than preserved: the selection lands on the tail, so the operator is at
47
+ * the position follow tracks and has not scrolled away from it.
48
+ * @param model - Operator model whose view selection is advanced.
49
+ * @returns Whether a row was available to select.
50
+ */
51
+ export function selectNewestTuiTraffic(model) {
52
+ const rows = selectTuiTraffic(model.state);
53
+ const newest = rows[rows.length - 1];
54
+ if (newest)
55
+ model.viewTraffic(newest);
56
+ model.updateView((view) => ({ ...view, followAgentLog: true }));
57
+ return newest !== undefined;
58
+ }
59
+ /**
60
+ * Render traffic rows and keep tail-follow state behind the model boundary.
61
+ * @param properties - Model, palette, whether agents are coloured, and the pane's columns.
62
+ * @returns A column heading over scrolling rows, or over a placeholder when the selection scopes no traffic.
63
+ */
64
+ export function AgentTrafficLog({ model, theme, colorEnabled = true, width = MESSAGE_LOG_COLUMNS, }) {
65
+ const state = useTuiModel(model);
66
+ const traffic = selectTuiTraffic(state);
67
+ const scope = agentLogScope(state);
68
+ const focused = state.view.focus === "agent_log";
69
+ const select = (entry) => {
70
+ model.viewTraffic(entry);
71
+ model.updateView((view) => ({
72
+ ...view,
73
+ focus: "agent_log",
74
+ overlay: closeOtherMessageOverlay(view.overlay, entry),
75
+ }));
76
+ };
77
+ const pause = () => {
78
+ model.updateView((view) => ({ ...view, followAgentLog: false }));
79
+ };
80
+ const openContext = (entry) => {
81
+ select(entry);
82
+ if (entry.kind === "accepted_delivery")
83
+ model.updateView((view) => ({
84
+ ...view,
85
+ overlay: { kind: "context_menu", target: "message", messageId: entry.messageId },
86
+ overlayReturnFocus: view.focus,
87
+ contextActionIndex: 0,
88
+ }));
89
+ };
90
+ useKeyboard((key) => {
91
+ // An overlay the shell owns takes the keyboard whole, so this pane answers nothing behind one.
92
+ if (model.state.view.overlay.kind !== "none")
93
+ return;
94
+ if (model.state.view.focus !== "agent_log")
95
+ return;
96
+ if (key.name === "f")
97
+ model.updateView((view) => ({ ...view, followAgentLog: true }));
98
+ if (key.name === "a") {
99
+ const selected = selectTuiTraffic(model.state).find((entry) => entry.trafficId === model.state.view.trafficId);
100
+ if (selected)
101
+ openContext(selected);
102
+ }
103
+ });
104
+ return (_jsxs("box", { flexDirection: "column", flexGrow: 1, paddingLeft: 1, paddingRight: 1, children: [_jsx(Line, { children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: trafficHeadingRow(scope.kind, width - 2 - SCROLLBAR_RESERVED_COLUMNS) }) }), _jsx(Rule, { theme: theme }), traffic.length === 0 ? (_jsx(Centered, { theme: theme, text: "no messages" })) : (_jsx("scrollbox", { flexGrow: 1, focused: focused, stickyScroll: state.view.followAgentLog, stickyStart: "bottom", onMouseScroll: pause, style: VERTICAL_SCROLL_STYLE, children: traffic.map((entry) => (_jsx(TrafficRow, { entry: entry, state: state, theme: theme, colorEnabled: colorEnabled, scope: scope.kind, width: width - 2 - SCROLLBAR_RESERVED_COLUMNS, onSelect: select, onContext: openContext }, entry.trafficId))) }))] }));
105
+ }
106
+ /**
107
+ * Render the selected accepted delivery's metadata, lifecycle, scrolling body, and full-body affordance.
108
+ * @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.
109
+ * @returns The message log pane content, or a placeholder when no delivery is selected.
110
+ */
111
+ export function MessageDetail({ model, theme, body, colorEnabled = true, width: paneWidth = MESSAGE_LOG_COLUMNS, bodyRows = MINIMUM_MESSAGE_BODY_ROWS, }) {
112
+ const state = useTuiModel(model);
113
+ const message = selectedMessage(state);
114
+ const focused = state.view.focus === "message_log";
115
+ useKeyboard((key) => {
116
+ if (model.state.view.overlay.kind !== "none")
117
+ return;
118
+ const selected = selectedMessage(model.state);
119
+ if (key.name === "o" &&
120
+ (model.state.view.focus === "message_log" || model.state.view.focus === "agent_log") &&
121
+ selected)
122
+ model.updateView((view) => ({
123
+ ...view,
124
+ overlay: { kind: "full_message", messageId: selected.messageId },
125
+ overlayReturnFocus: view.focus,
126
+ }));
127
+ });
128
+ if (!message)
129
+ return _jsx(Centered, { theme: theme, text: "no message selected" });
130
+ const content = body.read(message);
131
+ const bodyText = content.display;
132
+ const width = paneWidth - 2;
133
+ const type = trafficType(message);
134
+ const repository = state.snapshot?.repositories.find((entry) => entry.id === state.snapshot?.agents.find((agent) => agent.id === message.fromAgentId)?.repository.repositoryId);
135
+ return (_jsxs("box", { flexDirection: "column", flexGrow: 1, onMouseDown: () => {
136
+ model.updateView((view) => ({ ...view, focus: "message_log" }));
137
+ }, children: [_jsxs("box", { flexDirection: "column", flexShrink: 0, paddingLeft: 1, paddingRight: 1, children: [_jsx(MetadataRow, { label: "from", value: agentAddress(state, message.fromAgentId), color: colorEnabled ? agentColor(message.fromAgentId, theme) : theme.structure.bodyText, theme: theme, width: width }), _jsx(MetadataRow, { label: "to", value: agentAddress(state, message.toAgentId), color: colorEnabled ? agentColor(message.toAgentId, theme) : theme.structure.bodyText, theme: theme, width: width }), _jsx(MetadataRow, { label: "workspace", value: repository ? repository.name : message.workspaceId, color: theme.structure.bodyText, theme: theme, width: width }), _jsx(MetadataRow, { label: "type", value: type, color: theme.structure.bodyText, theme: theme, width: width })] }), _jsx(Rule, { theme: theme }), _jsxs("box", { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", paddingLeft: 1, paddingRight: 1, children: [_jsx(Line, { children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "LIFECYCLE" }) }), _jsx(Line, {}), _jsx("scrollbox", { flexGrow: 1, focused: focused, stickyScroll: true, stickyStart: "bottom", style: VERTICAL_SCROLL_STYLE, children: message.lifecycle.map((entry, index) => (_jsx(Line, { children: _jsxs("text", { fg: lifecycleColor(entry, index === message.lifecycle.length - 1, theme), children: [_jsx("span", { fg: theme.structure.rulesAndLabels, children: padCells(formatClockSeconds(entry.at), MESSAGE_LABEL_GUTTER) }), truncateCells(entry.type, width - MESSAGE_LABEL_GUTTER - SCROLLBAR_RESERVED_COLUMNS)] }) }, entry.id))) })] }), _jsx(Rule, { theme: theme }), _jsxs("box", { flexDirection: "column", flexShrink: 0, overflow: "hidden", paddingLeft: 1, paddingRight: 1, children: [_jsx(Line, { children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: truncateCells(trafficSubject(message), width) }) }), _jsx(Line, {}), _jsx("scrollbox", { height: Math.max(1, bodyRows - 2), focused: focused, style: WRAPPED_TEXT_SCROLL_STYLE, children: _jsx("text", { wrapMode: "word", fg: theme.structure.emphasis, children: bodyText }) })] })] }));
138
+ }
139
+ /**
140
+ * Render the full-body, message context, and expire confirmation overlays at the terminal root.
141
+ * @param properties - Model whose overlay state chooses what renders, and the body, copy and command ports the overlays use.
142
+ * @returns The open message overlay, or nothing when the open overlay is not a message overlay or its message has left the projection.
143
+ */
144
+ export function MessageOverlays(properties) {
145
+ const state = useTuiModel(properties.model);
146
+ const overlay = state.view.overlay;
147
+ if (overlay.kind === "full_message") {
148
+ const message = findTuiMessage(state, overlay.messageId);
149
+ return message ? _jsx(FullMessageOverlay, { ...properties, message: message }) : null;
150
+ }
151
+ if (overlay.kind === "expire_confirm") {
152
+ const message = findTuiMessage(state, overlay.messageId);
153
+ return message ? _jsx(ExpireConfirmOverlay, { ...properties, message: message }) : null;
154
+ }
155
+ return overlay.kind === "context_menu" && overlay.target === "message" ? (_jsx(MessageContextOverlay, { ...properties })) : null;
156
+ }
157
+ function MetadataRow({ label, value, color, theme, width, }) {
158
+ return (_jsx(Line, { children: _jsxs("text", { fg: color, children: [_jsx("span", { fg: theme.structure.rulesAndLabels, children: padCells(label, MESSAGE_LABEL_GUTTER) }), truncateCells(value, width - MESSAGE_LABEL_GUTTER)] }) }));
159
+ }
160
+ function fixedTrafficColumnsWithoutType(scope) {
161
+ return scope === "agent"
162
+ ? AGENT_TRAFFIC_COLUMNS.state +
163
+ AGENT_TRAFFIC_COLUMNS.time +
164
+ AGENT_TRAFFIC_COLUMNS.direction +
165
+ AGENT_TRAFFIC_COLUMNS.peer
166
+ : WORKSPACE_TRAFFIC_COLUMNS.state +
167
+ WORKSPACE_TRAFFIC_COLUMNS.time +
168
+ WORKSPACE_TRAFFIC_COLUMNS.from +
169
+ WORKSPACE_TRAFFIC_COLUMNS.arrow +
170
+ WORKSPACE_TRAFFIC_COLUMNS.to;
171
+ }
172
+ function trafficTypeColumnMax(scope) {
173
+ return scope === "agent" ? AGENT_TRAFFIC_COLUMNS.type : WORKSPACE_TRAFFIC_COLUMNS.type;
174
+ }
175
+ /** Blank standing where each row shows its selection glyph, keeping the heading over its columns. */
176
+ const SELECTION_GUTTER_HEADING = " ";
177
+ /**
178
+ * Report the cells a traffic row's columns share.
179
+ *
180
+ * One cell is spent before the first column, on the selection glyph a row shows and its heading
181
+ * reserves, so the columns cannot have the row's full width.
182
+ * @param rowWidth - Cells the row occupies, already inset by its pane's padding and scrollbar.
183
+ * @returns Cells the columns may divide between them.
184
+ */
185
+ function trafficColumnRoom(rowWidth) {
186
+ return Math.max(0, rowWidth - 1);
187
+ }
188
+ /**
189
+ * Report the cells the type column takes in one scope at one row width.
190
+ * @param scope - Whether a workspace or a single agent scopes the list.
191
+ * @param rowWidth - Cells the row occupies, already inset by its pane's padding and scrollbar.
192
+ * @returns Cells the type column occupies, zero where the row cannot afford it.
193
+ */
194
+ function trafficTypeColumn(scope, rowWidth) {
195
+ return trafficTypeColumnWidth(fixedTrafficColumnsWithoutType(scope), trafficTypeColumnMax(scope), trafficColumnRoom(rowWidth));
196
+ }
197
+ function fixedTrafficColumns(scope, rowWidth) {
198
+ return fixedTrafficColumnsWithoutType(scope) + trafficTypeColumn(scope, rowWidth);
199
+ }
200
+ /**
201
+ * Render the traffic table's column heading.
202
+ *
203
+ * Each row reserves a leading cell for the selection glyph outside its column layout, so the heading
204
+ * reserves the same cell; without it every heading label sits one cell left of the column it names.
205
+ * @param scope - Whether a workspace or a single agent scopes the list.
206
+ * @param rowWidth - Character cells the row occupies, deciding whether the type column is shown.
207
+ * @returns The heading line, aligned to the rows beneath it.
208
+ */
209
+ function trafficHeadingRow(scope, rowWidth) {
210
+ const typeColumnWidth = trafficTypeColumn(scope, rowWidth);
211
+ const type = typeColumnWidth === 0 ? "" : padCellsBeforeColumn("TYPE", typeColumnWidth);
212
+ if (scope === "agent")
213
+ return `${SELECTION_GUTTER_HEADING}${" ".repeat(AGENT_TRAFFIC_COLUMNS.state)}${padCells("TIME", AGENT_TRAFFIC_COLUMNS.time)}${padCellsBeforeColumn("PEER", AGENT_TRAFFIC_COLUMNS.peer)}${center("DIR", AGENT_TRAFFIC_COLUMNS.direction)}${type}SUBJECT`;
214
+ return `${SELECTION_GUTTER_HEADING}${" ".repeat(WORKSPACE_TRAFFIC_COLUMNS.state)}${padCells("TIME", WORKSPACE_TRAFFIC_COLUMNS.time)}${padCellsEnd("FROM", WORKSPACE_TRAFFIC_COLUMNS.from)}${center("→", WORKSPACE_TRAFFIC_COLUMNS.arrow)}${padCellsBeforeColumn("TO", WORKSPACE_TRAFFIC_COLUMNS.to)}${type}`;
215
+ }
216
+ function TrafficRow({ entry, state, theme, colorEnabled, scope, width, onSelect, onContext, }) {
217
+ const selected = state.view.trafficId === entry.trafficId;
218
+ const glyph = trafficGlyph(entry, theme);
219
+ const type = trafficType(entry);
220
+ const subject = trafficSubject(entry);
221
+ const typeColumnWidth = trafficTypeColumn(scope, width);
222
+ const focused = state.view.focus === "agent_log";
223
+ const filled = selected && focused;
224
+ const rowColor = filled
225
+ ? theme.structure.selection.text
226
+ : entry.kind === "ping_audit"
227
+ ? theme.structure.rulesAndLabels
228
+ : theme.structure.bodyText;
229
+ const peerColor = (agentId) => filled || !colorEnabled || agentId === null ? rowColor : agentColor(agentId, theme);
230
+ return (_jsxs("box", { height: 1, flexDirection: "row", onMouseDown: (event) => {
231
+ if (event.button === 2)
232
+ onContext(entry);
233
+ else
234
+ onSelect(entry);
235
+ }, children: [_jsx("text", { fg: theme.structure.selection.background, children: selected ? SELECTION_GUTTER_GLYPH : " " }), _jsx("box", { flexGrow: 1, flexDirection: "row", ...(filled ? { backgroundColor: theme.structure.selection.background } : {}), children: _jsxs("text", { fg: rowColor, children: [_jsx("span", { fg: glyph.color, children: padCells(` ${glyph.glyph} `, WORKSPACE_TRAFFIC_COLUMNS.state) }), padCells(formatClock(trafficTime(entry)), WORKSPACE_TRAFFIC_COLUMNS.time), scope === "agent" ? (_jsx(AgentScopeCells, { entry: entry, state: state, rowColor: rowColor, peerColor: peerColor })) : (_jsx(WorkspaceScopeCells, { entry: entry, state: state, peerColor: peerColor })), typeColumnWidth === 0 ? "" : padCellsBeforeColumn(type, typeColumnWidth), scope === "agent"
236
+ ? truncateCells(subject, Math.max(0, trafficColumnRoom(width) - fixedTrafficColumns(scope, width)))
237
+ : ""] }) })] }));
238
+ }
239
+ function WorkspaceScopeCells({ entry, state, peerColor, }) {
240
+ const from = trafficFrom(entry, state);
241
+ const to = trafficTo(entry, state);
242
+ return (_jsxs(_Fragment, { children: [_jsx("span", { fg: peerColor(from.agentId), children: padCellsEnd(from.label, WORKSPACE_TRAFFIC_COLUMNS.from) }), center("→", WORKSPACE_TRAFFIC_COLUMNS.arrow), _jsx("span", { fg: peerColor(to.agentId), children: padCellsBeforeColumn(to.label, WORKSPACE_TRAFFIC_COLUMNS.to) })] }));
243
+ }
244
+ function AgentScopeCells({ entry, state, rowColor, peerColor, }) {
245
+ const selection = state.view.selection;
246
+ const selectedAgentId = selection?.kind === "agent" ? selection.agentId : null;
247
+ const from = trafficFrom(entry, state);
248
+ const to = trafficTo(entry, state);
249
+ const outbound = from.agentId !== null && from.agentId === selectedAgentId;
250
+ const peer = outbound ? to : from;
251
+ return (_jsxs(_Fragment, { children: [_jsx("span", { fg: peerColor(peer.agentId), children: padCellsBeforeColumn(peer.label, AGENT_TRAFFIC_COLUMNS.peer) }), _jsx("span", { fg: rowColor, children: center(outbound ? "←" : "→", AGENT_TRAFFIC_COLUMNS.direction) })] }));
252
+ }
253
+ function FullMessageOverlay({ model, theme, body, copyFallback, message, colorEnabled = true, }) {
254
+ const state = useTuiModel(model);
255
+ const renderer = useRenderer();
256
+ const bodyView = body.read(message);
257
+ const [copyStatus, setCopyStatus] = useState(null);
258
+ useKeyboard((key) => {
259
+ if (key.name === "escape" || key.name === "o")
260
+ model.updateView((view) => ({ ...view, focus: view.overlayReturnFocus, overlay: { kind: "none" } }));
261
+ if (key.name === "y")
262
+ setCopyStatus(copyRaw(renderer, bodyView.rawCopy, copyFallback));
263
+ });
264
+ const bodyText = bodyView.display;
265
+ const width = FULL_MESSAGE_COLUMNS - 2;
266
+ const terminal = useTerminalDimensions();
267
+ // The border takes one cell on each side of the fitted overlay.
268
+ const barColumns = fitOverlayColumns(FULL_MESSAGE_COLUMNS, terminal.width) - 2;
269
+ const clock = formatClockSeconds(message.acceptedAt);
270
+ const from = agentHandle(state, message.fromAgentId);
271
+ const to = agentHandle(state, message.toAgentId);
272
+ const type = trafficType(message);
273
+ // The time is what the operator cannot get elsewhere in this overlay, so the endpoints go first.
274
+ const showRoute = Array.from(`${type} ${from} → ${to} · ${clock}`).length <= barColumns - PANE_BAR_INSET_COLUMNS;
275
+ const trailing = showRoute ? `${from} → ${to} · ${clock}` : clock;
276
+ return (_jsxs(Overlay, { theme: theme, width: FULL_MESSAGE_COLUMNS, height: FULL_MESSAGE_ROWS, children: [_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: fitBarLeading(type, barColumns, trailing) }), right: _jsxs("text", { fg: theme.structure.rulesAndLabels, children: [showRoute ? (_jsxs(_Fragment, { children: [_jsx("span", { fg: colorEnabled ? agentColor(message.fromAgentId, theme) : theme.structure.rulesAndLabels, children: from }), " → ", _jsx("span", { fg: colorEnabled ? agentColor(message.toAgentId, theme) : theme.structure.rulesAndLabels, children: to }), " · "] })) : null, clock] }) }), _jsx(Rule, { theme: theme }), _jsx("box", { flexDirection: "column", flexShrink: 0, paddingLeft: 1, paddingRight: 1, children: _jsx(Line, { children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: truncateCells(trafficSubject(message), width) }) }) }), _jsx(Rule, { theme: theme }), _jsx("box", { flexDirection: "column", flexGrow: 1, paddingLeft: 1, paddingRight: 1, overflow: "hidden", children: _jsx("scrollbox", { flexGrow: 1, focused: true, style: WRAPPED_TEXT_SCROLL_STYLE, children: _jsx("text", { selectable: true, wrapMode: "word", fg: theme.structure.emphasis, children: bodyText }) }) }), _jsx(Rule, { theme: theme }), _jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: copyHint(copyStatus) }), right: null })] }));
277
+ }
278
+ /**
279
+ * Actions the message context menu offers, in display order.
280
+ *
281
+ * An index that names none of them does nothing.
282
+ */
283
+ export const MESSAGE_CONTEXT_ACTIONS = ["open", "copy", "expire"];
284
+ const MESSAGE_CONTEXT_LABELS = {
285
+ open: "open full message",
286
+ copy: "copy raw",
287
+ expire: "expire message",
288
+ };
289
+ function MessageContextOverlay({ model, theme, body, copyFallback }) {
290
+ const state = useTuiModel(model);
291
+ const [copyStatus, setCopyStatus] = useState(null);
292
+ const renderer = useRenderer();
293
+ const overlay = state.view.overlay;
294
+ const message = overlay.kind === "context_menu" && overlay.target === "message"
295
+ ? findTuiMessage(state, overlay.messageId)
296
+ : undefined;
297
+ const close = () => {
298
+ model.updateView((view) => ({ ...view, focus: view.overlayReturnFocus, overlay: { kind: "none" } }));
299
+ };
300
+ const act = (action) => {
301
+ const current = model.state.view.overlay;
302
+ if (current.kind !== "context_menu" || current.target !== "message")
303
+ return;
304
+ const target = findTuiMessage(model.state, current.messageId);
305
+ if (!target)
306
+ return;
307
+ switch (action) {
308
+ case "open":
309
+ model.updateView((view) => ({ ...view, overlay: { kind: "full_message", messageId: target.messageId } }));
310
+ return;
311
+ case "copy":
312
+ setCopyStatus(copyRaw(renderer, body.read(target).rawCopy, copyFallback));
313
+ return;
314
+ case "expire":
315
+ if (!isTuiExpirable(target))
316
+ return;
317
+ model.updateView((view) => ({ ...view, overlay: { kind: "expire_confirm", messageId: target.messageId } }));
318
+ }
319
+ };
320
+ useKeyboard((key) => {
321
+ const current = model.state.view.overlay;
322
+ if (current.kind !== "context_menu" || current.target !== "message")
323
+ return;
324
+ if (key.name === "escape")
325
+ close();
326
+ if (key.name === "up" || key.name === "down")
327
+ model.updateView((view) => ({
328
+ ...view,
329
+ contextActionIndex: stepWrappingIndex(view.contextActionIndex, key.name === "up" ? "up" : "down", MESSAGE_CONTEXT_ACTIONS.length),
330
+ }));
331
+ if (key.name === "enter" || key.name === "return") {
332
+ const action = MESSAGE_CONTEXT_ACTIONS[model.state.view.contextActionIndex];
333
+ if (action)
334
+ act(action);
335
+ }
336
+ });
337
+ return (_jsxs(Overlay, { theme: theme, width: CONTEXT_OVERLAY_COLUMNS, children: [MESSAGE_CONTEXT_ACTIONS.map((action, index) => (_jsxs("box", { flexDirection: "column", children: [action === "expire" ? _jsx(Rule, { theme: theme }) : null, _jsx(ContextRow, { theme: theme, selected: index === state.view.contextActionIndex, label: MESSAGE_CONTEXT_LABELS[action], role: action !== "expire"
338
+ ? "ordinary"
339
+ : message !== undefined && isTuiExpirable(message)
340
+ ? "destructive"
341
+ : "unavailable", onActivate: () => {
342
+ act(action);
343
+ } })] }, action))), copyStatus === null ? null : (_jsx("text", { fg: copyStatus === "unavailable" ? theme.states.failed : theme.states.complete, children: copyStatus === "unavailable" ? " copy unavailable" : " copied" }))] }));
344
+ }
345
+ /** Keys the expire confirmation advertises in its footer. */
346
+ const EXPIRE_CONFIRM_FOOTER_KEYS = "⏎ expire esc cancel";
347
+ /**
348
+ * Ask again before expiring a message, and submit exactly once when the answer comes.
349
+ *
350
+ * The overlay closes in the same handler that submits, so the key that answered cannot answer twice
351
+ * while the command is still in flight.
352
+ * @param properties - Shared message collaborators and the delivery being expired.
353
+ * @returns - The confirmation overlay.
354
+ */
355
+ function ExpireConfirmOverlay({ model, theme, commands, message, }) {
356
+ const terminal = useTerminalDimensions();
357
+ // Border and padding take two cells on each side of the fitted overlay.
358
+ const idColumns = fitOverlayColumns(EXPIRE_OVERLAY_COLUMNS, terminal.width) - 4;
359
+ useKeyboard((key) => {
360
+ const overlay = model.state.view.overlay;
361
+ if (overlay.kind !== "expire_confirm")
362
+ return;
363
+ if (key.name !== "escape" && key.name !== "enter" && key.name !== "return")
364
+ return;
365
+ const submitting = key.name !== "escape";
366
+ model.updateView((view) => ({ ...view, focus: view.overlayReturnFocus, overlay: { kind: "none" } }));
367
+ if (submitting)
368
+ void submitExpiry(model, commands, overlay.messageId);
369
+ });
370
+ return (_jsxs(Overlay, { theme: theme, width: EXPIRE_OVERLAY_COLUMNS, children: [_jsx(PaneBar, { left: _jsx("text", { fg: theme.states.failed, children: "expire this message?" }), right: null }), _jsx(Rule, { theme: theme }), _jsx("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: splitCells(message.messageId, idColumns).map((line, index) => (_jsx(Line, { children: _jsx("text", { fg: theme.structure.bodyText, children: line }) }, index))) }), _jsx(Rule, { theme: theme }), _jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: EXPIRE_CONFIRM_FOOTER_KEYS }), right: null })] }));
371
+ }
372
+ /**
373
+ * Submit one expiry and report an outcome the operator would otherwise never see.
374
+ *
375
+ * A refusal arrives as an admitted result rather than a thrown error, so nothing on the surface
376
+ * changes when the broker declines: the diagnostics log is where that answer is put.
377
+ * @param model - Model owning the command port and the diagnostics log.
378
+ * @param commands - Correlation identities and the clock the command itself is stamped with.
379
+ * @param messageId - Delivery the operator asked to expire.
380
+ */
381
+ async function submitExpiry(model, commands, messageId) {
382
+ const command = { type: "expire_message", messageId };
383
+ let result;
384
+ try {
385
+ result = await model.execute({
386
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
387
+ commandId: commands.commandId(),
388
+ issuedAt: commands.issuedAt(),
389
+ command,
390
+ });
391
+ }
392
+ catch {
393
+ const outcome = describeCommandOutcome(model.state, command, null);
394
+ model.reportDiagnostic(outcome.severity, "command", outcome.summary);
395
+ return;
396
+ }
397
+ const outcome = describeCommandOutcome(model.state, command, result);
398
+ model.reportDiagnostic(outcome.severity, "command", outcome.summary);
399
+ try {
400
+ commands.observe(result);
401
+ }
402
+ catch {
403
+ model.reportDiagnostic("error", "command", "expire answered, but observing its result failed");
404
+ }
405
+ }
406
+ /**
407
+ * Render one context-menu row.
408
+ *
409
+ * An unavailable row keeps its dim colour while highlighted, so the highlight cannot make it read as
410
+ * an action the menu would perform.
411
+ * @param properties - Palette, highlight, label, what the row does, and its activation.
412
+ * @returns - One menu row.
413
+ */
414
+ function ContextRow({ theme, selected, label, role, onActivate, }) {
415
+ const color = role === "unavailable"
416
+ ? theme.structure.rulesAndLabels
417
+ : selected
418
+ ? theme.structure.selection.text
419
+ : role === "destructive"
420
+ ? theme.states.failed
421
+ : theme.structure.emphasis;
422
+ return (_jsx("box", { height: 1, ...(selected ? { backgroundColor: theme.structure.selection.background } : {}), onMouseDown: onActivate, children: _jsx("text", { fg: color, children: ` ${label}` }) }));
423
+ }
424
+ /**
425
+ * Guard OSC52 with OpenTUI's exact capability probe before emitting a clipboard sequence.
426
+ * @param renderer - Renderer whose capability probe and OSC52 write are used.
427
+ * @param rawCopy - Exact text to place on the clipboard.
428
+ * @param fallback - Native copy used when the terminal cannot take OSC52 or the write fails.
429
+ * @returns How the copy was performed, or that it could not be.
430
+ */
431
+ export function copyRaw(renderer, rawCopy, fallback) {
432
+ return renderer.isOsc52Supported() && renderer.copyToClipboardOSC52(rawCopy)
433
+ ? "osc52"
434
+ : fallback.copyUnavailable(rawCopy);
435
+ }
436
+ const FULL_MESSAGE_COLUMNS = 96;
437
+ const FULL_MESSAGE_ROWS = 28;
438
+ const CONTEXT_OVERLAY_COLUMNS = 26;
439
+ const EXPIRE_OVERLAY_COLUMNS = 46;
440
+ function copyHint(status) {
441
+ if (status === null)
442
+ return "y copy raw esc close";
443
+ return status === "unavailable" ? "copy unavailable esc close" : "copied esc close";
444
+ }
445
+ /**
446
+ * Close a message overlay once the traffic log selects a row other than the message it names.
447
+ *
448
+ * This is the log's rule on its own; it holds whatever the shell does with selection while an overlay
449
+ * is open.
450
+ * @param overlay - Overlay currently open.
451
+ * @param entry - Row the operator selected.
452
+ * @returns - The same overlay, or a closed one where it names another message.
453
+ */
454
+ function closeOtherMessageOverlay(overlay, entry) {
455
+ const named = overlay.kind === "full_message" ||
456
+ overlay.kind === "expire_confirm" ||
457
+ (overlay.kind === "context_menu" && overlay.target === "message")
458
+ ? overlay.messageId
459
+ : null;
460
+ if (named === null)
461
+ return overlay;
462
+ return entry.kind === "accepted_delivery" && entry.messageId === named ? overlay : { kind: "none" };
463
+ }
464
+ /**
465
+ * Break text into rows of a fixed width wherever the width falls, for identities that hold no spaces
466
+ * to wrap at and must be shown whole.
467
+ * @param value - Text to lay out.
468
+ * @param width - Character cells each row holds.
469
+ * @returns - Rows in order, the last one possibly shorter.
470
+ */
471
+ function splitCells(value, width) {
472
+ if (width <= 0)
473
+ return [];
474
+ const cells = Array.from(value);
475
+ const rows = [];
476
+ for (let start = 0; start < cells.length; start += width)
477
+ rows.push(cells.slice(start, start + width).join(""));
478
+ return rows;
479
+ }
480
+ function center(value, width) {
481
+ const left = Math.max(0, Math.floor((width - value.length) / 2));
482
+ return `${" ".repeat(left)}${value}`.padEnd(width);
483
+ }
484
+ function trafficGlyph(entry, theme) {
485
+ switch (entry.kind) {
486
+ case "accepted_delivery":
487
+ return { glyph: deliveryGlyph(entry.state), color: deliveryColor(entry.state, theme) };
488
+ case "ping_audit":
489
+ return { glyph: "⍾", color: theme.structure.rulesAndLabels };
490
+ }
491
+ }
492
+ function trafficFrom(entry, state) {
493
+ switch (entry.kind) {
494
+ case "accepted_delivery":
495
+ return { label: agentHandle(state, entry.fromAgentId), agentId: entry.fromAgentId };
496
+ case "ping_audit":
497
+ return { label: "broker", agentId: null };
498
+ }
499
+ }
500
+ function trafficTo(entry, state) {
501
+ switch (entry.kind) {
502
+ case "accepted_delivery":
503
+ return { label: agentHandle(state, entry.toAgentId), agentId: entry.toAgentId };
504
+ case "ping_audit":
505
+ return { label: agentHandle(state, entry.targetAgentId), agentId: entry.targetAgentId };
506
+ }
507
+ }
508
+ function lifecycleColor(entry, last, theme) {
509
+ if (entry.state === null)
510
+ return theme.structure.rulesAndLabels;
511
+ return last ? deliveryColor(entry.state, theme) : theme.structure.bodyText;
512
+ }
513
+ function Centered({ theme, text }) {
514
+ return (_jsx("box", { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: text }) }));
515
+ }