@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/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.RIG_GITHUB_TOKEN ?? env.GITHUB_TOKEN ?? env.GH_TOKEN ?? discovered.authToken;
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
- const response = await this.fetchImpl(joinUrl(requireServerUrl(this.context), pathname), { ...init, headers });
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.run && typeof run.run === "object" ? run.run : 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
- notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id]", "error");
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
- async function consumeQueuedSteering(pi, state, ctx) {
330
- if (!state.active || !state.runId || typeof pi.sendUserMessage !== "function")
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
- const text = steeringText(message);
336
- if (!text)
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 (messages.length > 0) {
341
- notify(ctx, `Delivered ${messages.length} Rig steering message${messages.length === 1 ? "" : "s"}.`);
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 startLiveSteeringPoll(pi, state, ctx) {
348
- if (!state.active || !state.runId || typeof pi.sendUserMessage !== "function")
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
- consumeQueuedSteering(pi, state, ctx).finally(() => {
1573
+ lastSweepAt = Date.now();
1574
+ consumeQueuedSteering(pi, state, ctx, gate, deliveredIds).finally(() => {
359
1575
  inFlight = false;
360
1576
  });
361
1577
  }, intervalMs);
362
- if (typeof timer.unref === "function") {
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?.(tool);
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
- startLiveSteeringPoll(pi, state, globalThis);
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 ui = ctx && typeof ctx === "object" ? ctx.ui : null;
396
- const setStatus = ui && typeof ui === "object" ? ui.setStatus : null;
397
- if (typeof setStatus === "function") {
398
- setStatus.call(ui, "rig", `Rig ${state.runId}`);
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 {