@h-rig/pi-rig 0.0.6-alpha.8 → 0.0.6-alpha.80
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/dist/src/client.d.ts +186 -0
- package/dist/src/client.js +425 -7
- package/dist/src/commands.d.ts +10 -0
- package/dist/src/commands.js +48 -3
- package/dist/src/index.d.ts +59 -0
- package/dist/src/index.js +1292 -34
- package/dist/src/live-mirror.d.ts +46 -0
- package/dist/src/live-mirror.js +223 -0
- package/dist/src/tools.d.ts +19 -0
- package/package.json +10 -2
package/dist/src/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// packages/pi-rig/src/client.ts
|
|
3
5
|
import { existsSync, readFileSync } from "fs";
|
|
4
6
|
import { homedir } from "os";
|
|
5
7
|
import { dirname, resolve } from "path";
|
|
8
|
+
import { RIG_PROTOCOL_VERSION, RIG_WS_CHANNELS, RIG_WS_METHODS } from "@rig/contracts";
|
|
6
9
|
function cleanString(value) {
|
|
7
10
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
8
11
|
}
|
|
@@ -79,8 +82,9 @@ function createRigContextFromEnv(env = process.env) {
|
|
|
79
82
|
const discovered = discoverRigContext(env);
|
|
80
83
|
const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
|
|
81
84
|
const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
|
|
82
|
-
const authToken = env.RIG_AUTH_TOKEN ?? env.
|
|
85
|
+
const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_SERVER_AUTH_TOKEN ?? discovered.authToken;
|
|
83
86
|
const steeringPollMs = cleanNonNegativeInteger(env.RIG_STEERING_POLL_MS);
|
|
87
|
+
const operatorSession = env.RIG_PI_OPERATOR_SESSION === "1" || env.RIG_PI_OPERATOR_SESSION === "true";
|
|
84
88
|
const active = Boolean(runId || taskId || serverUrl || projectRoot);
|
|
85
89
|
if (!active)
|
|
86
90
|
return { active: false };
|
|
@@ -91,7 +95,8 @@ function createRigContextFromEnv(env = process.env) {
|
|
|
91
95
|
...serverUrl ? { serverUrl } : {},
|
|
92
96
|
...projectRoot ? { projectRoot } : {},
|
|
93
97
|
...authToken ? { authToken } : {},
|
|
94
|
-
...steeringPollMs !== null ? { steeringPollMs } : {}
|
|
98
|
+
...steeringPollMs !== null ? { steeringPollMs } : {},
|
|
99
|
+
...operatorSession ? { operatorSession } : {}
|
|
95
100
|
};
|
|
96
101
|
}
|
|
97
102
|
function joinUrl(baseUrl, pathname) {
|
|
@@ -116,6 +121,8 @@ function requireServerUrl(context) {
|
|
|
116
121
|
}
|
|
117
122
|
return context.serverUrl;
|
|
118
123
|
}
|
|
124
|
+
var BRIDGE_REQUEST_TIMEOUT_MS = 30000;
|
|
125
|
+
var PROTOCOL_CHECK_TIMEOUT_MS = 1e4;
|
|
119
126
|
|
|
120
127
|
class RigBridgeClient {
|
|
121
128
|
context;
|
|
@@ -124,18 +131,46 @@ class RigBridgeClient {
|
|
|
124
131
|
this.context = input.context;
|
|
125
132
|
this.fetchImpl = input.fetchImpl ?? fetch;
|
|
126
133
|
}
|
|
127
|
-
async request(pathname, init) {
|
|
134
|
+
async request(pathname, init, timeoutMs = BRIDGE_REQUEST_TIMEOUT_MS) {
|
|
128
135
|
const headers = new Headers(init?.headers);
|
|
129
136
|
if (this.context.authToken && !headers.has("authorization")) {
|
|
130
137
|
headers.set("authorization", `Bearer ${this.context.authToken}`);
|
|
131
138
|
}
|
|
132
|
-
|
|
139
|
+
if (this.context.projectRoot && !headers.has("x-rig-project-root")) {
|
|
140
|
+
headers.set("x-rig-project-root", this.context.projectRoot);
|
|
141
|
+
}
|
|
142
|
+
const signal = init?.signal ?? (timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined);
|
|
143
|
+
const response = await this.fetchImpl(joinUrl(requireServerUrl(this.context), pathname), { ...init, headers, signal });
|
|
133
144
|
return readJsonResponse(response);
|
|
134
145
|
}
|
|
135
|
-
async status() {
|
|
136
|
-
const payload = await this.request("/api/server/status");
|
|
146
|
+
async status(timeoutMs) {
|
|
147
|
+
const payload = await this.request("/api/server/status", undefined, timeoutMs);
|
|
137
148
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
138
149
|
}
|
|
150
|
+
async checkProtocolCompatibility() {
|
|
151
|
+
let payload;
|
|
152
|
+
try {
|
|
153
|
+
payload = await this.status(PROTOCOL_CHECK_TIMEOUT_MS);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
status: "indeterminate",
|
|
157
|
+
serverProtocolVersion: null,
|
|
158
|
+
message: `Rig server protocol check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const raw = payload.protocolVersion;
|
|
162
|
+
const serverProtocolVersion = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : null;
|
|
163
|
+
if (serverProtocolVersion === RIG_PROTOCOL_VERSION) {
|
|
164
|
+
return { status: "compatible", serverProtocolVersion, message: null };
|
|
165
|
+
}
|
|
166
|
+
const serverLabel = serverProtocolVersion === null ? "v0 (no protocolVersion reported)" : `v${serverProtocolVersion}`;
|
|
167
|
+
const updateHint = (serverProtocolVersion ?? 0) < RIG_PROTOCOL_VERSION ? "update the Rig server (upgrade @h-rig/cli / @h-rig/server and restart it)" : "update pi-rig (upgrade @h-rig/pi-rig, or reinstall the extension from this server)";
|
|
168
|
+
return {
|
|
169
|
+
status: "mismatch",
|
|
170
|
+
serverProtocolVersion,
|
|
171
|
+
message: `Rig server speaks protocol ${serverLabel}, this pi-rig speaks v${RIG_PROTOCOL_VERSION} \u2014 ${updateHint}. The Rig bridge is disabled for this session.`
|
|
172
|
+
};
|
|
173
|
+
}
|
|
139
174
|
async listTasks() {
|
|
140
175
|
const payload = await this.request("/api/workspace/tasks");
|
|
141
176
|
return Array.isArray(payload) ? payload.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
@@ -157,6 +192,20 @@ class RigBridgeClient {
|
|
|
157
192
|
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}`);
|
|
158
193
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { runId };
|
|
159
194
|
}
|
|
195
|
+
async runLogs(runId = this.context.runId, limit = 20) {
|
|
196
|
+
if (!runId)
|
|
197
|
+
throw new Error("runId is required");
|
|
198
|
+
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/logs?limit=${encodeURIComponent(String(limit))}`);
|
|
199
|
+
const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
|
|
200
|
+
return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
201
|
+
}
|
|
202
|
+
async runTimeline(runId = this.context.runId, limit = 20) {
|
|
203
|
+
if (!runId)
|
|
204
|
+
throw new Error("runId is required");
|
|
205
|
+
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/timeline?limit=${encodeURIComponent(String(limit))}`);
|
|
206
|
+
const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
|
|
207
|
+
return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
208
|
+
}
|
|
160
209
|
async steer(message, runId = this.context.runId) {
|
|
161
210
|
if (!runId)
|
|
162
211
|
throw new Error("runId is required");
|
|
@@ -167,6 +216,16 @@ class RigBridgeClient {
|
|
|
167
216
|
});
|
|
168
217
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
169
218
|
}
|
|
219
|
+
async stop(runId = this.context.runId) {
|
|
220
|
+
if (!runId)
|
|
221
|
+
throw new Error("runId is required");
|
|
222
|
+
const payload = await this.request("/api/runs/stop", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: { "content-type": "application/json" },
|
|
225
|
+
body: JSON.stringify({ runId })
|
|
226
|
+
});
|
|
227
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true, runId };
|
|
228
|
+
}
|
|
170
229
|
async pollSteering(runId = this.context.runId) {
|
|
171
230
|
if (!runId)
|
|
172
231
|
return [];
|
|
@@ -198,9 +257,371 @@ class RigBridgeClient {
|
|
|
198
257
|
const payload = await this.request(`/api/workspace/tasks/${encodeURIComponent(taskId)}`);
|
|
199
258
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
200
259
|
}
|
|
260
|
+
async piProxy(action, init, runId = this.context.runId) {
|
|
261
|
+
if (!runId)
|
|
262
|
+
throw new Error("runId is required");
|
|
263
|
+
return this.request(`/api/runs/${encodeURIComponent(runId)}/pi/${action}`, init);
|
|
264
|
+
}
|
|
265
|
+
async workerCommands(runId = this.context.runId) {
|
|
266
|
+
const payload = await this.piProxy("commands", undefined, runId);
|
|
267
|
+
const commands = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.commands) ? payload.commands : [];
|
|
268
|
+
return commands.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
269
|
+
}
|
|
270
|
+
async workerRunCommand(command, args, runId = this.context.runId) {
|
|
271
|
+
const text = `/${command}${args.trim() ? ` ${args.trim()}` : ""}`;
|
|
272
|
+
const payload = await this.piProxy("commands/run", {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "content-type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ text })
|
|
276
|
+
}, runId);
|
|
277
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
278
|
+
}
|
|
279
|
+
async workerShell(command, runId = this.context.runId) {
|
|
280
|
+
const payload = await this.piProxy("shell", {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "content-type": "application/json" },
|
|
283
|
+
body: JSON.stringify({ text: command })
|
|
284
|
+
}, runId);
|
|
285
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
286
|
+
}
|
|
287
|
+
async workerAbort(runId = this.context.runId) {
|
|
288
|
+
const payload = await this.piProxy("abort", { method: "POST" }, runId);
|
|
289
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
290
|
+
}
|
|
291
|
+
async workerCapabilities(runId = this.context.runId) {
|
|
292
|
+
const payload = await this.piProxy("capabilities", undefined, runId);
|
|
293
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
294
|
+
}
|
|
295
|
+
async workerRespondExtensionUi(requestId, response, runId = this.context.runId) {
|
|
296
|
+
const payload = await this.piProxy("extension-ui/respond", {
|
|
297
|
+
method: "POST",
|
|
298
|
+
headers: { "content-type": "application/json" },
|
|
299
|
+
body: JSON.stringify({ requestId, ...response })
|
|
300
|
+
}, runId);
|
|
301
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
302
|
+
}
|
|
303
|
+
async fetchRunSessionFile(runId = this.context.runId) {
|
|
304
|
+
if (!runId)
|
|
305
|
+
return null;
|
|
306
|
+
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/session-file`);
|
|
307
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
308
|
+
return null;
|
|
309
|
+
const record = payload;
|
|
310
|
+
if (record.ok !== true || typeof record.content !== "string" || !record.content.trim())
|
|
311
|
+
return null;
|
|
312
|
+
return {
|
|
313
|
+
fileName: typeof record.fileName === "string" && record.fileName.trim() ? record.fileName : `rig-run-${runId}.jsonl`,
|
|
314
|
+
content: record.content
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function buildRigWebSocketUrl(serverUrl, authToken) {
|
|
319
|
+
const url = new URL(serverUrl);
|
|
320
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
321
|
+
if (authToken) {
|
|
322
|
+
url.searchParams.set("token", authToken);
|
|
323
|
+
}
|
|
324
|
+
return url.toString();
|
|
325
|
+
}
|
|
326
|
+
function defaultWebSocketFactory() {
|
|
327
|
+
const ctor = globalThis.WebSocket;
|
|
328
|
+
if (typeof ctor !== "function")
|
|
329
|
+
return null;
|
|
330
|
+
return (url) => new ctor(url);
|
|
331
|
+
}
|
|
332
|
+
function webSocketEventText(data) {
|
|
333
|
+
if (typeof data === "string")
|
|
334
|
+
return data;
|
|
335
|
+
if (data instanceof Uint8Array)
|
|
336
|
+
return new TextDecoder().decode(data);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
class RigBridgeSocket {
|
|
341
|
+
context;
|
|
342
|
+
handlers;
|
|
343
|
+
factory;
|
|
344
|
+
reconnectBaseMs;
|
|
345
|
+
reconnectMaxMs;
|
|
346
|
+
socket = null;
|
|
347
|
+
connectedFlag = false;
|
|
348
|
+
closed = false;
|
|
349
|
+
started = false;
|
|
350
|
+
attempt = 0;
|
|
351
|
+
reconnectTimer = null;
|
|
352
|
+
ackSequence = 0;
|
|
353
|
+
constructor(input) {
|
|
354
|
+
this.context = input.context;
|
|
355
|
+
this.handlers = input.handlers ?? {};
|
|
356
|
+
this.factory = input.webSocketFactory ?? defaultWebSocketFactory();
|
|
357
|
+
this.reconnectBaseMs = input.reconnectBaseMs ?? 1000;
|
|
358
|
+
this.reconnectMaxMs = input.reconnectMaxMs ?? 30000;
|
|
359
|
+
}
|
|
360
|
+
get connected() {
|
|
361
|
+
return this.connectedFlag;
|
|
362
|
+
}
|
|
363
|
+
start() {
|
|
364
|
+
if (this.closed)
|
|
365
|
+
return false;
|
|
366
|
+
if (this.started)
|
|
367
|
+
return true;
|
|
368
|
+
if (!this.context.serverUrl || !this.factory)
|
|
369
|
+
return false;
|
|
370
|
+
this.started = true;
|
|
371
|
+
this.connect();
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
close() {
|
|
375
|
+
this.closed = true;
|
|
376
|
+
this.connectedFlag = false;
|
|
377
|
+
if (this.reconnectTimer) {
|
|
378
|
+
clearTimeout(this.reconnectTimer);
|
|
379
|
+
this.reconnectTimer = null;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
this.socket?.close();
|
|
383
|
+
} catch {}
|
|
384
|
+
this.socket = null;
|
|
385
|
+
}
|
|
386
|
+
ackSteering(runId, ids) {
|
|
387
|
+
if (!this.connectedFlag || !this.socket || ids.length === 0)
|
|
388
|
+
return;
|
|
389
|
+
try {
|
|
390
|
+
this.socket.send(JSON.stringify({
|
|
391
|
+
id: `pi-rig-steer-ack-${++this.ackSequence}`,
|
|
392
|
+
body: { _tag: RIG_WS_METHODS.ackRunSteering, runId, ids }
|
|
393
|
+
}));
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
connect() {
|
|
397
|
+
if (this.closed || !this.context.serverUrl || !this.factory)
|
|
398
|
+
return;
|
|
399
|
+
let socket;
|
|
400
|
+
try {
|
|
401
|
+
socket = this.factory(buildRigWebSocketUrl(this.context.serverUrl, this.context.authToken));
|
|
402
|
+
} catch {
|
|
403
|
+
this.scheduleReconnect();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
this.socket = socket;
|
|
407
|
+
let gone = false;
|
|
408
|
+
socket.addEventListener("open", () => {
|
|
409
|
+
if (this.closed || gone)
|
|
410
|
+
return;
|
|
411
|
+
this.attempt = 0;
|
|
412
|
+
this.connectedFlag = true;
|
|
413
|
+
this.handlers.onConnect?.();
|
|
414
|
+
});
|
|
415
|
+
const onGone = () => {
|
|
416
|
+
if (gone)
|
|
417
|
+
return;
|
|
418
|
+
gone = true;
|
|
419
|
+
const wasConnected = this.connectedFlag;
|
|
420
|
+
this.connectedFlag = false;
|
|
421
|
+
try {
|
|
422
|
+
socket.close();
|
|
423
|
+
} catch {}
|
|
424
|
+
if (this.socket === socket)
|
|
425
|
+
this.socket = null;
|
|
426
|
+
if (this.closed)
|
|
427
|
+
return;
|
|
428
|
+
if (wasConnected)
|
|
429
|
+
this.handlers.onDisconnect?.();
|
|
430
|
+
this.scheduleReconnect();
|
|
431
|
+
};
|
|
432
|
+
socket.addEventListener("close", onGone);
|
|
433
|
+
socket.addEventListener("error", onGone);
|
|
434
|
+
socket.addEventListener("message", (event) => {
|
|
435
|
+
if (!this.closed)
|
|
436
|
+
this.handleMessage(event);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
scheduleReconnect() {
|
|
440
|
+
if (this.closed || this.reconnectTimer)
|
|
441
|
+
return;
|
|
442
|
+
const delay = Math.min(this.reconnectBaseMs * 2 ** this.attempt, this.reconnectMaxMs);
|
|
443
|
+
this.attempt += 1;
|
|
444
|
+
const timer = setTimeout(() => {
|
|
445
|
+
this.reconnectTimer = null;
|
|
446
|
+
this.connect();
|
|
447
|
+
}, delay);
|
|
448
|
+
this.reconnectTimer = timer;
|
|
449
|
+
if (typeof timer.unref === "function") {
|
|
450
|
+
timer.unref();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
handleMessage(event) {
|
|
454
|
+
const text = webSocketEventText(event.data);
|
|
455
|
+
if (!text)
|
|
456
|
+
return;
|
|
457
|
+
let parsed;
|
|
458
|
+
try {
|
|
459
|
+
parsed = JSON.parse(text);
|
|
460
|
+
} catch {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
464
|
+
return;
|
|
465
|
+
const record = parsed;
|
|
466
|
+
if (record.type !== "push" || typeof record.channel !== "string")
|
|
467
|
+
return;
|
|
468
|
+
const data = record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data : null;
|
|
469
|
+
if (record.channel === RIG_WS_CHANNELS.runSteering) {
|
|
470
|
+
if (!data || !this.context.runId || data.runId !== this.context.runId)
|
|
471
|
+
return;
|
|
472
|
+
const message = data.message && typeof data.message === "object" && !Array.isArray(data.message) ? data.message : null;
|
|
473
|
+
if (message)
|
|
474
|
+
this.handlers.onSteeringMessage?.(message);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (record.channel === RIG_WS_CHANNELS.event) {
|
|
478
|
+
if (data)
|
|
479
|
+
this.handlers.onRigEvent?.(data);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (record.channel === RIG_WS_CHANNELS.snapshotInvalidated) {
|
|
483
|
+
this.handlers.onSnapshotInvalidated?.();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function buildRunPiEventsWebSocketUrl(context) {
|
|
488
|
+
if (!context.serverUrl || !context.runId)
|
|
489
|
+
return null;
|
|
490
|
+
const url = new URL(`${context.serverUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(context.runId)}/pi/events`);
|
|
491
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
492
|
+
if (context.authToken)
|
|
493
|
+
url.searchParams.set("token", context.authToken);
|
|
494
|
+
if (context.projectRoot)
|
|
495
|
+
url.searchParams.set("rigProjectRoot", context.projectRoot);
|
|
496
|
+
return url.toString();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
class RigWorkerEventsSocket {
|
|
500
|
+
context;
|
|
501
|
+
handlers;
|
|
502
|
+
factory;
|
|
503
|
+
reconnectBaseMs;
|
|
504
|
+
reconnectMaxMs;
|
|
505
|
+
socket = null;
|
|
506
|
+
connectedFlag = false;
|
|
507
|
+
closed = false;
|
|
508
|
+
started = false;
|
|
509
|
+
attempt = 0;
|
|
510
|
+
reconnectTimer = null;
|
|
511
|
+
constructor(input) {
|
|
512
|
+
this.context = input.context;
|
|
513
|
+
this.handlers = input.handlers ?? {};
|
|
514
|
+
this.factory = input.webSocketFactory ?? defaultWebSocketFactory();
|
|
515
|
+
this.reconnectBaseMs = input.reconnectBaseMs ?? 1000;
|
|
516
|
+
this.reconnectMaxMs = input.reconnectMaxMs ?? 30000;
|
|
517
|
+
}
|
|
518
|
+
get connected() {
|
|
519
|
+
return this.connectedFlag;
|
|
520
|
+
}
|
|
521
|
+
start() {
|
|
522
|
+
if (this.closed)
|
|
523
|
+
return false;
|
|
524
|
+
if (this.started)
|
|
525
|
+
return true;
|
|
526
|
+
if (!buildRunPiEventsWebSocketUrl(this.context) || !this.factory)
|
|
527
|
+
return false;
|
|
528
|
+
this.started = true;
|
|
529
|
+
this.connect();
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
close() {
|
|
533
|
+
this.closed = true;
|
|
534
|
+
this.connectedFlag = false;
|
|
535
|
+
if (this.reconnectTimer) {
|
|
536
|
+
clearTimeout(this.reconnectTimer);
|
|
537
|
+
this.reconnectTimer = null;
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
this.socket?.close();
|
|
541
|
+
} catch {}
|
|
542
|
+
this.socket = null;
|
|
543
|
+
}
|
|
544
|
+
connect() {
|
|
545
|
+
const target = buildRunPiEventsWebSocketUrl(this.context);
|
|
546
|
+
if (this.closed || !target || !this.factory)
|
|
547
|
+
return;
|
|
548
|
+
let socket;
|
|
549
|
+
try {
|
|
550
|
+
socket = this.factory(target);
|
|
551
|
+
} catch {
|
|
552
|
+
this.scheduleReconnect();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
this.socket = socket;
|
|
556
|
+
let gone = false;
|
|
557
|
+
socket.addEventListener("open", () => {
|
|
558
|
+
if (this.closed || gone)
|
|
559
|
+
return;
|
|
560
|
+
this.attempt = 0;
|
|
561
|
+
this.connectedFlag = true;
|
|
562
|
+
this.handlers.onConnect?.();
|
|
563
|
+
});
|
|
564
|
+
const onGone = () => {
|
|
565
|
+
if (gone)
|
|
566
|
+
return;
|
|
567
|
+
gone = true;
|
|
568
|
+
const wasConnected = this.connectedFlag;
|
|
569
|
+
this.connectedFlag = false;
|
|
570
|
+
try {
|
|
571
|
+
socket.close();
|
|
572
|
+
} catch {}
|
|
573
|
+
if (this.socket === socket)
|
|
574
|
+
this.socket = null;
|
|
575
|
+
if (this.closed)
|
|
576
|
+
return;
|
|
577
|
+
if (wasConnected)
|
|
578
|
+
this.handlers.onDisconnect?.();
|
|
579
|
+
this.scheduleReconnect();
|
|
580
|
+
};
|
|
581
|
+
socket.addEventListener("close", onGone);
|
|
582
|
+
socket.addEventListener("error", onGone);
|
|
583
|
+
socket.addEventListener("message", (event) => {
|
|
584
|
+
if (this.closed)
|
|
585
|
+
return;
|
|
586
|
+
const text = webSocketEventText(event.data);
|
|
587
|
+
if (!text)
|
|
588
|
+
return;
|
|
589
|
+
let parsed;
|
|
590
|
+
try {
|
|
591
|
+
parsed = JSON.parse(text);
|
|
592
|
+
} catch {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
596
|
+
this.handlers.onFrame?.(parsed);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
scheduleReconnect() {
|
|
601
|
+
if (this.closed || this.reconnectTimer)
|
|
602
|
+
return;
|
|
603
|
+
const delay = Math.min(this.reconnectBaseMs * 2 ** this.attempt, this.reconnectMaxMs);
|
|
604
|
+
this.attempt += 1;
|
|
605
|
+
const timer = setTimeout(() => {
|
|
606
|
+
this.reconnectTimer = null;
|
|
607
|
+
this.connect();
|
|
608
|
+
}, delay);
|
|
609
|
+
this.reconnectTimer = timer;
|
|
610
|
+
if (typeof timer.unref === "function") {
|
|
611
|
+
timer.unref();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
201
614
|
}
|
|
202
615
|
|
|
203
616
|
// packages/pi-rig/src/commands.ts
|
|
617
|
+
function runRecordFromPayload(payload) {
|
|
618
|
+
return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
619
|
+
}
|
|
620
|
+
function formatEntry(entry) {
|
|
621
|
+
const type = String(entry.type ?? entry.title ?? "event");
|
|
622
|
+
const text = typeof entry.text === "string" ? entry.text : typeof entry.detail === "string" ? entry.detail : typeof entry.message === "string" ? entry.message : JSON.stringify(entry);
|
|
623
|
+
return `${type}: ${text}`;
|
|
624
|
+
}
|
|
204
625
|
function createRigSlashCommands(input) {
|
|
205
626
|
const notify = input.notify ?? (() => {});
|
|
206
627
|
async function handleRig(args) {
|
|
@@ -229,23 +650,276 @@ function createRigSlashCommands(input) {
|
|
|
229
650
|
}
|
|
230
651
|
if (first === "attach") {
|
|
231
652
|
const run = await input.client.attach(second);
|
|
232
|
-
const runRecord = run
|
|
653
|
+
const runRecord = runRecordFromPayload(run);
|
|
233
654
|
notify(`Attached to ${String(runRecord.runId ?? second ?? input.context.runId ?? "run")}: ${String(runRecord.status ?? "unknown")}`, "info");
|
|
234
655
|
return;
|
|
235
656
|
}
|
|
236
|
-
|
|
657
|
+
if (first === "timeline" || first === "logs") {
|
|
658
|
+
const runId = second || input.context.runId;
|
|
659
|
+
const entries = first === "timeline" ? await input.client.runTimeline(runId, 20) : await input.client.runLogs(runId, 20);
|
|
660
|
+
notify(entries.length > 0 ? entries.slice(-10).map(formatEntry).join(`
|
|
661
|
+
`) : `No ${first} entries for ${runId ?? "run"}.`, "info");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (first === "steer") {
|
|
665
|
+
const message = args.trim().slice("steer".length).trim();
|
|
666
|
+
if (!message) {
|
|
667
|
+
notify("Usage: /rig steer <message>", "error");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
await input.client.steer(message);
|
|
671
|
+
notify("Rig steering message queued.", "info");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (first === "stop") {
|
|
675
|
+
await input.client.stop(second || input.context.runId);
|
|
676
|
+
notify("Rig stop requested.", "info");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (first === "abort") {
|
|
680
|
+
await input.client.workerAbort(second || input.context.runId);
|
|
681
|
+
notify("Worker turn abort requested.", "info");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (first === "sh") {
|
|
685
|
+
const command = args.trim().slice("sh".length).trim();
|
|
686
|
+
if (!command) {
|
|
687
|
+
notify("Usage: /rig sh <command> \u2014 runs in the WORKER workspace", "error");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
await input.client.workerShell(command, input.context.runId);
|
|
691
|
+
notify("Shell command sent to the worker workspace.", "info");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id] | /rig timeline [run-id] | /rig logs [run-id] | /rig steer <message> | /rig stop [run-id] | /rig abort [run-id] | /rig sh <command>", "error");
|
|
237
695
|
} catch (error) {
|
|
238
696
|
notify(error instanceof Error ? error.message : String(error), "error");
|
|
239
697
|
}
|
|
240
698
|
}
|
|
241
699
|
return {
|
|
242
700
|
rig: {
|
|
243
|
-
description: "Rig control-plane commands: status, task list, task run, attach",
|
|
701
|
+
description: "Rig control-plane commands: status, task list, task run, attach, timeline, logs, steer, stop",
|
|
244
702
|
handler: handleRig
|
|
245
703
|
}
|
|
246
704
|
};
|
|
247
705
|
}
|
|
248
706
|
|
|
707
|
+
// packages/pi-rig/src/live-mirror.ts
|
|
708
|
+
async function loadPiModules() {
|
|
709
|
+
const components = await import("@earendil-works/pi-coding-agent");
|
|
710
|
+
return { components };
|
|
711
|
+
}
|
|
712
|
+
function createRootContainer() {
|
|
713
|
+
const children = [];
|
|
714
|
+
return {
|
|
715
|
+
addChild(child) {
|
|
716
|
+
children.push(child);
|
|
717
|
+
},
|
|
718
|
+
removeChild(child) {
|
|
719
|
+
const index = children.indexOf(child);
|
|
720
|
+
if (index >= 0)
|
|
721
|
+
children.splice(index, 1);
|
|
722
|
+
},
|
|
723
|
+
clear() {
|
|
724
|
+
children.length = 0;
|
|
725
|
+
},
|
|
726
|
+
render(width) {
|
|
727
|
+
return children.flatMap((child) => child.render(width));
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
var DRONE_MESSAGE_TYPE = "rig-drone";
|
|
732
|
+
var DRONE_USER_MESSAGE_TYPE = "rig-drone-user";
|
|
733
|
+
function recordOf(value) {
|
|
734
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
735
|
+
}
|
|
736
|
+
function messageRole(event) {
|
|
737
|
+
const message = recordOf(event.message);
|
|
738
|
+
return message && typeof message.role === "string" ? message.role : null;
|
|
739
|
+
}
|
|
740
|
+
function userMessageText(message) {
|
|
741
|
+
const content = message.content;
|
|
742
|
+
if (typeof content === "string")
|
|
743
|
+
return content;
|
|
744
|
+
if (Array.isArray(content)) {
|
|
745
|
+
return content.flatMap((block) => {
|
|
746
|
+
const record = recordOf(block);
|
|
747
|
+
return record && record.type === "text" && typeof record.text === "string" ? [record.text] : [];
|
|
748
|
+
}).join(`
|
|
749
|
+
`);
|
|
750
|
+
}
|
|
751
|
+
return "";
|
|
752
|
+
}
|
|
753
|
+
async function createLiveMirror(input) {
|
|
754
|
+
const { pi } = input;
|
|
755
|
+
const modules = input.modules ?? await loadPiModules();
|
|
756
|
+
const cwd = input.workerCwd ?? process.cwd();
|
|
757
|
+
const turns = new Map;
|
|
758
|
+
const toolOwners = new Map;
|
|
759
|
+
const userComponents = new Map;
|
|
760
|
+
let streamingTurn = null;
|
|
761
|
+
let sequence = 0;
|
|
762
|
+
let tui = null;
|
|
763
|
+
let renderTimer = null;
|
|
764
|
+
const requestRender = () => {
|
|
765
|
+
if (renderTimer)
|
|
766
|
+
return;
|
|
767
|
+
renderTimer = setTimeout(() => {
|
|
768
|
+
renderTimer = null;
|
|
769
|
+
tui?.requestRender?.();
|
|
770
|
+
}, 33);
|
|
771
|
+
renderTimer.unref?.();
|
|
772
|
+
};
|
|
773
|
+
pi.registerMessageRenderer?.(DRONE_MESSAGE_TYPE, (message) => {
|
|
774
|
+
const details = recordOf(recordOf(message)?.details);
|
|
775
|
+
const key = details && typeof details.key === "string" ? details.key : null;
|
|
776
|
+
return key ? turns.get(key)?.root : undefined;
|
|
777
|
+
});
|
|
778
|
+
pi.registerMessageRenderer?.(DRONE_USER_MESSAGE_TYPE, (message) => {
|
|
779
|
+
const details = recordOf(recordOf(message)?.details);
|
|
780
|
+
const key = details && typeof details.key === "string" ? details.key : null;
|
|
781
|
+
return key ? userComponents.get(key) : undefined;
|
|
782
|
+
});
|
|
783
|
+
const ensureToolComponent = (turn, toolCallId, toolName, args) => {
|
|
784
|
+
const existing = turn.tools.get(toolCallId);
|
|
785
|
+
if (existing)
|
|
786
|
+
return existing;
|
|
787
|
+
const component = new modules.components.ToolExecutionComponent(toolName, toolCallId, args, {}, undefined, tui, cwd);
|
|
788
|
+
turn.root.addChild(component);
|
|
789
|
+
turn.tools.set(toolCallId, component);
|
|
790
|
+
toolOwners.set(toolCallId, turn);
|
|
791
|
+
return component;
|
|
792
|
+
};
|
|
793
|
+
const startAssistantTurn = (message) => {
|
|
794
|
+
const key = `turn-${++sequence}`;
|
|
795
|
+
const root = createRootContainer();
|
|
796
|
+
const assistant = new modules.components.AssistantMessageComponent;
|
|
797
|
+
root.addChild(assistant);
|
|
798
|
+
const turn = { root, assistant, tools: new Map };
|
|
799
|
+
assistant.updateContent(message);
|
|
800
|
+
turns.set(key, turn);
|
|
801
|
+
streamingTurn = turn;
|
|
802
|
+
pi.sendMessage?.({ customType: DRONE_MESSAGE_TYPE, content: "drone turn", display: true, details: { key } }, { triggerTurn: false });
|
|
803
|
+
requestRender();
|
|
804
|
+
};
|
|
805
|
+
const updateAssistantTurn = (message) => {
|
|
806
|
+
const turn = streamingTurn;
|
|
807
|
+
if (!turn)
|
|
808
|
+
return;
|
|
809
|
+
turn.assistant.updateContent(message);
|
|
810
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
811
|
+
for (const block of content) {
|
|
812
|
+
const record = recordOf(block);
|
|
813
|
+
if (!record || record.type !== "toolCall")
|
|
814
|
+
continue;
|
|
815
|
+
const id = typeof record.id === "string" ? record.id : null;
|
|
816
|
+
const name = typeof record.name === "string" ? record.name : "tool";
|
|
817
|
+
if (!id)
|
|
818
|
+
continue;
|
|
819
|
+
const component = ensureToolComponent(turn, id, name, record.arguments);
|
|
820
|
+
component.updateArgs(record.arguments);
|
|
821
|
+
}
|
|
822
|
+
requestRender();
|
|
823
|
+
};
|
|
824
|
+
const endAssistantTurn = (message) => {
|
|
825
|
+
const turn = streamingTurn;
|
|
826
|
+
if (!turn)
|
|
827
|
+
return;
|
|
828
|
+
turn.assistant.updateContent(message);
|
|
829
|
+
const stopReason = typeof message.stopReason === "string" ? message.stopReason : "stop";
|
|
830
|
+
if (stopReason === "aborted" || stopReason === "error") {
|
|
831
|
+
const errorText = typeof message.errorMessage === "string" && message.errorMessage ? message.errorMessage : stopReason === "aborted" ? "Operation aborted" : "Error";
|
|
832
|
+
for (const component of turn.tools.values()) {
|
|
833
|
+
component.updateResult({ content: [{ type: "text", text: errorText }], isError: true });
|
|
834
|
+
}
|
|
835
|
+
} else {
|
|
836
|
+
for (const component of turn.tools.values()) {
|
|
837
|
+
component.setArgsComplete();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
streamingTurn = null;
|
|
841
|
+
requestRender();
|
|
842
|
+
};
|
|
843
|
+
const mirrorUserMessage = (message) => {
|
|
844
|
+
const text = userMessageText(message).trim();
|
|
845
|
+
if (!text)
|
|
846
|
+
return;
|
|
847
|
+
const key = `user-${++sequence}`;
|
|
848
|
+
userComponents.set(key, new modules.components.UserMessageComponent(text));
|
|
849
|
+
pi.sendMessage?.({ customType: DRONE_USER_MESSAGE_TYPE, content: text, display: true, details: { key } }, { triggerTurn: false });
|
|
850
|
+
requestRender();
|
|
851
|
+
};
|
|
852
|
+
return {
|
|
853
|
+
captureTui(capturedTui) {
|
|
854
|
+
tui = recordOf(capturedTui);
|
|
855
|
+
return { render: () => [] };
|
|
856
|
+
},
|
|
857
|
+
handleWorkerEvent(event) {
|
|
858
|
+
const type = typeof event.type === "string" ? event.type : null;
|
|
859
|
+
switch (type) {
|
|
860
|
+
case "message_start": {
|
|
861
|
+
const message = recordOf(event.message);
|
|
862
|
+
if (!message)
|
|
863
|
+
return;
|
|
864
|
+
if (messageRole(event) === "assistant")
|
|
865
|
+
startAssistantTurn(message);
|
|
866
|
+
else if (messageRole(event) === "user")
|
|
867
|
+
mirrorUserMessage(message);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
case "message_update": {
|
|
871
|
+
const message = recordOf(event.message);
|
|
872
|
+
if (message && messageRole(event) === "assistant")
|
|
873
|
+
updateAssistantTurn(message);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
case "message_end": {
|
|
877
|
+
const message = recordOf(event.message);
|
|
878
|
+
if (message && messageRole(event) === "assistant")
|
|
879
|
+
endAssistantTurn(message);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
case "tool_execution_start": {
|
|
883
|
+
const id = typeof event.toolCallId === "string" ? event.toolCallId : null;
|
|
884
|
+
const name = typeof event.toolName === "string" ? event.toolName : "tool";
|
|
885
|
+
if (!id)
|
|
886
|
+
return;
|
|
887
|
+
const turn = toolOwners.get(id) ?? streamingTurn;
|
|
888
|
+
if (!turn)
|
|
889
|
+
return;
|
|
890
|
+
ensureToolComponent(turn, id, name, event.args).markExecutionStarted();
|
|
891
|
+
requestRender();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
case "tool_execution_update": {
|
|
895
|
+
const id = typeof event.toolCallId === "string" ? event.toolCallId : null;
|
|
896
|
+
const component = id ? toolOwners.get(id)?.tools.get(id) : undefined;
|
|
897
|
+
const partial = recordOf(event.partialResult);
|
|
898
|
+
if (component && partial) {
|
|
899
|
+
const content = Array.isArray(partial.content) ? partial.content : [];
|
|
900
|
+
component.updateResult({ ...partial, content, isError: false }, true);
|
|
901
|
+
requestRender();
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
case "tool_execution_end": {
|
|
906
|
+
const id = typeof event.toolCallId === "string" ? event.toolCallId : null;
|
|
907
|
+
const component = id ? toolOwners.get(id)?.tools.get(id) : undefined;
|
|
908
|
+
const result = recordOf(event.result);
|
|
909
|
+
if (component && result) {
|
|
910
|
+
const content = Array.isArray(result.content) ? result.content : [];
|
|
911
|
+
component.updateResult({ ...result, content, isError: event.isError === true });
|
|
912
|
+
requestRender();
|
|
913
|
+
}
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
default:
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
249
923
|
// packages/pi-rig/src/tools.ts
|
|
250
924
|
function textResult(text, details) {
|
|
251
925
|
return { content: [{ type: "text", text }], ...details ? { details } : {} };
|
|
@@ -308,7 +982,8 @@ function createPiRigExtensionState(input = {}) {
|
|
|
308
982
|
const context = createRigContextFromEnv(input.env ?? process.env);
|
|
309
983
|
return {
|
|
310
984
|
...context,
|
|
311
|
-
client: new RigBridgeClient({ context, fetchImpl: input.fetchImpl })
|
|
985
|
+
client: new RigBridgeClient({ context, fetchImpl: input.fetchImpl }),
|
|
986
|
+
...input.webSocketFactory ? { webSocketFactory: input.webSocketFactory } : {}
|
|
312
987
|
};
|
|
313
988
|
}
|
|
314
989
|
function notify(ctx, message, level = "info") {
|
|
@@ -318,6 +993,91 @@ function notify(ctx, message, level = "info") {
|
|
|
318
993
|
notifyFn.call(ui, message, level);
|
|
319
994
|
}
|
|
320
995
|
}
|
|
996
|
+
function canNotify(ctx) {
|
|
997
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
998
|
+
return Boolean(ui && typeof ui === "object" && typeof ui.notify === "function");
|
|
999
|
+
}
|
|
1000
|
+
function setWidget(ctx, id, lines) {
|
|
1001
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
1002
|
+
const setWidgetFn = ui && typeof ui === "object" ? ui.setWidget : null;
|
|
1003
|
+
if (typeof setWidgetFn === "function") {
|
|
1004
|
+
setWidgetFn.call(ui, id, lines);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function setStatus(ctx, id, text) {
|
|
1008
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
1009
|
+
const setStatusFn = ui && typeof ui === "object" ? ui.setStatus : null;
|
|
1010
|
+
if (typeof setStatusFn === "function") {
|
|
1011
|
+
setStatusFn.call(ui, id, text);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function uiOf(ctx) {
|
|
1015
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
1016
|
+
return ui && typeof ui === "object" ? ui : null;
|
|
1017
|
+
}
|
|
1018
|
+
function setTitle(ctx, title) {
|
|
1019
|
+
const ui = uiOf(ctx);
|
|
1020
|
+
const setTitleFn = ui?.setTitle;
|
|
1021
|
+
if (typeof setTitleFn === "function")
|
|
1022
|
+
setTitleFn.call(ui, title);
|
|
1023
|
+
}
|
|
1024
|
+
function shutdownPi(ctx) {
|
|
1025
|
+
const shutdownFn = ctx && typeof ctx === "object" ? ctx.shutdown : null;
|
|
1026
|
+
if (typeof shutdownFn === "function")
|
|
1027
|
+
shutdownFn.call(ctx);
|
|
1028
|
+
}
|
|
1029
|
+
async function askNativeDialog(ctx, request) {
|
|
1030
|
+
const ui = uiOf(ctx);
|
|
1031
|
+
if (!ui)
|
|
1032
|
+
return null;
|
|
1033
|
+
const { method, prompt, choices } = request;
|
|
1034
|
+
try {
|
|
1035
|
+
if (method === "confirm" && typeof ui.confirm === "function") {
|
|
1036
|
+
const confirmed = await ui.confirm("Worker request", prompt);
|
|
1037
|
+
return { value: confirmed, confirmed };
|
|
1038
|
+
}
|
|
1039
|
+
if (choices.length > 0 && typeof ui.select === "function") {
|
|
1040
|
+
const selected = await ui.select(prompt, choices);
|
|
1041
|
+
return selected === undefined ? { cancelled: true } : { value: selected };
|
|
1042
|
+
}
|
|
1043
|
+
if (typeof ui.input === "function") {
|
|
1044
|
+
const value = await ui.input("Worker request", prompt);
|
|
1045
|
+
return value === undefined ? { cancelled: true } : { value };
|
|
1046
|
+
}
|
|
1047
|
+
} catch {
|
|
1048
|
+
return { cancelled: true };
|
|
1049
|
+
}
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
function shortPath(path, segments = 3) {
|
|
1053
|
+
const parts = path.split("/").filter(Boolean);
|
|
1054
|
+
if (parts.length <= segments)
|
|
1055
|
+
return path;
|
|
1056
|
+
return `\u2026/${parts.slice(-segments).join("/")}`;
|
|
1057
|
+
}
|
|
1058
|
+
function createBridgeGate(state) {
|
|
1059
|
+
let pending = null;
|
|
1060
|
+
let warned = false;
|
|
1061
|
+
return async (ctx) => {
|
|
1062
|
+
if (!state.active || !state.serverUrl)
|
|
1063
|
+
return { allowed: true, message: null, status: "indeterminate" };
|
|
1064
|
+
pending ??= state.client.checkProtocolCompatibility().then((check2) => {
|
|
1065
|
+
if (check2.status === "indeterminate")
|
|
1066
|
+
pending = null;
|
|
1067
|
+
return check2;
|
|
1068
|
+
});
|
|
1069
|
+
const check = await pending;
|
|
1070
|
+
if (check.status !== "mismatch")
|
|
1071
|
+
return { allowed: true, message: null, status: check.status };
|
|
1072
|
+
const message = check.message ?? "Rig protocol mismatch \u2014 the Rig bridge is disabled.";
|
|
1073
|
+
if (!warned && canNotify(ctx)) {
|
|
1074
|
+
warned = true;
|
|
1075
|
+
notify(ctx, message, "error");
|
|
1076
|
+
setStatus(ctx, "rig", "Rig bridge disabled (protocol mismatch)");
|
|
1077
|
+
}
|
|
1078
|
+
return { allowed: false, message, status: "mismatch" };
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
321
1081
|
function steeringText(message) {
|
|
322
1082
|
const text = typeof message.message === "string" ? message.message.trim() : "";
|
|
323
1083
|
if (!text)
|
|
@@ -326,54 +1086,525 @@ function steeringText(message) {
|
|
|
326
1086
|
return `[Rig steering from ${actor}]
|
|
327
1087
|
${text}`;
|
|
328
1088
|
}
|
|
329
|
-
|
|
330
|
-
if (
|
|
1089
|
+
function unrefTimer(timer) {
|
|
1090
|
+
if (typeof timer.unref === "function") {
|
|
1091
|
+
timer.unref();
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
var STEERING_DEDUPE_LIMIT = 500;
|
|
1095
|
+
function rememberDeliveredSteeringId(deliveredIds, id) {
|
|
1096
|
+
deliveredIds.add(id);
|
|
1097
|
+
if (deliveredIds.size > STEERING_DEDUPE_LIMIT) {
|
|
1098
|
+
const oldest = deliveredIds.values().next().value;
|
|
1099
|
+
if (typeof oldest === "string")
|
|
1100
|
+
deliveredIds.delete(oldest);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
async function deliverSteeringMessage(pi, deliveredIds, message) {
|
|
1104
|
+
if (typeof pi.sendUserMessage !== "function")
|
|
1105
|
+
return false;
|
|
1106
|
+
const id = typeof message.id === "string" && message.id.trim() ? message.id : null;
|
|
1107
|
+
if (id && deliveredIds.has(id))
|
|
1108
|
+
return false;
|
|
1109
|
+
const text = steeringText(message);
|
|
1110
|
+
if (!text)
|
|
1111
|
+
return false;
|
|
1112
|
+
if (id)
|
|
1113
|
+
rememberDeliveredSteeringId(deliveredIds, id);
|
|
1114
|
+
await pi.sendUserMessage(text, { deliverAs: "steer", triggerTurn: true });
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
async function consumeQueuedSteering(pi, state, ctx, gate, deliveredIds) {
|
|
1118
|
+
if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
|
|
1119
|
+
return;
|
|
1120
|
+
if (!(await gate(ctx)).allowed)
|
|
331
1121
|
return;
|
|
332
1122
|
try {
|
|
333
1123
|
const messages = await state.client.consumeSteering(state.runId);
|
|
1124
|
+
let deliveredCount = 0;
|
|
334
1125
|
for (const message of messages) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
continue;
|
|
338
|
-
await pi.sendUserMessage(text, { deliverAs: "steer", triggerTurn: true });
|
|
1126
|
+
if (await deliverSteeringMessage(pi, deliveredIds, message))
|
|
1127
|
+
deliveredCount += 1;
|
|
339
1128
|
}
|
|
340
|
-
if (
|
|
341
|
-
notify(ctx, `Delivered ${
|
|
1129
|
+
if (deliveredCount > 0) {
|
|
1130
|
+
notify(ctx, `Delivered ${deliveredCount} Rig steering message${deliveredCount === 1 ? "" : "s"}.`);
|
|
342
1131
|
}
|
|
343
1132
|
} catch (error) {
|
|
344
1133
|
notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
345
1134
|
}
|
|
346
1135
|
}
|
|
347
|
-
function
|
|
348
|
-
if (!
|
|
1136
|
+
function inputText(event) {
|
|
1137
|
+
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
1138
|
+
return null;
|
|
1139
|
+
const text = event.text;
|
|
1140
|
+
return typeof text === "string" && text.trim() ? text.trim() : null;
|
|
1141
|
+
}
|
|
1142
|
+
async function handleOperatorInput(event, state, ctx, gate) {
|
|
1143
|
+
if (!state.operatorSession || !state.active || !state.runId)
|
|
1144
|
+
return;
|
|
1145
|
+
const text = inputText(event);
|
|
1146
|
+
if (!text || text.startsWith("/"))
|
|
1147
|
+
return;
|
|
1148
|
+
if (!(await gate(ctx)).allowed)
|
|
1149
|
+
return;
|
|
1150
|
+
if (text.startsWith("!")) {
|
|
1151
|
+
const command = text.slice(1).trim();
|
|
1152
|
+
if (!command)
|
|
1153
|
+
return;
|
|
1154
|
+
try {
|
|
1155
|
+
await state.client.workerShell(command, state.runId);
|
|
1156
|
+
notify(ctx, "Shell command sent to the worker workspace.");
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
notify(ctx, `Worker shell failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1159
|
+
}
|
|
1160
|
+
return { action: "handled" };
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
await state.client.steer(text, state.runId);
|
|
1164
|
+
notify(ctx, "Rig steering message queued.");
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
notify(ctx, `Rig steering failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1167
|
+
}
|
|
1168
|
+
return { action: "handled" };
|
|
1169
|
+
}
|
|
1170
|
+
function runLocation(run) {
|
|
1171
|
+
const worktree = typeof run.worktreePath === "string" && run.worktreePath.trim() ? run.worktreePath.trim() : null;
|
|
1172
|
+
const projectRoot = typeof run.projectRoot === "string" && run.projectRoot.trim() ? run.projectRoot.trim() : null;
|
|
1173
|
+
return worktree ?? projectRoot ?? "remote/local worker workspace";
|
|
1174
|
+
}
|
|
1175
|
+
function runPayload(payload) {
|
|
1176
|
+
return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
1177
|
+
}
|
|
1178
|
+
var OPERATOR_WIDGET_WS_FALLBACK_MS = 1e4;
|
|
1179
|
+
function startOperatorRunStatusLine(state, ctx, live) {
|
|
1180
|
+
if (!state.operatorSession || !state.active || !state.runId)
|
|
1181
|
+
return;
|
|
1182
|
+
const shortId = state.runId.slice(0, 8);
|
|
1183
|
+
let inFlight = false;
|
|
1184
|
+
let lastRefreshAt = 0;
|
|
1185
|
+
const refresh = async () => {
|
|
1186
|
+
if (inFlight)
|
|
1187
|
+
return;
|
|
1188
|
+
inFlight = true;
|
|
1189
|
+
lastRefreshAt = Date.now();
|
|
1190
|
+
try {
|
|
1191
|
+
const run = runPayload(await state.client.attach(state.runId));
|
|
1192
|
+
const status = String(run.status ?? "unknown");
|
|
1193
|
+
setStatus(ctx, "rig", `drone ${shortId} \xB7 ${status} \xB7 ${shortPath(runLocation(run))}`);
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
setStatus(ctx, "rig", `drone ${shortId} \xB7 server unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1196
|
+
} finally {
|
|
1197
|
+
inFlight = false;
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
refresh();
|
|
1201
|
+
const timer = setInterval(() => {
|
|
1202
|
+
const triggered = live?.consumePushTrigger() ?? false;
|
|
1203
|
+
if ((live?.isConnected() ?? false) && !triggered && Date.now() - lastRefreshAt < OPERATOR_WIDGET_WS_FALLBACK_MS) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
refresh();
|
|
1207
|
+
}, 5000);
|
|
1208
|
+
unrefTimer(timer);
|
|
1209
|
+
}
|
|
1210
|
+
function operatorInboxNotification(event) {
|
|
1211
|
+
const type = typeof event.type === "string" ? event.type : null;
|
|
1212
|
+
if (type !== "rig.approval.requested" && type !== "rig.user-input.requested")
|
|
1213
|
+
return null;
|
|
1214
|
+
const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
|
|
1215
|
+
const runId = typeof payload.runId === "string" && payload.runId.trim() ? payload.runId : typeof event.aggregateId === "string" && event.aggregateId.trim() ? event.aggregateId : "unknown";
|
|
1216
|
+
const requestId = typeof payload.requestId === "string" && payload.requestId.trim() ? payload.requestId : `${type}:${runId}`;
|
|
1217
|
+
const waitingOn = type === "rig.approval.requested" ? "an approval" : "user input";
|
|
1218
|
+
return {
|
|
1219
|
+
key: requestId,
|
|
1220
|
+
text: `Run ${runId} is waiting on ${waitingOn} \u2014 resolve with /rig inbox or \`rig inbox\`.`
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
function startOperatorBridge(state, ctx) {
|
|
1224
|
+
if (!state.operatorSession || !state.active)
|
|
1225
|
+
return;
|
|
1226
|
+
const notifiedRequests = new Set;
|
|
1227
|
+
let pushTrigger = false;
|
|
1228
|
+
const socket = new RigBridgeSocket({
|
|
1229
|
+
context: state,
|
|
1230
|
+
webSocketFactory: state.webSocketFactory,
|
|
1231
|
+
handlers: {
|
|
1232
|
+
onRigEvent: (event) => {
|
|
1233
|
+
pushTrigger = true;
|
|
1234
|
+
const notification = operatorInboxNotification(event);
|
|
1235
|
+
if (!notification || notifiedRequests.has(notification.key))
|
|
1236
|
+
return;
|
|
1237
|
+
notifiedRequests.add(notification.key);
|
|
1238
|
+
if (notifiedRequests.size > 1000) {
|
|
1239
|
+
const oldest = notifiedRequests.values().next().value;
|
|
1240
|
+
if (typeof oldest === "string")
|
|
1241
|
+
notifiedRequests.delete(oldest);
|
|
1242
|
+
}
|
|
1243
|
+
notify(ctx, notification.text);
|
|
1244
|
+
},
|
|
1245
|
+
onSnapshotInvalidated: () => {
|
|
1246
|
+
pushTrigger = true;
|
|
1247
|
+
},
|
|
1248
|
+
onDisconnect: () => {
|
|
1249
|
+
pushTrigger = true;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
if (!socket.start())
|
|
1254
|
+
return;
|
|
1255
|
+
return {
|
|
1256
|
+
isConnected: () => socket.connected,
|
|
1257
|
+
consumePushTrigger: () => {
|
|
1258
|
+
const triggered = pushTrigger;
|
|
1259
|
+
pushTrigger = false;
|
|
1260
|
+
return triggered;
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function workerStatusLine(status) {
|
|
1265
|
+
const model = status.model && typeof status.model === "object" && !Array.isArray(status.model) ? status.model : null;
|
|
1266
|
+
const modelId = model && typeof model.id === "string" ? model.id : null;
|
|
1267
|
+
const usage = status.contextUsage && typeof status.contextUsage === "object" && !Array.isArray(status.contextUsage) ? status.contextUsage : null;
|
|
1268
|
+
const percent = usage && typeof usage.percent === "number" ? `${Math.round(usage.percent)}% ctx` : null;
|
|
1269
|
+
const streaming = status.isStreaming === true ? "streaming" : null;
|
|
1270
|
+
const parts = [modelId, percent, streaming].filter((value) => Boolean(value));
|
|
1271
|
+
return parts.length > 0 ? `worker ${parts.join(" \xB7 ")}` : "worker session connected";
|
|
1272
|
+
}
|
|
1273
|
+
function applyRigTheme(ctx) {
|
|
1274
|
+
const ui = uiOf(ctx);
|
|
1275
|
+
if (!ui || typeof ui.getTheme !== "function" || typeof ui.setTheme !== "function")
|
|
1276
|
+
return;
|
|
1277
|
+
try {
|
|
1278
|
+
const theme = ui.getTheme("rig");
|
|
1279
|
+
if (theme)
|
|
1280
|
+
ui.setTheme(theme);
|
|
1281
|
+
} catch {}
|
|
1282
|
+
}
|
|
1283
|
+
var MICRO_DRONE_BLADES = ["---", "\\\\\\", "|||", "///"];
|
|
1284
|
+
var MICRO_DRONE_EYES = ["@", "o", "."];
|
|
1285
|
+
function applyDroneWorkingIndicator(ctx) {
|
|
1286
|
+
const ui = uiOf(ctx);
|
|
1287
|
+
if (!ui || typeof ui.setWorkingIndicator !== "function")
|
|
1288
|
+
return;
|
|
1289
|
+
const frames = Array.from({ length: 12 }, (_, index) => {
|
|
1290
|
+
const blade = MICRO_DRONE_BLADES[index % MICRO_DRONE_BLADES.length];
|
|
1291
|
+
const eye = MICRO_DRONE_EYES[Math.floor(index / 2) % MICRO_DRONE_EYES.length];
|
|
1292
|
+
return `(${blade})${eye}(${blade})`;
|
|
1293
|
+
});
|
|
1294
|
+
try {
|
|
1295
|
+
ui.setWorkingIndicator({ frames, intervalMs: 120 });
|
|
1296
|
+
} catch {}
|
|
1297
|
+
}
|
|
1298
|
+
function renderWorkerCapabilities(capabilities) {
|
|
1299
|
+
const names = (value, key = "name") => Array.isArray(value) ? value.flatMap((entry) => {
|
|
1300
|
+
if (typeof entry === "string")
|
|
1301
|
+
return [entry];
|
|
1302
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
1303
|
+
const name = entry[key];
|
|
1304
|
+
return typeof name === "string" ? [name] : [];
|
|
1305
|
+
}
|
|
1306
|
+
return [];
|
|
1307
|
+
}) : [];
|
|
1308
|
+
const lines = ["Drone capabilities (in effect for this run)"];
|
|
1309
|
+
const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
|
|
1310
|
+
const activeTools = tools.flatMap((tool) => {
|
|
1311
|
+
const record = tool && typeof tool === "object" && !Array.isArray(tool) ? tool : null;
|
|
1312
|
+
return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
|
|
1313
|
+
});
|
|
1314
|
+
const inactiveCount = tools.length - activeTools.length;
|
|
1315
|
+
lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
|
|
1316
|
+
const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
|
|
1317
|
+
const extensionLabels = extensions.flatMap((entry) => {
|
|
1318
|
+
const record = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
|
|
1319
|
+
if (!record)
|
|
1320
|
+
return [];
|
|
1321
|
+
const path = typeof record.path === "string" ? record.path : "";
|
|
1322
|
+
const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
|
|
1323
|
+
return [short];
|
|
1324
|
+
});
|
|
1325
|
+
lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
|
|
1326
|
+
lines.push(` hooks ${names(capabilities.hookEvents).join(", ") || "(none)"}`);
|
|
1327
|
+
lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
|
|
1328
|
+
lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
|
|
1329
|
+
const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
|
|
1330
|
+
const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
|
|
1331
|
+
lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
|
|
1332
|
+
if (typeof capabilities.cwd === "string" && capabilities.cwd)
|
|
1333
|
+
lines.push(` cwd ${capabilities.cwd}`);
|
|
1334
|
+
lines.push(" (drone commands are in your palette \xB7 /worker toggles this panel)");
|
|
1335
|
+
return lines;
|
|
1336
|
+
}
|
|
1337
|
+
function registerOperatorConsoleCommands(pi, state, tryRegister) {
|
|
1338
|
+
if (typeof pi.registerCommand !== "function")
|
|
1339
|
+
return;
|
|
1340
|
+
let capabilitiesShown = false;
|
|
1341
|
+
tryRegister("command:detach", () => pi.registerCommand?.("detach", {
|
|
1342
|
+
description: "Detach from this run; the drone keeps flying",
|
|
1343
|
+
handler: async (_args, ctx) => {
|
|
1344
|
+
notify(ctx, "Detached. The drone continues on the worker.");
|
|
1345
|
+
shutdownPi(ctx);
|
|
1346
|
+
}
|
|
1347
|
+
}));
|
|
1348
|
+
tryRegister("command:stop", () => pi.registerCommand?.("stop", {
|
|
1349
|
+
description: "Abort the drone's current turn and detach",
|
|
1350
|
+
handler: async (_args, ctx) => {
|
|
1351
|
+
try {
|
|
1352
|
+
await state.client.workerAbort(state.runId);
|
|
1353
|
+
notify(ctx, "Abort requested; detaching.");
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
notify(ctx, `Abort failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1356
|
+
}
|
|
1357
|
+
shutdownPi(ctx);
|
|
1358
|
+
}
|
|
1359
|
+
}));
|
|
1360
|
+
tryRegister("command:worker", () => pi.registerCommand?.("worker", {
|
|
1361
|
+
description: "Toggle the drone's capability panel (tools, extensions, hooks, skills, model)",
|
|
1362
|
+
handler: async (_args, ctx) => {
|
|
1363
|
+
if (capabilitiesShown) {
|
|
1364
|
+
const ui = uiOf(ctx);
|
|
1365
|
+
if (ui && typeof ui.setWidget === "function")
|
|
1366
|
+
ui.setWidget("rig-worker-capabilities", undefined);
|
|
1367
|
+
capabilitiesShown = false;
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
try {
|
|
1371
|
+
const capabilities = await state.client.workerCapabilities(state.runId);
|
|
1372
|
+
setWidget(ctx, "rig-worker-capabilities", renderWorkerCapabilities(capabilities));
|
|
1373
|
+
capabilitiesShown = true;
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
notify(ctx, `Drone capabilities unavailable: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}));
|
|
1379
|
+
}
|
|
1380
|
+
function startWorkerSessionMirror(pi, state, ctx) {
|
|
1381
|
+
if (!state.operatorSession || !state.active || !state.runId)
|
|
1382
|
+
return;
|
|
1383
|
+
let mirror = null;
|
|
1384
|
+
const pendingEvents = [];
|
|
1385
|
+
createLiveMirror({ pi }).then((created) => {
|
|
1386
|
+
mirror = created;
|
|
1387
|
+
const ui = uiOf(ctx);
|
|
1388
|
+
if (ui && typeof ui.setWidget === "function") {
|
|
1389
|
+
ui.setWidget("rig-tui-capture", (tui) => created.captureTui(tui));
|
|
1390
|
+
}
|
|
1391
|
+
for (const event of pendingEvents.splice(0))
|
|
1392
|
+
created.handleWorkerEvent(event);
|
|
1393
|
+
}).catch((error) => {
|
|
1394
|
+
notify(ctx, `Live drone transcript unavailable: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1395
|
+
});
|
|
1396
|
+
const socket = new RigWorkerEventsSocket({
|
|
1397
|
+
context: state,
|
|
1398
|
+
webSocketFactory: state.webSocketFactory,
|
|
1399
|
+
handlers: {
|
|
1400
|
+
onFrame: (frame) => {
|
|
1401
|
+
if (frame.type === "status.update") {
|
|
1402
|
+
const status = frame.status && typeof frame.status === "object" && !Array.isArray(frame.status) ? frame.status : null;
|
|
1403
|
+
if (status)
|
|
1404
|
+
setStatus(ctx, "rig-worker", workerStatusLine(status));
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (frame.type !== "pi.event")
|
|
1408
|
+
return;
|
|
1409
|
+
const event = frame.event && typeof frame.event === "object" && !Array.isArray(frame.event) ? frame.event : null;
|
|
1410
|
+
if (!event)
|
|
1411
|
+
return;
|
|
1412
|
+
if (event.type === "extension_ui_request") {
|
|
1413
|
+
forwardWorkerUiRequest(state, ctx, event);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
if (mirror)
|
|
1417
|
+
mirror.handleWorkerEvent(event);
|
|
1418
|
+
else
|
|
1419
|
+
pendingEvents.push(event);
|
|
1420
|
+
},
|
|
1421
|
+
onConnect: () => {
|
|
1422
|
+
setStatus(ctx, "rig-worker", "drone link live");
|
|
1423
|
+
},
|
|
1424
|
+
onDisconnect: () => {
|
|
1425
|
+
setStatus(ctx, "rig-worker", "drone link down (reconnecting\u2026)");
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
socket.start();
|
|
1430
|
+
}
|
|
1431
|
+
async function forwardWorkerUiRequest(state, ctx, event) {
|
|
1432
|
+
const request = event.request && typeof event.request === "object" && !Array.isArray(event.request) ? event.request : event;
|
|
1433
|
+
const requestId = String(request.requestId ?? request.id ?? `ui-${state.runId}-${event.timestamp ?? ""}`);
|
|
1434
|
+
const method = String(request.method ?? request.type ?? "input");
|
|
1435
|
+
const prompt = typeof request.prompt === "string" && request.prompt.trim() ? request.prompt : typeof request.message === "string" && request.message.trim() ? request.message : typeof request.title === "string" && request.title.trim() ? request.title : method;
|
|
1436
|
+
const rawChoices = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
|
|
1437
|
+
const choices = rawChoices.map((option) => {
|
|
1438
|
+
if (typeof option === "string")
|
|
1439
|
+
return option;
|
|
1440
|
+
if (option && typeof option === "object" && !Array.isArray(option)) {
|
|
1441
|
+
const record = option;
|
|
1442
|
+
return String(record.label ?? record.value ?? record.name ?? "");
|
|
1443
|
+
}
|
|
1444
|
+
return "";
|
|
1445
|
+
}).filter(Boolean);
|
|
1446
|
+
const answer = await askNativeDialog(ctx, { method, prompt, choices });
|
|
1447
|
+
if (!answer)
|
|
1448
|
+
return;
|
|
1449
|
+
try {
|
|
1450
|
+
await state.client.workerRespondExtensionUi(requestId, answer, state.runId);
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
notify(ctx, `Failed to answer the drone's question: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async function registerWorkerCommands(pi, state, ctx, registeredNames = new Set) {
|
|
1456
|
+
if (!state.operatorSession || !state.active || !state.runId)
|
|
1457
|
+
return false;
|
|
1458
|
+
if (typeof pi.registerCommand !== "function")
|
|
1459
|
+
return false;
|
|
1460
|
+
let commands = [];
|
|
1461
|
+
try {
|
|
1462
|
+
commands = await state.client.workerCommands(state.runId);
|
|
1463
|
+
} catch {
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
let registered = 0;
|
|
1467
|
+
for (const command of commands) {
|
|
1468
|
+
const name = typeof command.name === "string" && command.name.trim() ? command.name.trim() : null;
|
|
1469
|
+
if (!name || name === "rig" || registeredNames.has(name))
|
|
1470
|
+
continue;
|
|
1471
|
+
const description = typeof command.description === "string" && command.description.trim() ? `[worker] ${command.description.trim()}` : "[worker] remote session command";
|
|
1472
|
+
try {
|
|
1473
|
+
pi.registerCommand(name, {
|
|
1474
|
+
description,
|
|
1475
|
+
handler: async (args, commandCtx) => {
|
|
1476
|
+
try {
|
|
1477
|
+
await state.client.workerRunCommand(name, args, state.runId);
|
|
1478
|
+
notify(commandCtx, `Sent /${name} to the worker session.`);
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
notify(commandCtx, `Worker /${name} failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
registeredNames.add(name);
|
|
1485
|
+
registered += 1;
|
|
1486
|
+
} catch {
|
|
1487
|
+
registeredNames.add(name);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (registered > 0) {
|
|
1491
|
+
notify(ctx, `Worker session commands available: ${registered} registered from the run's Pi session.`);
|
|
1492
|
+
}
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1495
|
+
function startWorkerCommandRegistration(pi, state, ctx) {
|
|
1496
|
+
const registeredNames = new Set;
|
|
1497
|
+
let attempts = 0;
|
|
1498
|
+
let inFlight = false;
|
|
1499
|
+
const attempt = async () => {
|
|
1500
|
+
if (inFlight)
|
|
1501
|
+
return false;
|
|
1502
|
+
inFlight = true;
|
|
1503
|
+
attempts += 1;
|
|
1504
|
+
try {
|
|
1505
|
+
return await registerWorkerCommands(pi, state, ctx, registeredNames);
|
|
1506
|
+
} finally {
|
|
1507
|
+
inFlight = false;
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
attempt().then((ready) => {
|
|
1511
|
+
if (ready)
|
|
1512
|
+
return;
|
|
1513
|
+
const timer = setInterval(() => {
|
|
1514
|
+
if (attempts >= 60) {
|
|
1515
|
+
clearInterval(timer);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
attempt().then((nextReady) => {
|
|
1519
|
+
if (nextReady)
|
|
1520
|
+
clearInterval(timer);
|
|
1521
|
+
});
|
|
1522
|
+
}, 2000);
|
|
1523
|
+
unrefTimer(timer);
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
function startSteeringBridge(pi, state, ctx, gate, deliveredIds) {
|
|
1527
|
+
if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
|
|
349
1528
|
return;
|
|
1529
|
+
const runId = state.runId;
|
|
1530
|
+
const socket = new RigBridgeSocket({
|
|
1531
|
+
context: state,
|
|
1532
|
+
webSocketFactory: state.webSocketFactory,
|
|
1533
|
+
handlers: {
|
|
1534
|
+
onSteeringMessage: (message) => {
|
|
1535
|
+
(async () => {
|
|
1536
|
+
try {
|
|
1537
|
+
if (!await deliverSteeringMessage(pi, deliveredIds, message))
|
|
1538
|
+
return;
|
|
1539
|
+
const id = typeof message.id === "string" && message.id.trim() ? message.id : null;
|
|
1540
|
+
if (id)
|
|
1541
|
+
socket.ackSteering(runId, [id]);
|
|
1542
|
+
notify(ctx, "Delivered 1 Rig steering message.");
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1545
|
+
}
|
|
1546
|
+
})();
|
|
1547
|
+
},
|
|
1548
|
+
onConnect: () => {
|
|
1549
|
+
consumeQueuedSteering(pi, state, ctx, gate, deliveredIds);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
(async () => {
|
|
1554
|
+
const gateResult = await gate(ctx);
|
|
1555
|
+
if (!gateResult.allowed)
|
|
1556
|
+
return;
|
|
1557
|
+
if (gateResult.status === "compatible") {
|
|
1558
|
+
socket.start();
|
|
1559
|
+
}
|
|
1560
|
+
})();
|
|
350
1561
|
const intervalMs = state.steeringPollMs ?? 1000;
|
|
351
1562
|
if (intervalMs <= 0)
|
|
352
1563
|
return;
|
|
1564
|
+
const WS_CONNECTED_SWEEP_MS = 1e4;
|
|
353
1565
|
let inFlight = false;
|
|
1566
|
+
let lastSweepAt = 0;
|
|
354
1567
|
const timer = setInterval(() => {
|
|
355
1568
|
if (inFlight)
|
|
356
1569
|
return;
|
|
1570
|
+
if (socket.connected && Date.now() - lastSweepAt < WS_CONNECTED_SWEEP_MS)
|
|
1571
|
+
return;
|
|
357
1572
|
inFlight = true;
|
|
358
|
-
|
|
1573
|
+
lastSweepAt = Date.now();
|
|
1574
|
+
consumeQueuedSteering(pi, state, ctx, gate, deliveredIds).finally(() => {
|
|
359
1575
|
inFlight = false;
|
|
360
1576
|
});
|
|
361
1577
|
}, intervalMs);
|
|
362
|
-
|
|
363
|
-
timer.unref();
|
|
364
|
-
}
|
|
1578
|
+
unrefTimer(timer);
|
|
365
1579
|
}
|
|
366
1580
|
function createPiRigExtension(pi, options = {}) {
|
|
367
1581
|
const state = options.state ?? createPiRigExtensionState();
|
|
1582
|
+
const gate = createBridgeGate(state);
|
|
1583
|
+
const deliveredSteeringIds = new Set;
|
|
368
1584
|
const commands = createRigSlashCommands({
|
|
369
1585
|
context: state,
|
|
370
1586
|
client: state.client,
|
|
371
1587
|
notify: (message, level) => notify(globalThis, message, level)
|
|
372
1588
|
});
|
|
1589
|
+
const tryRegister = (label, register) => {
|
|
1590
|
+
try {
|
|
1591
|
+
register();
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1594
|
+
if (/conflict|already|duplicate/i.test(message))
|
|
1595
|
+
return;
|
|
1596
|
+
throw error;
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
373
1599
|
for (const [name, command] of Object.entries(commands)) {
|
|
374
|
-
pi.registerCommand?.(name, {
|
|
1600
|
+
tryRegister(`command:${name}`, () => pi.registerCommand?.(name, {
|
|
375
1601
|
description: command.description,
|
|
376
1602
|
handler: async (args, ctx) => {
|
|
1603
|
+
const gateResult = await gate(ctx);
|
|
1604
|
+
if (!gateResult.allowed) {
|
|
1605
|
+
notify(ctx, gateResult.message ?? "Rig bridge disabled (protocol mismatch).", "error");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
377
1608
|
const nextCommands = createRigSlashCommands({
|
|
378
1609
|
context: state,
|
|
379
1610
|
client: state.client,
|
|
@@ -381,26 +1612,53 @@ function createPiRigExtension(pi, options = {}) {
|
|
|
381
1612
|
});
|
|
382
1613
|
await nextCommands.rig.handler(args, ctx);
|
|
383
1614
|
}
|
|
384
|
-
});
|
|
1615
|
+
}));
|
|
1616
|
+
}
|
|
1617
|
+
if (state.operatorSession && state.active && state.runId) {
|
|
1618
|
+
registerOperatorConsoleCommands(pi, state, tryRegister);
|
|
385
1619
|
}
|
|
386
1620
|
if (state.active && state.runId) {
|
|
387
1621
|
for (const tool of createRigTools({ context: state, client: state.client })) {
|
|
388
|
-
pi.registerTool?.(
|
|
1622
|
+
tryRegister(`tool:${String(tool.name ?? "rig-tool")}`, () => pi.registerTool?.({
|
|
1623
|
+
...tool,
|
|
1624
|
+
execute: async (toolCallId, params) => {
|
|
1625
|
+
const gateResult = await gate(globalThis);
|
|
1626
|
+
if (!gateResult.allowed) {
|
|
1627
|
+
return { content: [{ type: "text", text: gateResult.message ?? "Rig bridge disabled (protocol mismatch)." }], isError: true };
|
|
1628
|
+
}
|
|
1629
|
+
return tool.execute(toolCallId, params);
|
|
1630
|
+
}
|
|
1631
|
+
}));
|
|
389
1632
|
}
|
|
390
|
-
|
|
1633
|
+
startSteeringBridge(pi, state, globalThis, gate, deliveredSteeringIds);
|
|
391
1634
|
}
|
|
1635
|
+
pi.on?.("input", async (event, ctx) => handleOperatorInput(event, state, ctx, gate));
|
|
392
1636
|
pi.on?.("session_start", async (_event, ctx) => {
|
|
393
1637
|
if (!state.active || !state.runId)
|
|
394
1638
|
return;
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
if (
|
|
398
|
-
|
|
1639
|
+
const shortId = state.runId.slice(0, 8);
|
|
1640
|
+
setStatus(ctx, "rig", `drone ${shortId} \xB7 waiting for worker daemon\u2026`);
|
|
1641
|
+
if (state.operatorSession) {
|
|
1642
|
+
setTitle(ctx, `Rig \xB7 run ${shortId}`);
|
|
1643
|
+
applyRigTheme(ctx);
|
|
1644
|
+
applyDroneWorkingIndicator(ctx);
|
|
1645
|
+
}
|
|
1646
|
+
const gateResult = await gate(ctx);
|
|
1647
|
+
if (!gateResult.allowed) {
|
|
1648
|
+
setStatus(ctx, "rig", `drone ${shortId} \xB7 bridge disabled (protocol mismatch)`);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
setStatus(ctx, "rig", `drone ${shortId} \xB7 connecting\u2026`);
|
|
1652
|
+
const live = gateResult.status === "compatible" ? startOperatorBridge(state, ctx) : undefined;
|
|
1653
|
+
startOperatorRunStatusLine(state, ctx, live);
|
|
1654
|
+
if (state.operatorSession && gateResult.status === "compatible") {
|
|
1655
|
+
startWorkerSessionMirror(pi, state, ctx);
|
|
1656
|
+
startWorkerCommandRegistration(pi, state, ctx);
|
|
399
1657
|
}
|
|
400
|
-
await consumeQueuedSteering(pi, state, ctx);
|
|
1658
|
+
await consumeQueuedSteering(pi, state, ctx, gate, deliveredSteeringIds);
|
|
401
1659
|
});
|
|
402
1660
|
pi.on?.("turn_end", async (_event, ctx) => {
|
|
403
|
-
await consumeQueuedSteering(pi, state, ctx);
|
|
1661
|
+
await consumeQueuedSteering(pi, state, ctx, gate, deliveredSteeringIds);
|
|
404
1662
|
});
|
|
405
1663
|
}
|
|
406
1664
|
export {
|