@fitchmultz/pi-subagents 0.50.0 → 0.51.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 (169) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +36 -5
  3. package/dist/agent/contracts.js +6 -24
  4. package/dist/agent/host.js +3 -16
  5. package/dist/agent/index.js +207 -173
  6. package/dist/agent/journal-tail.js +115 -156
  7. package/dist/agent/launch.js +20 -8
  8. package/dist/agent/legacy.js +65 -23
  9. package/dist/agent/notifications.js +138 -47
  10. package/dist/agent/observation-state.js +246 -0
  11. package/dist/agent/observation-view.js +258 -0
  12. package/dist/agent/observation.js +346 -0
  13. package/dist/agent/observe-card.js +243 -0
  14. package/dist/agent/observe-chrome.js +85 -0
  15. package/dist/agent/observe-client.js +285 -0
  16. package/dist/agent/observe-layout.js +65 -0
  17. package/dist/agent/observe-reading.js +84 -0
  18. package/dist/agent/observe-screen.js +400 -96
  19. package/dist/agent/observe-transcript.js +382 -0
  20. package/dist/agent/observe.js +250 -43
  21. package/dist/agent/process.js +198 -25
  22. package/dist/agent/receipts.js +92 -20
  23. package/dist/agent/runner.js +125 -20
  24. package/dist/agent/shell.js +2 -1
  25. package/dist/agent/storage.js +29 -12
  26. package/dist/agent/tool-schemas.js +20 -0
  27. package/dist/agent/typebox.js +10 -5
  28. package/dist/agent/usage.js +0 -115
  29. package/dist/agent/view.js +46 -16
  30. package/dist/agent/wake.js +99 -0
  31. package/dist/build-info.js +1 -1
  32. package/dist/intercom-client.d.ts +26 -0
  33. package/dist/intercom-client.js +45 -0
  34. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  35. package/dist/pi-intercom/bridge-session.js +2 -1
  36. package/dist/pi-intercom/broker/broker.js +27 -21
  37. package/dist/pi-intercom/broker/client-last.d.ts +13 -0
  38. package/dist/pi-intercom/broker/client-last.js +17 -5
  39. package/dist/pi-intercom/broker/client.d.ts +60 -0
  40. package/dist/pi-intercom/broker/client.js +47 -16
  41. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  42. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  43. package/dist/pi-intercom/broker/framing.js +10 -7
  44. package/dist/pi-intercom/broker/guard.js +23 -0
  45. package/dist/pi-intercom/broker/last.d.ts +26 -0
  46. package/dist/pi-intercom/broker/last.js +57 -32
  47. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  48. package/dist/pi-intercom/broker/paths.js +35 -12
  49. package/dist/pi-intercom/broker/pid.js +5 -1
  50. package/dist/pi-intercom/broker/server-connection.js +19 -0
  51. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  52. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  53. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  54. package/dist/pi-intercom/connection.js +10 -2
  55. package/dist/pi-intercom/identity.js +6 -5
  56. package/dist/pi-intercom/inbound-delivery.js +16 -60
  57. package/dist/pi-intercom/inbound-journal.js +83 -6
  58. package/dist/pi-intercom/inbound-record.js +18 -1
  59. package/dist/pi-intercom/inbound.js +12 -38
  60. package/dist/pi-intercom/index.js +3 -0
  61. package/dist/pi-intercom/last-message.js +4 -16
  62. package/dist/pi-intercom/lifecycle.js +88 -3
  63. package/dist/pi-intercom/outbound.js +5 -15
  64. package/dist/pi-intercom/reply-tracker.js +7 -6
  65. package/dist/pi-intercom/reply-wait.js +2 -2
  66. package/dist/pi-intercom/session-targets.d.ts +24 -0
  67. package/dist/pi-intercom/tool-registration.js +4 -1
  68. package/dist/pi-intercom/tool-results.js +5 -7
  69. package/dist/pi-intercom/tool-schemas.js +12 -26
  70. package/dist/pi-intercom/types.d.ts +212 -0
  71. package/dist/pi-intercom/types.js +54 -3
  72. package/dist/pi-intercom/validation.d.ts +12 -0
  73. package/dist/pi-intercom/validation.js +3 -0
  74. package/dist/shared/bounded-json.js +151 -0
  75. package/dist/shared/compact-cards.js +66 -0
  76. package/dist/shared/journal-frames.js +3 -6
  77. package/dist/shared/json-projection.js +267 -129
  78. package/dist/shared/native-journal.js +123 -56
  79. package/dist/shared/post-exit-stdio-guard.js +120 -7
  80. package/dist/shared/tui-action.js +29 -0
  81. package/dist/shared/unknown.d.ts +9 -0
  82. package/docs/code-quality.md +2 -0
  83. package/docs/development.md +40 -1
  84. package/docs/intercom-bridge.md +9 -4
  85. package/docs/intercom.md +226 -6
  86. package/docs/performance.md +87 -11
  87. package/docs/reference.md +157 -20
  88. package/docs/retired-history-maintenance.md +252 -0
  89. package/package.json +28 -15
  90. package/scripts/archive-retired-history.mjs +464 -0
  91. package/scripts/build.mjs +48 -28
  92. package/scripts/measure-size.mjs +1 -1
  93. package/scripts/native-package-smoke.mjs +153 -12
  94. package/scripts/package-smoke.mjs +80 -11
  95. package/scripts/prepare.mjs +2 -2
  96. package/scripts/real-pi-live.mjs +377 -0
  97. package/scripts/real-pi-processes.mjs +6 -1
  98. package/scripts/real-pi-smoke.mjs +6 -282
  99. package/scripts/run-tests.mjs +4 -2
  100. package/scripts/typescript.mjs +35 -0
  101. package/skills/pi-intercom/SKILL.md +2 -1
  102. package/skills/pi-subagents/SKILL.md +23 -5
  103. package/src/agent/contracts.ts +8 -34
  104. package/src/agent/host.ts +19 -20
  105. package/src/agent/index.ts +224 -190
  106. package/src/agent/journal-tail.ts +141 -153
  107. package/src/agent/launch.ts +33 -9
  108. package/src/agent/legacy.ts +85 -31
  109. package/src/agent/notifications.ts +192 -67
  110. package/src/agent/observation-state.ts +277 -0
  111. package/src/agent/observation-view.ts +267 -0
  112. package/src/agent/observation.ts +405 -0
  113. package/src/agent/observe-card.ts +304 -0
  114. package/src/agent/observe-chrome.ts +170 -0
  115. package/src/agent/observe-client.ts +318 -0
  116. package/src/agent/observe-layout.ts +79 -0
  117. package/src/agent/observe-reading.ts +97 -0
  118. package/src/agent/observe-screen.ts +443 -129
  119. package/src/agent/observe-transcript.ts +434 -0
  120. package/src/agent/observe.ts +304 -71
  121. package/src/agent/process.ts +240 -34
  122. package/src/agent/receipts.ts +114 -24
  123. package/src/agent/runner.ts +125 -27
  124. package/src/agent/shell.ts +2 -1
  125. package/src/agent/storage.ts +27 -11
  126. package/src/agent/tool-schemas.ts +33 -0
  127. package/src/agent/typebox.ts +12 -7
  128. package/src/agent/usage.ts +0 -130
  129. package/src/agent/view.ts +70 -20
  130. package/src/agent/wake.ts +107 -0
  131. package/src/intercom-client.ts +108 -0
  132. package/src/pi-intercom/bridge-session.ts +2 -2
  133. package/src/pi-intercom/broker/broker.ts +27 -23
  134. package/src/pi-intercom/broker/client-last.ts +25 -12
  135. package/src/pi-intercom/broker/client.ts +55 -33
  136. package/src/pi-intercom/broker/framing.ts +11 -8
  137. package/src/pi-intercom/broker/guard.ts +33 -0
  138. package/src/pi-intercom/broker/last.ts +86 -33
  139. package/src/pi-intercom/broker/paths.ts +39 -12
  140. package/src/pi-intercom/broker/pid.ts +6 -1
  141. package/src/pi-intercom/broker/server-connection.ts +25 -0
  142. package/src/pi-intercom/connection.ts +21 -10
  143. package/src/pi-intercom/identity.ts +8 -5
  144. package/src/pi-intercom/inbound-delivery.ts +20 -66
  145. package/src/pi-intercom/inbound-journal.ts +103 -7
  146. package/src/pi-intercom/inbound-record.ts +21 -4
  147. package/src/pi-intercom/inbound.ts +18 -49
  148. package/src/pi-intercom/index.ts +3 -0
  149. package/src/pi-intercom/last-message.ts +10 -19
  150. package/src/pi-intercom/lifecycle.ts +117 -14
  151. package/src/pi-intercom/outbound.ts +7 -16
  152. package/src/pi-intercom/reply-tracker.ts +8 -6
  153. package/src/pi-intercom/reply-wait.ts +3 -2
  154. package/src/pi-intercom/runtime-types.ts +0 -1
  155. package/src/pi-intercom/tool-registration.ts +4 -1
  156. package/src/pi-intercom/tool-results.ts +10 -9
  157. package/src/pi-intercom/tool-schemas.ts +12 -44
  158. package/src/pi-intercom/types.ts +115 -4
  159. package/src/pi-intercom/validation.ts +4 -0
  160. package/src/shared/bounded-json.ts +162 -0
  161. package/src/shared/compact-cards.ts +118 -0
  162. package/src/shared/journal-frames.ts +3 -19
  163. package/src/shared/json-projection.ts +267 -147
  164. package/src/shared/native-journal.ts +157 -75
  165. package/src/shared/post-exit-stdio-guard.ts +165 -7
  166. package/src/shared/tui-action.ts +39 -0
  167. package/tsconfig.client.json +8 -0
  168. package/tsconfig.json +1 -0
  169. package/src/shared/stream-json.d.ts +0 -10
@@ -0,0 +1,243 @@
1
+ import { isRecord } from "../shared/unknown.js";
2
+ import { AssistantMessageComponent, Container, Text, ToolExecutionComponent, createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createPowerShellToolDefinition, createReadToolDefinition, createWriteToolDefinition, } from "./observe-layout.js";
3
+ const renderers = Object.entries({
4
+ bash: createBashToolDefinition,
5
+ edit: createEditToolDefinition,
6
+ find: createFindToolDefinition,
7
+ grep: createGrepToolDefinition,
8
+ ls: createLsToolDefinition,
9
+ powershell: createPowerShellToolDefinition,
10
+ read: createReadToolDefinition,
11
+ write: createWriteToolDefinition,
12
+ });
13
+ function nativeTool(name, id, args, options) {
14
+ return new ToolExecutionComponent(name, id, args, { showImages: false, outputPad: 0 }, renderers.find(([key]) => key === name)?.[1](options.cwd) ?? {}, options.tui, options.cwd);
15
+ }
16
+ export function blocks(value) {
17
+ if (typeof value === "string") {
18
+ return [{ type: "text", text: value }];
19
+ }
20
+ if (!Array.isArray(value)) {
21
+ return [];
22
+ }
23
+ return value.flatMap((part) => {
24
+ if (!isRecord(part)) {
25
+ return [];
26
+ }
27
+ if (part.type === "text" && typeof part.text === "string") {
28
+ return [{ type: "text", text: part.text }];
29
+ }
30
+ if (part.type === "thinking" && typeof part.thinking === "string") {
31
+ return [{ type: "thinking", text: part.thinking }];
32
+ }
33
+ if (part.type === "toolCall" && typeof part.id === "string" && typeof part.name === "string") {
34
+ return [{ type: "toolCall", id: part.id, name: part.name, args: part.arguments }];
35
+ }
36
+ return [
37
+ {
38
+ type: "text",
39
+ text: `[${typeof part.type === "string" ? part.type : "non-text"} attachment]`,
40
+ },
41
+ ];
42
+ });
43
+ }
44
+ export function projected(value) {
45
+ if (!isRecord(value)) {
46
+ return undefined;
47
+ }
48
+ return "json" in value ? value.json : undefined;
49
+ }
50
+ export function assistant(content, streaming) {
51
+ // Rendering only. Accounting and completion still use native journals and receipts.
52
+ return {
53
+ role: "assistant",
54
+ api: "observer",
55
+ provider: "observer",
56
+ model: "",
57
+ timestamp: 0,
58
+ stopReason: streaming ? "pending" : "stop",
59
+ usage: {
60
+ input: 0,
61
+ output: 0,
62
+ cacheRead: 0,
63
+ cacheWrite: 0,
64
+ totalTokens: 0,
65
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
66
+ },
67
+ content: content.flatMap((block) => {
68
+ if (block.type === "thinking") {
69
+ return [{ type: "thinking", thinking: block.text ?? "" }];
70
+ }
71
+ return block.type === "text" ? [{ type: "text", text: block.text ?? "" }] : [];
72
+ }),
73
+ };
74
+ }
75
+ export class Card {
76
+ id;
77
+ content = new Container();
78
+ footer = new Container();
79
+ assistant;
80
+ status;
81
+ blocks;
82
+ ended;
83
+ value;
84
+ tool;
85
+ selected = false;
86
+ expanded = false;
87
+ ready = false;
88
+ page = 0;
89
+ pages = 1;
90
+ cache;
91
+ arguments;
92
+ phase = "";
93
+ constructor(id) {
94
+ this.id = id;
95
+ }
96
+ publish(page, args, options) {
97
+ this.content.clear();
98
+ this.footer.clear();
99
+ this.ready = true;
100
+ this.page = page.page;
101
+ this.pages = page.pages;
102
+ const { message } = page;
103
+ const content = blocks(message.content);
104
+ if (message.role === "assistant") {
105
+ this.content.addChild(new AssistantMessageComponent(assistant(content, false), options.hideThinking));
106
+ if (typeof message.errorMessage === "string") {
107
+ this.content.addChild(new Text(message.errorMessage, 0, 0));
108
+ }
109
+ }
110
+ else if (message.role === "toolResult") {
111
+ this.publishTool(message, args, options);
112
+ }
113
+ else {
114
+ this.content.addChild(new Text(`${typeof message.role === "string" ? message.role : "message"}\n${content.map((part) => part.text ?? "").join("\n")}`, 0, 0));
115
+ }
116
+ }
117
+ publishTool(message, args, options) {
118
+ this.tool ??= nativeTool(typeof message.toolName === "string" ? message.toolName : "tool", typeof message.toolCallId === "string" ? message.toolCallId : this.id, args ?? {}, options);
119
+ this.tool.updateResult({
120
+ content: blocks(message.content)
121
+ .filter((part) => part.type === "text")
122
+ .map((part) => ({ type: "text", text: part.text })),
123
+ isError: message.isError === true,
124
+ });
125
+ this.tool.setExpanded(this.expanded);
126
+ this.content.addChild(this.tool);
127
+ this.footer.addChild(new Text("Arguments are a bounded preview. Exact call in Details journal.", 0, 0));
128
+ }
129
+ configureTool(value, options) {
130
+ if (!this.tool) {
131
+ this.tool = nativeTool(value.name, value.id, projected(value.args) ?? {}, options);
132
+ this.tool.setExpanded(this.expanded);
133
+ this.content.addChild(this.tool);
134
+ this.status = new Text("", 0, 0);
135
+ this.content.addChild(this.status);
136
+ }
137
+ if (this.arguments !== value.args) {
138
+ this.tool.updateArgs(projected(value.args) ?? {});
139
+ this.arguments = value.args;
140
+ this.invalidate();
141
+ }
142
+ }
143
+ updateTool(value, options, preparing) {
144
+ this.configureTool(value, options);
145
+ const ended = value.end !== undefined;
146
+ const source = value.end ?? { result: value.partial, isError: false };
147
+ let phase = preparing ? "Preparing tool call" : "Running";
148
+ if (ended) {
149
+ phase = "Awaiting journal publication";
150
+ }
151
+ if (this.value !== source.result || this.phase !== phase) {
152
+ if (source.result !== undefined) {
153
+ this.preview(source.result, source.isError, !ended);
154
+ }
155
+ this.status?.setText(phase);
156
+ this.phase = phase;
157
+ this.value = source.result;
158
+ this.ended = ended;
159
+ this.invalidate();
160
+ }
161
+ }
162
+ preview(value, isError, partial) {
163
+ const result = projected(value) ?? {
164
+ content: [
165
+ { type: "text", text: "Live preview unavailable; full result appears after publication." },
166
+ ],
167
+ };
168
+ if (isRecord(result)) {
169
+ this.tool?.updateResult({
170
+ content: blocks(result.content)
171
+ .filter((part) => part.type === "text")
172
+ .map((part) => ({ type: "text", text: part.text })),
173
+ isError,
174
+ }, partial);
175
+ }
176
+ }
177
+ stream(message, hideThinking) {
178
+ if (!this.assistant) {
179
+ this.assistant = new AssistantMessageComponent(undefined, hideThinking);
180
+ this.status = new Text("", 0, 0);
181
+ this.content.addChild(this.assistant);
182
+ this.content.addChild(this.status);
183
+ }
184
+ if (this.matches(message.blocks, message.ended)) {
185
+ return;
186
+ }
187
+ this.assistant.updateContent(assistant(message.blocks.filter(Boolean), !message.ended), !message.ended);
188
+ this.status?.setText(message.ended ? "Awaiting journal publication" : "In progress");
189
+ this.blocks = message.blocks.slice();
190
+ this.ended = message.ended;
191
+ this.invalidate();
192
+ }
193
+ invalidate() {
194
+ this.cache = undefined;
195
+ this.content.invalidate();
196
+ this.footer.invalidate();
197
+ }
198
+ render(width) {
199
+ if (this.cache?.width !== width) {
200
+ const lines = [...this.content.render(width), ...this.footer.render(width)];
201
+ if (this.selected && this.tool && lines.length > 0) {
202
+ lines[0] = "> Selected tool".slice(0, width);
203
+ }
204
+ this.cache = { width, lines };
205
+ }
206
+ return this.cache.lines;
207
+ }
208
+ matches(content, ended) {
209
+ return (this.ended === ended &&
210
+ this.blocks?.length === content.length &&
211
+ content.every((block, index) => this.blocks?.[index] === block));
212
+ }
213
+ toggle() {
214
+ if (this.tool) {
215
+ this.expanded = !this.expanded;
216
+ this.tool.setExpanded(this.expanded);
217
+ this.cache = undefined;
218
+ }
219
+ }
220
+ handleMouse(event) {
221
+ const body = this.content.render(event.width).length;
222
+ if (event.y >= body) {
223
+ return this.footer.handleMouse({
224
+ ...event,
225
+ y: event.y - body,
226
+ height: Math.max(0, event.height - body),
227
+ });
228
+ }
229
+ if (this.tool &&
230
+ event.button === "left" &&
231
+ (event.type === "press" || event.type === "click")) {
232
+ if (event.type === "click") {
233
+ this.toggle();
234
+ }
235
+ return { handled: true, render: event.type === "click" };
236
+ }
237
+ const result = this.content.handleMouse(event);
238
+ if (result?.handled) {
239
+ this.cache = undefined;
240
+ }
241
+ return result;
242
+ }
243
+ }
@@ -0,0 +1,85 @@
1
+ import { actionHints, actionText } from "../shared/tui-action.js";
2
+ import { Container, HStack, rawKeyHint, ScrollView, Text, TruncatedText, } from "./observe-layout.js";
3
+ import { plainText } from "./journal-tail.js";
4
+ export class Details extends ScrollView {
5
+ constructor(launch) {
6
+ super(new Text(`${launch.context === "fork" ? "Fork context: includes inherited parent activity.\n" : ""}${launch.id}\nAssignment preview: ${plainText(launch.prompt, 160)}\n${launch.sessionFile}`, 0, 0), { primary: false, scrollbar: "always" });
7
+ }
8
+ }
9
+ export class ChildList extends ScrollView {
10
+ list;
11
+ open;
12
+ constructor(open) {
13
+ const list = new Container();
14
+ super(list, { primary: false, scrollbar: "always" });
15
+ this.list = list;
16
+ this.open = open;
17
+ }
18
+ update(rows, selected, checking) {
19
+ this.list.clear();
20
+ if (rows.length === 0) {
21
+ this.list.addChild(new Text(checking
22
+ ? "Checking saved child runs…"
23
+ : "No child runs to show. Completed runs are hidden.", 0, 0));
24
+ }
25
+ for (const row of rows) {
26
+ this.list.addChild(actionText(`${row.id === selected ? ">" : " "} ${row.status} · ${row.name}`, () => this.open(row.id)));
27
+ }
28
+ }
29
+ reveal(index) {
30
+ this.scrollTo(Math.max(0, index - Math.floor(this.viewportHeight / 2)));
31
+ }
32
+ }
33
+ /** Header activity label: a live connection wins over the last known row status. */
34
+ export function activityLabel(options) {
35
+ const { row, client } = options;
36
+ if (row?.live !== true) {
37
+ return row?.status ?? "checking";
38
+ }
39
+ if (client?.connected === true) {
40
+ return client.activity;
41
+ }
42
+ if (client) {
43
+ return "Reconnecting";
44
+ }
45
+ return options.connecting ? "Connecting" : (row.activity ?? row.status);
46
+ }
47
+ /** Footer key hints press the same actions as the keyboard table. */
48
+ export function appendHints(target, options) {
49
+ const hints = options.actions
50
+ .filter(([, label]) => label.length > 0)
51
+ .map(([key, label, action]) => ({
52
+ text: rawKeyHint(options.keys.getKeys(key)[0] ?? "", label),
53
+ action: () => options.perform(action),
54
+ }));
55
+ target.addChild(actionHints(hints), { basis: 1 });
56
+ }
57
+ /** Conversation body: persistent child strip beside the transcript, plus the follow footer. */
58
+ export function appendConversation(target, options) {
59
+ target.addChild(new HStack([
60
+ {
61
+ component: options.list,
62
+ basis: 27,
63
+ visible: (viewport) => viewport.width >= 100,
64
+ },
65
+ { component: options.reading.scroll, basis: 0, grow: 1, minSize: 1 },
66
+ ], { gap: 1 }), { basis: 0, grow: 1, minSize: 1 });
67
+ const { following, pending } = options.reading;
68
+ let follow = pending
69
+ ? "New activity below · End to follow live"
70
+ : "Reading earlier · End to follow live";
71
+ if (following) {
72
+ follow = "Following live";
73
+ }
74
+ target.addChild(actionText(follow, options.follow), { basis: 1 });
75
+ }
76
+ export function appendHeader(target, options) {
77
+ const { launch } = options;
78
+ target.addChild(actionText(`${plainText(launch.name ?? "Child", 32)} · ${options.activity} · Details`, options.toggle), { basis: 1 });
79
+ target.addChild(new TruncatedText(`${options.model} · Thinking ${options.thinking}`, 0, 0), {
80
+ basis: 1,
81
+ });
82
+ if (options.details) {
83
+ target.addChild(options.details, { maxSize: 5 });
84
+ }
85
+ }
@@ -0,0 +1,285 @@
1
+ import * as fs from "node:fs";
2
+ import * as net from "node:net";
3
+ import * as path from "node:path";
4
+ import { agentObservationSocketPath } from "../pi-intercom/broker/paths.js";
5
+ import { createMessageReader, writeFrame } from "../pi-intercom/broker/framing.js";
6
+ import { Assert, Type } from "./typebox.js";
7
+ const activitySchema = Type.Object({
8
+ phase: Type.String(),
9
+ tools: Type.Array(Type.String()),
10
+ model: Type.Optional(Type.String()),
11
+ thinking: Type.Optional(Type.String()),
12
+ });
13
+ const nullableString = Type.Union([Type.String(), Type.Null()]);
14
+ const settlementSchema = Type.Tuple([
15
+ Type.Number(),
16
+ Type.String(),
17
+ Type.Union([Type.Number(), Type.Null()]),
18
+ nullableString,
19
+ ]);
20
+ const blockSchema = Type.Union([
21
+ Type.Tuple([
22
+ Type.Integer({ minimum: 0, maximum: 255 }),
23
+ Type.Union([Type.Literal("text"), Type.Literal("thinking")]),
24
+ Type.Integer({ minimum: 0 }),
25
+ Type.String(),
26
+ ]),
27
+ Type.Tuple([
28
+ Type.Integer({ minimum: 0, maximum: 255 }),
29
+ Type.Literal("toolCall"),
30
+ Type.String(),
31
+ Type.String(),
32
+ Type.Unknown(),
33
+ ]),
34
+ ]);
35
+ const messageSchema = Type.Object({
36
+ key: Type.Number(),
37
+ reset: Type.Boolean(),
38
+ ended: Type.Boolean(),
39
+ count: Type.Number(),
40
+ blocks: Type.Array(blockSchema),
41
+ });
42
+ const toolSchema = Type.Object({
43
+ id: Type.String(),
44
+ name: Type.Optional(Type.String()),
45
+ args: Type.Optional(Type.Unknown()),
46
+ partial: Type.Optional(Type.Object({ rev: Type.Number(), value: Type.Unknown() })),
47
+ end: Type.Optional(Type.Object({ result: Type.Unknown(), isError: Type.Boolean() })),
48
+ });
49
+ const frameSchema = Type.Union([
50
+ Type.Object({
51
+ t: Type.Literal("snapshot"),
52
+ v: Type.Literal(1),
53
+ generation: Type.String(),
54
+ rootId: Type.String(),
55
+ sessionId: Type.String(),
56
+ seq: Type.Number(),
57
+ leafId: nullableString,
58
+ activity: activitySchema,
59
+ }),
60
+ Type.Object({ t: Type.Literal("activity"), activity: activitySchema }),
61
+ Type.Object({
62
+ t: Type.Literal("delta"),
63
+ settled: Type.Array(settlementSchema),
64
+ tools: Type.Array(toolSchema),
65
+ message: Type.Optional(messageSchema),
66
+ }),
67
+ ]);
68
+ export class ObservationChannel {
69
+ current;
70
+ pending;
71
+ changed;
72
+ constructor(changed) {
73
+ this.changed = changed;
74
+ }
75
+ get connecting() {
76
+ return this.pending !== undefined;
77
+ }
78
+ connect(identity) {
79
+ if (this.pending || this.current?.connected === true) {
80
+ return;
81
+ }
82
+ const client = new ObservationClient(identity, "detail", () => {
83
+ if (this.pending !== client && this.current !== client) {
84
+ return;
85
+ }
86
+ if (this.pending === client && client.connected) {
87
+ this.current = client;
88
+ this.pending = undefined;
89
+ }
90
+ if (this.pending === client && client.closed) {
91
+ this.pending = undefined;
92
+ }
93
+ this.changed();
94
+ });
95
+ this.pending = client;
96
+ }
97
+ pause() {
98
+ this.current?.close();
99
+ this.pending?.close();
100
+ this.pending = undefined;
101
+ }
102
+ clear() {
103
+ this.pause();
104
+ this.current = undefined;
105
+ }
106
+ }
107
+ /** A selected read-only peer. Destruction closes its own socket, never native execution. */
108
+ export class ObservationClient {
109
+ activity = "Connecting";
110
+ model = "";
111
+ thinking = "";
112
+ connected = false;
113
+ closed = false;
114
+ revision = 0;
115
+ leaf;
116
+ message;
117
+ tools = new Map();
118
+ handovers = [];
119
+ seq = -1;
120
+ chars = 0;
121
+ socket;
122
+ identity;
123
+ changed;
124
+ constructor(identity, level, changed) {
125
+ this.identity = identity;
126
+ this.changed = changed;
127
+ const file = agentObservationSocketPath(identity.token);
128
+ const directory = fs.lstatSync(path.dirname(file));
129
+ const endpoint = fs.lstatSync(file);
130
+ const uid = process.getuid?.();
131
+ if (!directory.isDirectory() ||
132
+ directory.uid !== uid ||
133
+ (directory.mode & 0o777) !== 0o700 ||
134
+ !endpoint.isSocket() ||
135
+ endpoint.uid !== uid ||
136
+ (endpoint.mode & 0o777) !== 0o600) {
137
+ throw new Error("Unsafe child observation endpoint.");
138
+ }
139
+ this.socket = net.createConnection(file);
140
+ this.socket.on("connect", () => {
141
+ writeFrame(this.socket, JSON.stringify({ t: "hello", v: 1, ...identity, level }));
142
+ });
143
+ this.socket.on("data", createMessageReader((frame) => {
144
+ try {
145
+ this.receive(frame);
146
+ }
147
+ catch {
148
+ this.socket.destroy();
149
+ }
150
+ }, () => {
151
+ this.socket.destroy();
152
+ }, 256 * 1024));
153
+ this.socket.on("error", () => {
154
+ this.socket.destroy();
155
+ });
156
+ this.socket.on("close", () => {
157
+ this.closed = true;
158
+ this.connected = false;
159
+ this.changed();
160
+ });
161
+ }
162
+ receive(value) {
163
+ Assert(frameSchema, value);
164
+ if (value.t === "snapshot") {
165
+ this.baseline(value);
166
+ }
167
+ else if (this.seq < 0) {
168
+ throw new Error("Observation before baseline.");
169
+ }
170
+ else if (value.t === "delta") {
171
+ this.delta(value);
172
+ }
173
+ if (value.t !== "delta") {
174
+ this.activity = [value.activity.phase, ...value.activity.tools].join(" · ");
175
+ this.model = value.activity.model ?? this.model;
176
+ this.thinking = value.activity.thinking ?? this.thinking;
177
+ }
178
+ this.revision++;
179
+ this.changed();
180
+ }
181
+ baseline(value) {
182
+ const { token, rootId, sessionId } = this.identity;
183
+ if (value.generation !== token || value.rootId !== rootId || value.sessionId !== sessionId) {
184
+ throw new Error("Foreign child observation.");
185
+ }
186
+ this.leaf = value.leafId;
187
+ this.seq = value.seq;
188
+ this.connected = true;
189
+ this.message = undefined;
190
+ this.tools.clear();
191
+ this.handovers.length = 0;
192
+ this.chars = 0;
193
+ }
194
+ delta(value) {
195
+ for (const item of value.settled) {
196
+ if (item[0] !== this.seq + 1) {
197
+ throw new Error("Invalid handover sequence.");
198
+ }
199
+ this.seq = item[0];
200
+ this.leaf = item[1];
201
+ this.handovers.push(item);
202
+ }
203
+ if (this.handovers.length > 256) {
204
+ throw new Error("Journal handover stalled.");
205
+ }
206
+ for (const item of value.tools) {
207
+ const tool = this.tools.get(item.id) ?? {
208
+ id: item.id,
209
+ name: item.name ?? "tool",
210
+ args: item.args,
211
+ };
212
+ if (item.partial) {
213
+ tool.partial = item.partial.value;
214
+ }
215
+ if (item.end) {
216
+ tool.end = item.end;
217
+ }
218
+ this.tools.set(item.id, tool);
219
+ }
220
+ if (this.tools.size > 256) {
221
+ throw new Error("Too many live tools.");
222
+ }
223
+ this.assistant(value.message);
224
+ }
225
+ assistant(value) {
226
+ if (!value) {
227
+ return;
228
+ }
229
+ if (value.reset) {
230
+ this.message = { key: value.key, ended: false, blocks: [] };
231
+ this.chars = 0;
232
+ }
233
+ const message = this.message;
234
+ if (message?.key !== value.key) {
235
+ throw new Error("Assistant changed without reset.");
236
+ }
237
+ message.ended = value.ended;
238
+ message.blocks.length = Math.min(message.blocks.length, value.count);
239
+ for (const item of value.blocks) {
240
+ const [index, type, first, second] = item;
241
+ if (type === "toolCall") {
242
+ message.blocks[index] = { type, id: first, name: second, args: item[4] };
243
+ }
244
+ else {
245
+ message.blocks[index] = this.append(message.blocks[index], type, first, second);
246
+ }
247
+ }
248
+ }
249
+ append(current, type, offset, text) {
250
+ const previous = offset === 0 ? "" : current?.text;
251
+ if (previous === undefined || previous.length !== offset) {
252
+ throw new Error("Assistant append gap.");
253
+ }
254
+ this.chars += text.length;
255
+ if (this.chars > 1024 * 1024) {
256
+ throw new Error("Live content awaits journal publication.");
257
+ }
258
+ return { type, text: previous + text };
259
+ }
260
+ /** Retire live state only after its exact journal entries have been indexed. */
261
+ settle(has) {
262
+ while (this.handovers.length > 0 && has(this.handovers[0][1])) {
263
+ const item = this.handovers.shift();
264
+ if (item) {
265
+ this.retire(item[2], item[3]);
266
+ }
267
+ }
268
+ return (this.handovers.length === 0 &&
269
+ (this.leaf === undefined || this.leaf === null || has(this.leaf)));
270
+ }
271
+ retire(key, tool) {
272
+ if (key !== null && this.message?.key === key) {
273
+ this.message = undefined;
274
+ }
275
+ for (const id of this.tools.keys()) {
276
+ if (tool !== null && (id === tool || id.startsWith(`${tool}/`))) {
277
+ this.tools.delete(id);
278
+ }
279
+ }
280
+ }
281
+ close() {
282
+ this.closed = true;
283
+ this.socket.destroy();
284
+ }
285
+ }
@@ -0,0 +1,65 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { findPackageJSON } from "node:module";
10
+ import * as path from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+ import { loadSDK, packageRoot } from "./host.js";
13
+ // Runtime values in lazy-only viewer modules come from the selected host's files.
14
+ // A bare import would resolve against whatever node_modules exist when /agents first opens.
15
+ // Jiti aliases the pi-tui root to an index file, so submodule URLs must resolve here too.
16
+ const sdkRoot = packageRoot();
17
+ const manifest = findPackageJSON("@earendil-works/pi-tui", path.join(sdkRoot, "dist/index.js"));
18
+ if (manifest === undefined) {
19
+ throw new Error("Cannot locate the native Pi TUI package.");
20
+ }
21
+ const root = pathToFileURL(path.join(path.dirname(manifest), "dist/index.js"));
22
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
23
+ const tui = await import(__rewriteRelativeImportExtension(root.href));
24
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
25
+ const layout = await import(__rewriteRelativeImportExtension(new URL("./layout.js", root).href));
26
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
27
+ const dispatch = await import(__rewriteRelativeImportExtension(new URL("./tui.js", root).href));
28
+ const sdk = await loadSDK(sdkRoot);
29
+ export const { Container, HStack, ScrollView, Text, TruncatedText, VStack } = tui;
30
+ export const { AssistantMessageComponent, ToolExecutionComponent, createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createPowerShellToolDefinition, createReadToolDefinition, createWriteToolDefinition, rawKeyHint, } = sdk;
31
+ export const { renderLayoutFrame, getLayoutBoxesAt, getScrollViewsAt } = layout;
32
+ export const { dispatchMouseEvent } = dispatch;
33
+ export function dispatchLayoutMouse(component, frame, event) {
34
+ if (event.type === "wheel") {
35
+ getScrollViewsAt(frame, event.x, event.y)
36
+ .at(0)
37
+ ?.scrollBy(event.wheelDelta ?? 0);
38
+ return {
39
+ handled: true,
40
+ target: {
41
+ component,
42
+ originX: event.screenX - event.x,
43
+ originY: event.screenY - event.y,
44
+ width: event.width,
45
+ height: event.height,
46
+ },
47
+ };
48
+ }
49
+ for (const box of getLayoutBoxesAt(frame, event.x, event.y)) {
50
+ if (box.children.length > 0 || box.component === component) {
51
+ continue;
52
+ }
53
+ const result = dispatchMouseEvent(box.component, {
54
+ ...event,
55
+ x: event.x - box.rect.x,
56
+ y: event.y - box.rect.y,
57
+ width: box.rect.width,
58
+ height: box.rect.height,
59
+ });
60
+ if (result) {
61
+ return result;
62
+ }
63
+ }
64
+ return undefined;
65
+ }