@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.
- package/CHANGELOG.md +51 -2
- package/README.md +36 -5
- package/dist/agent/contracts.js +6 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +207 -173
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +346 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/process.js +198 -25
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +125 -20
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +29 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +27 -21
- package/dist/pi-intercom/broker/client-last.d.ts +13 -0
- package/dist/pi-intercom/broker/client-last.js +17 -5
- package/dist/pi-intercom/broker/client.d.ts +60 -0
- package/dist/pi-intercom/broker/client.js +47 -16
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +26 -0
- package/dist/pi-intercom/broker/last.js +57 -32
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +19 -0
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +10 -2
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +12 -38
- package/dist/pi-intercom/index.js +3 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +2 -0
- package/docs/development.md +40 -1
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +226 -6
- package/docs/performance.md +87 -11
- package/docs/reference.md +157 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/contracts.ts +8 -34
- package/src/agent/host.ts +19 -20
- package/src/agent/index.ts +224 -190
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +405 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/process.ts +240 -34
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +125 -27
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +27 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +27 -23
- package/src/pi-intercom/broker/client-last.ts +25 -12
- package/src/pi-intercom/broker/client.ts +55 -33
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +86 -33
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +25 -0
- package/src/pi-intercom/connection.ts +21 -10
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +18 -49
- package/src/pi-intercom/index.ts +3 -0
- package/src/pi-intercom/last-message.ts +10 -19
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { Static } from "typebox";
|
|
5
|
+
import { agentObservationSocketPath } from "../pi-intercom/broker/paths.ts";
|
|
6
|
+
import { createMessageReader, writeFrame } from "../pi-intercom/broker/framing.ts";
|
|
7
|
+
import type { ObservationIdentity } from "./observation.ts";
|
|
8
|
+
import type { Launch } from "./contracts.ts";
|
|
9
|
+
export type Watch = { launch: Launch; identity?: ObservationIdentity };
|
|
10
|
+
import { Assert, Type } from "./typebox.ts";
|
|
11
|
+
|
|
12
|
+
const activitySchema = Type.Object({
|
|
13
|
+
phase: Type.String(),
|
|
14
|
+
tools: Type.Array(Type.String()),
|
|
15
|
+
model: Type.Optional(Type.String()),
|
|
16
|
+
thinking: Type.Optional(Type.String()),
|
|
17
|
+
});
|
|
18
|
+
const nullableString = Type.Union([Type.String(), Type.Null()]);
|
|
19
|
+
const settlementSchema = Type.Tuple([
|
|
20
|
+
Type.Number(),
|
|
21
|
+
Type.String(),
|
|
22
|
+
Type.Union([Type.Number(), Type.Null()]),
|
|
23
|
+
nullableString,
|
|
24
|
+
]);
|
|
25
|
+
const blockSchema = Type.Union([
|
|
26
|
+
Type.Tuple([
|
|
27
|
+
Type.Integer({ minimum: 0, maximum: 255 }),
|
|
28
|
+
Type.Union([Type.Literal("text"), Type.Literal("thinking")]),
|
|
29
|
+
Type.Integer({ minimum: 0 }),
|
|
30
|
+
Type.String(),
|
|
31
|
+
]),
|
|
32
|
+
Type.Tuple([
|
|
33
|
+
Type.Integer({ minimum: 0, maximum: 255 }),
|
|
34
|
+
Type.Literal("toolCall"),
|
|
35
|
+
Type.String(),
|
|
36
|
+
Type.String(),
|
|
37
|
+
Type.Unknown(),
|
|
38
|
+
]),
|
|
39
|
+
]);
|
|
40
|
+
const messageSchema = Type.Object({
|
|
41
|
+
key: Type.Number(),
|
|
42
|
+
reset: Type.Boolean(),
|
|
43
|
+
ended: Type.Boolean(),
|
|
44
|
+
count: Type.Number(),
|
|
45
|
+
blocks: Type.Array(blockSchema),
|
|
46
|
+
});
|
|
47
|
+
const toolSchema = Type.Object({
|
|
48
|
+
id: Type.String(),
|
|
49
|
+
name: Type.Optional(Type.String()),
|
|
50
|
+
args: Type.Optional(Type.Unknown()),
|
|
51
|
+
partial: Type.Optional(Type.Object({ rev: Type.Number(), value: Type.Unknown() })),
|
|
52
|
+
end: Type.Optional(Type.Object({ result: Type.Unknown(), isError: Type.Boolean() })),
|
|
53
|
+
});
|
|
54
|
+
const frameSchema = Type.Union([
|
|
55
|
+
Type.Object({
|
|
56
|
+
t: Type.Literal("snapshot"),
|
|
57
|
+
v: Type.Literal(1),
|
|
58
|
+
generation: Type.String(),
|
|
59
|
+
rootId: Type.String(),
|
|
60
|
+
sessionId: Type.String(),
|
|
61
|
+
seq: Type.Number(),
|
|
62
|
+
leafId: nullableString,
|
|
63
|
+
activity: activitySchema,
|
|
64
|
+
}),
|
|
65
|
+
Type.Object({ t: Type.Literal("activity"), activity: activitySchema }),
|
|
66
|
+
Type.Object({
|
|
67
|
+
t: Type.Literal("delta"),
|
|
68
|
+
settled: Type.Array(settlementSchema),
|
|
69
|
+
tools: Type.Array(toolSchema),
|
|
70
|
+
message: Type.Optional(messageSchema),
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
export interface ViewedBlock {
|
|
74
|
+
type: "text" | "thinking" | "toolCall";
|
|
75
|
+
text?: string;
|
|
76
|
+
id?: string;
|
|
77
|
+
name?: string;
|
|
78
|
+
args?: unknown;
|
|
79
|
+
}
|
|
80
|
+
export interface ViewedTool {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
args?: unknown;
|
|
84
|
+
partial?: unknown;
|
|
85
|
+
end?: { result: unknown; isError: boolean };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class ObservationChannel {
|
|
89
|
+
current?: ObservationClient;
|
|
90
|
+
private pending?: ObservationClient;
|
|
91
|
+
private readonly changed: () => void;
|
|
92
|
+
constructor(changed: () => void) {
|
|
93
|
+
this.changed = changed;
|
|
94
|
+
}
|
|
95
|
+
get connecting(): boolean {
|
|
96
|
+
return this.pending !== undefined;
|
|
97
|
+
}
|
|
98
|
+
connect(identity: ObservationIdentity): void {
|
|
99
|
+
if (this.pending || this.current?.connected === true) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const client = new ObservationClient(identity, "detail", () => {
|
|
103
|
+
if (this.pending !== client && this.current !== client) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (this.pending === client && client.connected) {
|
|
107
|
+
this.current = client;
|
|
108
|
+
this.pending = undefined;
|
|
109
|
+
}
|
|
110
|
+
if (this.pending === client && client.closed) {
|
|
111
|
+
this.pending = undefined;
|
|
112
|
+
}
|
|
113
|
+
this.changed();
|
|
114
|
+
});
|
|
115
|
+
this.pending = client;
|
|
116
|
+
}
|
|
117
|
+
pause(): void {
|
|
118
|
+
this.current?.close();
|
|
119
|
+
this.pending?.close();
|
|
120
|
+
this.pending = undefined;
|
|
121
|
+
}
|
|
122
|
+
clear(): void {
|
|
123
|
+
this.pause();
|
|
124
|
+
this.current = undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A selected read-only peer. Destruction closes its own socket, never native execution. */
|
|
129
|
+
export class ObservationClient {
|
|
130
|
+
activity = "Connecting";
|
|
131
|
+
model = "";
|
|
132
|
+
thinking = "";
|
|
133
|
+
connected = false;
|
|
134
|
+
closed = false;
|
|
135
|
+
revision = 0;
|
|
136
|
+
leaf?: string | null;
|
|
137
|
+
message?: { key: number; ended: boolean; blocks: ViewedBlock[] };
|
|
138
|
+
readonly tools = new Map<string, ViewedTool>();
|
|
139
|
+
private readonly handovers: Array<Static<typeof settlementSchema>> = [];
|
|
140
|
+
private seq = -1;
|
|
141
|
+
private chars = 0;
|
|
142
|
+
private readonly socket: net.Socket;
|
|
143
|
+
private readonly identity: ObservationIdentity;
|
|
144
|
+
private readonly changed: () => void;
|
|
145
|
+
constructor(identity: ObservationIdentity, level: "activity" | "detail", changed: () => void) {
|
|
146
|
+
this.identity = identity;
|
|
147
|
+
this.changed = changed;
|
|
148
|
+
const file = agentObservationSocketPath(identity.token);
|
|
149
|
+
const directory = fs.lstatSync(path.dirname(file));
|
|
150
|
+
const endpoint = fs.lstatSync(file);
|
|
151
|
+
const uid = process.getuid?.();
|
|
152
|
+
if (
|
|
153
|
+
!directory.isDirectory() ||
|
|
154
|
+
directory.uid !== uid ||
|
|
155
|
+
(directory.mode & 0o777) !== 0o700 ||
|
|
156
|
+
!endpoint.isSocket() ||
|
|
157
|
+
endpoint.uid !== uid ||
|
|
158
|
+
(endpoint.mode & 0o777) !== 0o600
|
|
159
|
+
) {
|
|
160
|
+
throw new Error("Unsafe child observation endpoint.");
|
|
161
|
+
}
|
|
162
|
+
this.socket = net.createConnection(file);
|
|
163
|
+
this.socket.on("connect", () => {
|
|
164
|
+
writeFrame(this.socket, JSON.stringify({ t: "hello", v: 1, ...identity, level }));
|
|
165
|
+
});
|
|
166
|
+
this.socket.on(
|
|
167
|
+
"data",
|
|
168
|
+
createMessageReader(
|
|
169
|
+
(frame) => {
|
|
170
|
+
try {
|
|
171
|
+
this.receive(frame);
|
|
172
|
+
} catch {
|
|
173
|
+
this.socket.destroy();
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
() => {
|
|
177
|
+
this.socket.destroy();
|
|
178
|
+
},
|
|
179
|
+
256 * 1024,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
this.socket.on("error", () => {
|
|
183
|
+
this.socket.destroy();
|
|
184
|
+
});
|
|
185
|
+
this.socket.on("close", () => {
|
|
186
|
+
this.closed = true;
|
|
187
|
+
this.connected = false;
|
|
188
|
+
this.changed();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
private receive(value: unknown): void {
|
|
192
|
+
Assert(frameSchema, value);
|
|
193
|
+
if (value.t === "snapshot") {
|
|
194
|
+
this.baseline(value);
|
|
195
|
+
} else if (this.seq < 0) {
|
|
196
|
+
throw new Error("Observation before baseline.");
|
|
197
|
+
} else if (value.t === "delta") {
|
|
198
|
+
this.delta(value);
|
|
199
|
+
}
|
|
200
|
+
if (value.t !== "delta") {
|
|
201
|
+
this.activity = [value.activity.phase, ...value.activity.tools].join(" · ");
|
|
202
|
+
this.model = value.activity.model ?? this.model;
|
|
203
|
+
this.thinking = value.activity.thinking ?? this.thinking;
|
|
204
|
+
}
|
|
205
|
+
this.revision++;
|
|
206
|
+
this.changed();
|
|
207
|
+
}
|
|
208
|
+
private baseline(value: Extract<Static<typeof frameSchema>, { t: "snapshot" }>): void {
|
|
209
|
+
const { token, rootId, sessionId } = this.identity;
|
|
210
|
+
if (value.generation !== token || value.rootId !== rootId || value.sessionId !== sessionId) {
|
|
211
|
+
throw new Error("Foreign child observation.");
|
|
212
|
+
}
|
|
213
|
+
this.leaf = value.leafId;
|
|
214
|
+
this.seq = value.seq;
|
|
215
|
+
this.connected = true;
|
|
216
|
+
this.message = undefined;
|
|
217
|
+
this.tools.clear();
|
|
218
|
+
this.handovers.length = 0;
|
|
219
|
+
this.chars = 0;
|
|
220
|
+
}
|
|
221
|
+
private delta(value: Extract<Static<typeof frameSchema>, { t: "delta" }>): void {
|
|
222
|
+
for (const item of value.settled) {
|
|
223
|
+
if (item[0] !== this.seq + 1) {
|
|
224
|
+
throw new Error("Invalid handover sequence.");
|
|
225
|
+
}
|
|
226
|
+
this.seq = item[0];
|
|
227
|
+
this.leaf = item[1];
|
|
228
|
+
this.handovers.push(item);
|
|
229
|
+
}
|
|
230
|
+
if (this.handovers.length > 256) {
|
|
231
|
+
throw new Error("Journal handover stalled.");
|
|
232
|
+
}
|
|
233
|
+
for (const item of value.tools) {
|
|
234
|
+
const tool = this.tools.get(item.id) ?? {
|
|
235
|
+
id: item.id,
|
|
236
|
+
name: item.name ?? "tool",
|
|
237
|
+
args: item.args,
|
|
238
|
+
};
|
|
239
|
+
if (item.partial) {
|
|
240
|
+
tool.partial = item.partial.value;
|
|
241
|
+
}
|
|
242
|
+
if (item.end) {
|
|
243
|
+
tool.end = item.end;
|
|
244
|
+
}
|
|
245
|
+
this.tools.set(item.id, tool);
|
|
246
|
+
}
|
|
247
|
+
if (this.tools.size > 256) {
|
|
248
|
+
throw new Error("Too many live tools.");
|
|
249
|
+
}
|
|
250
|
+
this.assistant(value.message);
|
|
251
|
+
}
|
|
252
|
+
private assistant(value: Static<typeof messageSchema> | undefined): void {
|
|
253
|
+
if (!value) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (value.reset) {
|
|
257
|
+
this.message = { key: value.key, ended: false, blocks: [] };
|
|
258
|
+
this.chars = 0;
|
|
259
|
+
}
|
|
260
|
+
const message = this.message;
|
|
261
|
+
if (message?.key !== value.key) {
|
|
262
|
+
throw new Error("Assistant changed without reset.");
|
|
263
|
+
}
|
|
264
|
+
message.ended = value.ended;
|
|
265
|
+
message.blocks.length = Math.min(message.blocks.length, value.count);
|
|
266
|
+
for (const item of value.blocks) {
|
|
267
|
+
const [index, type, first, second] = item;
|
|
268
|
+
if (type === "toolCall") {
|
|
269
|
+
message.blocks[index] = { type, id: first, name: second, args: item[4] };
|
|
270
|
+
} else {
|
|
271
|
+
message.blocks[index] = this.append(message.blocks[index], type, first, second);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
private append(
|
|
276
|
+
current: ViewedBlock | undefined,
|
|
277
|
+
type: "text" | "thinking",
|
|
278
|
+
offset: number,
|
|
279
|
+
text: string,
|
|
280
|
+
): ViewedBlock {
|
|
281
|
+
const previous = offset === 0 ? "" : current?.text;
|
|
282
|
+
if (previous === undefined || previous.length !== offset) {
|
|
283
|
+
throw new Error("Assistant append gap.");
|
|
284
|
+
}
|
|
285
|
+
this.chars += text.length;
|
|
286
|
+
if (this.chars > 1024 * 1024) {
|
|
287
|
+
throw new Error("Live content awaits journal publication.");
|
|
288
|
+
}
|
|
289
|
+
return { type, text: previous + text };
|
|
290
|
+
}
|
|
291
|
+
/** Retire live state only after its exact journal entries have been indexed. */
|
|
292
|
+
settle(has: (entry: string) => boolean): boolean {
|
|
293
|
+
while (this.handovers.length > 0 && has(this.handovers[0][1])) {
|
|
294
|
+
const item = this.handovers.shift();
|
|
295
|
+
if (item) {
|
|
296
|
+
this.retire(item[2], item[3]);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return (
|
|
300
|
+
this.handovers.length === 0 &&
|
|
301
|
+
(this.leaf === undefined || this.leaf === null || has(this.leaf))
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
private retire(key: number | null, tool: string | null): void {
|
|
305
|
+
if (key !== null && this.message?.key === key) {
|
|
306
|
+
this.message = undefined;
|
|
307
|
+
}
|
|
308
|
+
for (const id of this.tools.keys()) {
|
|
309
|
+
if (tool !== null && (id === tool || id.startsWith(`${tool}/`))) {
|
|
310
|
+
this.tools.delete(id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
close(): void {
|
|
315
|
+
this.closed = true;
|
|
316
|
+
this.socket.destroy();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { findPackageJSON } from "node:module";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type * as Tui from "@earendil-works/pi-tui";
|
|
5
|
+
import type * as Layout from "@earendil-works/pi-tui/dist/layout.js";
|
|
6
|
+
import type * as Dispatch from "@earendil-works/pi-tui/dist/tui.js";
|
|
7
|
+
import { loadSDK, packageRoot } from "./host.ts";
|
|
8
|
+
|
|
9
|
+
// Runtime values in lazy-only viewer modules come from the selected host's files.
|
|
10
|
+
// A bare import would resolve against whatever node_modules exist when /agents first opens.
|
|
11
|
+
// Jiti aliases the pi-tui root to an index file, so submodule URLs must resolve here too.
|
|
12
|
+
const sdkRoot = packageRoot();
|
|
13
|
+
const manifest = findPackageJSON("@earendil-works/pi-tui", path.join(sdkRoot, "dist/index.js"));
|
|
14
|
+
if (manifest === undefined) {
|
|
15
|
+
throw new Error("Cannot locate the native Pi TUI package.");
|
|
16
|
+
}
|
|
17
|
+
const root = pathToFileURL(path.join(path.dirname(manifest), "dist/index.js"));
|
|
18
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
19
|
+
const tui: typeof Tui = await import(root.href);
|
|
20
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
21
|
+
const layout: typeof Layout = await import(new URL("./layout.js", root).href);
|
|
22
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
23
|
+
const dispatch: typeof Dispatch = await import(new URL("./tui.js", root).href);
|
|
24
|
+
const sdk = await loadSDK(sdkRoot);
|
|
25
|
+
export const { Container, HStack, ScrollView, Text, TruncatedText, VStack } = tui;
|
|
26
|
+
export const {
|
|
27
|
+
AssistantMessageComponent,
|
|
28
|
+
ToolExecutionComponent,
|
|
29
|
+
createBashToolDefinition,
|
|
30
|
+
createEditToolDefinition,
|
|
31
|
+
createFindToolDefinition,
|
|
32
|
+
createGrepToolDefinition,
|
|
33
|
+
createLsToolDefinition,
|
|
34
|
+
createPowerShellToolDefinition,
|
|
35
|
+
createReadToolDefinition,
|
|
36
|
+
createWriteToolDefinition,
|
|
37
|
+
rawKeyHint,
|
|
38
|
+
} = sdk;
|
|
39
|
+
export const { renderLayoutFrame, getLayoutBoxesAt, getScrollViewsAt } = layout;
|
|
40
|
+
export const { dispatchMouseEvent } = dispatch;
|
|
41
|
+
export type { LayoutFrame } from "@earendil-works/pi-tui/dist/layout.js";
|
|
42
|
+
|
|
43
|
+
export function dispatchLayoutMouse(
|
|
44
|
+
component: Tui.Component,
|
|
45
|
+
frame: Layout.LayoutFrame,
|
|
46
|
+
event: Tui.TuiMouseEvent,
|
|
47
|
+
): ReturnType<typeof dispatchMouseEvent> {
|
|
48
|
+
if (event.type === "wheel") {
|
|
49
|
+
getScrollViewsAt(frame, event.x, event.y)
|
|
50
|
+
.at(0)
|
|
51
|
+
?.scrollBy(event.wheelDelta ?? 0);
|
|
52
|
+
return {
|
|
53
|
+
handled: true,
|
|
54
|
+
target: {
|
|
55
|
+
component,
|
|
56
|
+
originX: event.screenX - event.x,
|
|
57
|
+
originY: event.screenY - event.y,
|
|
58
|
+
width: event.width,
|
|
59
|
+
height: event.height,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
for (const box of getLayoutBoxesAt(frame, event.x, event.y)) {
|
|
64
|
+
if (box.children.length > 0 || box.component === component) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const result = dispatchMouseEvent(box.component, {
|
|
68
|
+
...event,
|
|
69
|
+
x: event.x - box.rect.x,
|
|
70
|
+
y: event.y - box.rect.y,
|
|
71
|
+
width: box.rect.width,
|
|
72
|
+
height: box.rect.height,
|
|
73
|
+
});
|
|
74
|
+
if (result) {
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { Watch } from "./observe-client.ts";
|
|
3
|
+
import { ObserverTranscript, type EntryAnchor } from "./observe-transcript.ts";
|
|
4
|
+
import { ScrollView } from "./observe-layout.ts";
|
|
5
|
+
|
|
6
|
+
export class Reading {
|
|
7
|
+
readonly watch: Watch;
|
|
8
|
+
readonly transcript: ObserverTranscript;
|
|
9
|
+
readonly scroll: InstanceType<typeof ScrollView>;
|
|
10
|
+
following = true;
|
|
11
|
+
pending = false;
|
|
12
|
+
width = 0;
|
|
13
|
+
destination?: EntryAnchor;
|
|
14
|
+
private readonly changed: (kind: "history" | "content") => void;
|
|
15
|
+
constructor(
|
|
16
|
+
watch: Watch,
|
|
17
|
+
options: {
|
|
18
|
+
tui: TUI;
|
|
19
|
+
hideThinking: boolean;
|
|
20
|
+
changed: (kind: "history" | "content") => void;
|
|
21
|
+
loaded: () => void;
|
|
22
|
+
},
|
|
23
|
+
) {
|
|
24
|
+
this.watch = watch;
|
|
25
|
+
this.changed = options.changed;
|
|
26
|
+
const { sessionFile: file, sessionId, cwd } = watch.launch;
|
|
27
|
+
this.transcript = new ObserverTranscript({
|
|
28
|
+
...options,
|
|
29
|
+
file,
|
|
30
|
+
sessionId,
|
|
31
|
+
cwd,
|
|
32
|
+
changed: (event) => {
|
|
33
|
+
this.following = false;
|
|
34
|
+
if (event.kind === "history") {
|
|
35
|
+
this.history(event.direction);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (event.kind === "content") {
|
|
39
|
+
this.destination = { id: event.id, offset: 0, end: event.direction === -1 };
|
|
40
|
+
}
|
|
41
|
+
this.changed("content");
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
this.transcript.sync();
|
|
45
|
+
this.scroll = new ScrollView(this.transcript, {
|
|
46
|
+
follow: "end",
|
|
47
|
+
primary: false,
|
|
48
|
+
overscroll: "contain",
|
|
49
|
+
scrollbar: "always",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
anchor(): EntryAnchor | undefined {
|
|
53
|
+
return (
|
|
54
|
+
this.destination ??
|
|
55
|
+
(this.following ? undefined : this.transcript.anchor(this.scroll.scrollTop))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
resize(width: number): boolean {
|
|
59
|
+
const changed = this.width >= 100 !== width >= 100;
|
|
60
|
+
this.width = width;
|
|
61
|
+
return changed;
|
|
62
|
+
}
|
|
63
|
+
reveal(anchor: EntryAnchor): void {
|
|
64
|
+
const line = this.transcript.line(anchor);
|
|
65
|
+
const top = line - (anchor.end === true ? this.scroll.viewportHeight : 0);
|
|
66
|
+
const row = line <= 1 && anchor.end !== true && this.scroll.scrollTop === 0;
|
|
67
|
+
this.scroll.scrollTo(row ? 0 : top, { disableFollow: true });
|
|
68
|
+
this.destination = undefined;
|
|
69
|
+
}
|
|
70
|
+
scrollBy(lines: number): void {
|
|
71
|
+
this.following = false;
|
|
72
|
+
this.scroll.scrollTo(this.scroll.scrollTop + lines, { disableFollow: true });
|
|
73
|
+
}
|
|
74
|
+
page(direction: -1 | 1): void {
|
|
75
|
+
this.following = false;
|
|
76
|
+
const height = Math.max(1, this.scroll.viewportHeight);
|
|
77
|
+
if (this.transcript.contentPage(this.scroll.scrollTop, height, direction)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// A history row keeps a full scroll above line zero; use the actual scroll result.
|
|
81
|
+
const remaining = this.scroll.scrollBy(direction * height);
|
|
82
|
+
if (remaining === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.history(direction);
|
|
86
|
+
}
|
|
87
|
+
private history(direction: -1 | 1): void {
|
|
88
|
+
const id = this.transcript.history(direction);
|
|
89
|
+
if (id !== undefined) {
|
|
90
|
+
this.destination = {
|
|
91
|
+
id,
|
|
92
|
+
offset: direction < 0 ? -Math.max(1, this.scroll.viewportHeight) : 1,
|
|
93
|
+
};
|
|
94
|
+
this.changed("history");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|