@h-rig/pi-rig 0.0.6-alpha.77 → 0.0.6-alpha.79

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,4 +1,6 @@
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";
@@ -266,10 +268,11 @@ class RigBridgeClient {
266
268
  return commands.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
267
269
  }
268
270
  async workerRunCommand(command, args, runId = this.context.runId) {
271
+ const text = `/${command}${args.trim() ? ` ${args.trim()}` : ""}`;
269
272
  const payload = await this.piProxy("commands/run", {
270
273
  method: "POST",
271
274
  headers: { "content-type": "application/json" },
272
- body: JSON.stringify({ command, args })
275
+ body: JSON.stringify({ text })
273
276
  }, runId);
274
277
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
275
278
  }
@@ -277,7 +280,7 @@ class RigBridgeClient {
277
280
  const payload = await this.piProxy("shell", {
278
281
  method: "POST",
279
282
  headers: { "content-type": "application/json" },
280
- body: JSON.stringify({ command })
283
+ body: JSON.stringify({ text: command })
281
284
  }, runId);
282
285
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
283
286
  }
@@ -285,6 +288,32 @@ class RigBridgeClient {
285
288
  const payload = await this.piProxy("abort", { method: "POST" }, runId);
286
289
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
287
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
+ }
288
317
  }
289
318
  function buildRigWebSocketUrl(serverUrl, authToken) {
290
319
  const url = new URL(serverUrl);
@@ -675,6 +704,222 @@ function createRigSlashCommands(input) {
675
704
  };
676
705
  }
677
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
+
678
923
  // packages/pi-rig/src/tools.ts
679
924
  function textResult(text, details) {
680
925
  return { content: [{ type: "text", text }], ...details ? { details } : {} };
@@ -766,20 +1011,49 @@ function setStatus(ctx, id, text) {
766
1011
  setStatusFn.call(ui, id, text);
767
1012
  }
768
1013
  }
769
- function setFooter(ctx, line) {
1014
+ function uiOf(ctx) {
770
1015
  const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
771
- const setFooterFn = ui && typeof ui === "object" ? ui.setFooter : null;
772
- if (typeof setFooterFn !== "function")
773
- return;
774
- setFooterFn.call(ui, () => ({
775
- render(width) {
776
- const max = Math.max(0, Math.trunc(width));
777
- if (max === 0)
778
- return [""];
779
- return [line.length > max ? `${line.slice(0, Math.max(0, max - 1))}\u2026` : line];
780
- },
781
- invalidate() {}
782
- }));
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("/")}`;
783
1057
  }
784
1058
  function createBridgeGate(state) {
785
1059
  let pending = null;
@@ -893,12 +1167,6 @@ async function handleOperatorInput(event, state, ctx, gate) {
893
1167
  }
894
1168
  return { action: "handled" };
895
1169
  }
896
- function shortEntry(entry) {
897
- const type = String(entry.type ?? entry.title ?? "event");
898
- const text = typeof entry.text === "string" ? entry.text : typeof entry.detail === "string" ? entry.detail : typeof entry.message === "string" ? entry.message : "";
899
- return `${type}: ${text}`.slice(0, 160);
900
- }
901
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
902
1170
  function runLocation(run) {
903
1171
  const worktree = typeof run.worktreePath === "string" && run.worktreePath.trim() ? run.worktreePath.trim() : null;
904
1172
  const projectRoot = typeof run.projectRoot === "string" && run.projectRoot.trim() ? run.projectRoot.trim() : null;
@@ -908,42 +1176,23 @@ function runPayload(payload) {
908
1176
  return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
909
1177
  }
910
1178
  var OPERATOR_WIDGET_WS_FALLBACK_MS = 1e4;
911
- function startOperatorRunWidget(state, ctx, live) {
1179
+ function startOperatorRunStatusLine(state, ctx, live) {
912
1180
  if (!state.operatorSession || !state.active || !state.runId)
913
1181
  return;
1182
+ const shortId = state.runId.slice(0, 8);
914
1183
  let inFlight = false;
915
- let frame = 0;
916
1184
  let lastRefreshAt = 0;
917
1185
  const refresh = async () => {
918
1186
  if (inFlight)
919
1187
  return;
920
1188
  inFlight = true;
921
1189
  lastRefreshAt = Date.now();
922
- const spinner = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length] ?? "\u2022";
923
1190
  try {
924
- const [runPayloadRecord, timeline] = await Promise.all([
925
- state.client.attach(state.runId),
926
- state.client.runTimeline(state.runId, 8).catch(() => [])
927
- ]);
928
- const run = runPayload(runPayloadRecord);
1191
+ const run = runPayload(await state.client.attach(state.runId));
929
1192
  const status = String(run.status ?? "unknown");
930
- const header = `${spinner} Rig ${String(run.runId ?? state.runId)} \xB7 ${status}`;
931
- const location = runLocation(run);
932
- const detail = typeof run.statusDetail === "string" && run.statusDetail.trim() ? run.statusDetail.trim() : String(run.title ?? run.taskId ?? "");
933
- const lines = [
934
- header,
935
- `worker: ${location}`.slice(0, 200),
936
- ...detail ? [detail.slice(0, 160)] : [],
937
- ...timeline.slice(-5).map(shortEntry)
938
- ];
939
- setStatus(ctx, "rig", `${spinner} Rig ${status}`);
940
- setFooter(ctx, `${spinner} Rig ${String(run.runId ?? state.runId)} \xB7 ${status} \xB7 worker ${location}`);
941
- setWidget(ctx, "rig-run", lines);
1193
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 ${status} \xB7 ${shortPath(runLocation(run))}`);
942
1194
  } catch (error) {
943
- const message = error instanceof Error ? error.message : String(error);
944
- setStatus(ctx, "rig", `${spinner} Rig unavailable`);
945
- setFooter(ctx, `${spinner} Rig ${state.runId} \xB7 unavailable \xB7 ${message}`);
946
- setWidget(ctx, "rig-run", [`${spinner} Rig ${state.runId} \xB7 unavailable`, message]);
1195
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 server unreachable: ${error instanceof Error ? error.message : String(error)}`);
947
1196
  } finally {
948
1197
  inFlight = false;
949
1198
  }
@@ -955,7 +1204,7 @@ function startOperatorRunWidget(state, ctx, live) {
955
1204
  return;
956
1205
  }
957
1206
  refresh();
958
- }, 1000);
1207
+ }, 5000);
959
1208
  unrefTimer(timer);
960
1209
  }
961
1210
  function operatorInboxNotification(event) {
@@ -1012,47 +1261,6 @@ function startOperatorBridge(state, ctx) {
1012
1261
  }
1013
1262
  };
1014
1263
  }
1015
- var MIRROR_TEXT_LIMIT = 2000;
1016
- function clipMirrorText(text) {
1017
- const trimmed = text.trim();
1018
- return trimmed.length > MIRROR_TEXT_LIMIT ? `${trimmed.slice(0, MIRROR_TEXT_LIMIT)}
1019
- \u2026 [clipped; full output in the worker session]` : trimmed;
1020
- }
1021
- function workerMirrorText(message) {
1022
- const role = typeof message.role === "string" ? message.role : "assistant";
1023
- const content = message.content;
1024
- const parts = [];
1025
- if (typeof content === "string") {
1026
- parts.push(content);
1027
- } else if (Array.isArray(content)) {
1028
- for (const block of content) {
1029
- if (!block || typeof block !== "object" || Array.isArray(block))
1030
- continue;
1031
- const record = block;
1032
- if (record.type === "text" && typeof record.text === "string" && record.text.trim()) {
1033
- parts.push(record.text);
1034
- continue;
1035
- }
1036
- if (record.type === "toolCall") {
1037
- const name = typeof record.name === "string" ? record.name : "tool";
1038
- let args = "";
1039
- try {
1040
- args = record.arguments === undefined ? "" : JSON.stringify(record.arguments);
1041
- } catch {
1042
- args = "";
1043
- }
1044
- parts.push(`\u2699 ${name}(${args.length > 160 ? `${args.slice(0, 160)}\u2026` : args})`);
1045
- }
1046
- }
1047
- }
1048
- const body = parts.join(`
1049
- `).trim();
1050
- if (!body)
1051
- return null;
1052
- const label = role === "assistant" ? "worker" : role === "user" ? "worker \u21D0 input" : role === "toolResult" ? `worker \u2937 ${typeof message.toolName === "string" ? message.toolName : "tool"} result` : `worker (${role})`;
1053
- return `[${label}]
1054
- ${clipMirrorText(body)}`;
1055
- }
1056
1264
  function workerStatusLine(status) {
1057
1265
  const model = status.model && typeof status.model === "object" && !Array.isArray(status.model) ? status.model : null;
1058
1266
  const modelId = model && typeof model.id === "string" ? model.id : null;
@@ -1062,23 +1270,129 @@ function workerStatusLine(status) {
1062
1270
  const parts = [modelId, percent, streaming].filter((value) => Boolean(value));
1063
1271
  return parts.length > 0 ? `worker ${parts.join(" \xB7 ")}` : "worker session connected";
1064
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
+ }
1065
1380
  function startWorkerSessionMirror(pi, state, ctx) {
1066
1381
  if (!state.operatorSession || !state.active || !state.runId)
1067
1382
  return;
1068
- if (typeof pi.sendMessage !== "function")
1069
- return;
1070
- const mirrored = new Set;
1071
- const rememberMirrored = (key) => {
1072
- if (mirrored.has(key))
1073
- return false;
1074
- mirrored.add(key);
1075
- if (mirrored.size > 1000) {
1076
- const oldest = mirrored.values().next().value;
1077
- if (typeof oldest === "string")
1078
- mirrored.delete(oldest);
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));
1079
1390
  }
1080
- return true;
1081
- };
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
+ });
1082
1396
  const socket = new RigWorkerEventsSocket({
1083
1397
  context: state,
1084
1398
  webSocketFactory: state.webSocketFactory,
@@ -1093,44 +1407,66 @@ function startWorkerSessionMirror(pi, state, ctx) {
1093
1407
  if (frame.type !== "pi.event")
1094
1408
  return;
1095
1409
  const event = frame.event && typeof frame.event === "object" && !Array.isArray(frame.event) ? frame.event : null;
1096
- if (!event || event.type !== "message_end")
1410
+ if (!event)
1097
1411
  return;
1098
- const message = event.message && typeof event.message === "object" && !Array.isArray(event.message) ? event.message : null;
1099
- if (!message)
1412
+ if (event.type === "extension_ui_request") {
1413
+ forwardWorkerUiRequest(state, ctx, event);
1100
1414
  return;
1101
- const key = typeof message.id === "string" && message.id.trim() ? message.id : `${String(message.role ?? "message")}:${String(message.timestamp ?? "")}`;
1102
- if (!rememberMirrored(key))
1103
- return;
1104
- const text = workerMirrorText(message);
1105
- if (!text)
1106
- return;
1107
- pi.sendMessage?.({ customType: "rig-worker-mirror", content: text, display: true }, { triggerTurn: false });
1415
+ }
1416
+ if (mirror)
1417
+ mirror.handleWorkerEvent(event);
1418
+ else
1419
+ pendingEvents.push(event);
1108
1420
  },
1109
1421
  onConnect: () => {
1110
- setStatus(ctx, "rig-worker", "worker session connected");
1422
+ setStatus(ctx, "rig-worker", "drone link live");
1111
1423
  },
1112
1424
  onDisconnect: () => {
1113
- setStatus(ctx, "rig-worker", "worker session disconnected (reconnecting\u2026)");
1425
+ setStatus(ctx, "rig-worker", "drone link down (reconnecting\u2026)");
1114
1426
  }
1115
1427
  }
1116
1428
  });
1117
1429
  socket.start();
1118
1430
  }
1119
- async function registerWorkerCommands(pi, state, ctx) {
1120
- if (!state.operatorSession || !state.active || !state.runId)
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)
1121
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;
1122
1458
  if (typeof pi.registerCommand !== "function")
1123
- return;
1459
+ return false;
1124
1460
  let commands = [];
1125
1461
  try {
1126
1462
  commands = await state.client.workerCommands(state.runId);
1127
1463
  } catch {
1128
- return;
1464
+ return false;
1129
1465
  }
1130
1466
  let registered = 0;
1131
1467
  for (const command of commands) {
1132
1468
  const name = typeof command.name === "string" && command.name.trim() ? command.name.trim() : null;
1133
- if (!name || name === "rig")
1469
+ if (!name || name === "rig" || registeredNames.has(name))
1134
1470
  continue;
1135
1471
  const description = typeof command.description === "string" && command.description.trim() ? `[worker] ${command.description.trim()}` : "[worker] remote session command";
1136
1472
  try {
@@ -1145,12 +1481,47 @@ async function registerWorkerCommands(pi, state, ctx) {
1145
1481
  }
1146
1482
  }
1147
1483
  });
1484
+ registeredNames.add(name);
1148
1485
  registered += 1;
1149
- } catch {}
1486
+ } catch {
1487
+ registeredNames.add(name);
1488
+ }
1150
1489
  }
1151
1490
  if (registered > 0) {
1152
1491
  notify(ctx, `Worker session commands available: ${registered} registered from the run's Pi session.`);
1153
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
+ });
1154
1525
  }
1155
1526
  function startSteeringBridge(pi, state, ctx, gate, deliveredIds) {
1156
1527
  if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
@@ -1243,6 +1614,9 @@ function createPiRigExtension(pi, options = {}) {
1243
1614
  }
1244
1615
  }));
1245
1616
  }
1617
+ if (state.operatorSession && state.active && state.runId) {
1618
+ registerOperatorConsoleCommands(pi, state, tryRegister);
1619
+ }
1246
1620
  if (state.active && state.runId) {
1247
1621
  for (const tool of createRigTools({ context: state, client: state.client })) {
1248
1622
  tryRegister(`tool:${String(tool.name ?? "rig-tool")}`, () => pi.registerTool?.({
@@ -1262,26 +1636,24 @@ function createPiRigExtension(pi, options = {}) {
1262
1636
  pi.on?.("session_start", async (_event, ctx) => {
1263
1637
  if (!state.active || !state.runId)
1264
1638
  return;
1265
- setStatus(ctx, "rig", `Rig ${state.runId} \xB7 connecting\u2026`);
1639
+ const shortId = state.runId.slice(0, 8);
1640
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 waiting for worker daemon\u2026`);
1266
1641
  if (state.operatorSession) {
1267
- setFooter(ctx, `Rig ${state.runId} \xB7 connecting to ${state.serverUrl ?? "Rig server"}\u2026`);
1268
- setWidget(ctx, "rig-run", [`Rig ${state.runId} \xB7 connecting\u2026`]);
1642
+ setTitle(ctx, `Rig \xB7 run ${shortId}`);
1643
+ applyRigTheme(ctx);
1644
+ applyDroneWorkingIndicator(ctx);
1269
1645
  }
1270
1646
  const gateResult = await gate(ctx);
1271
1647
  if (!gateResult.allowed) {
1272
- if (state.operatorSession) {
1273
- const message = gateResult.message ?? "Rig bridge disabled (protocol mismatch).";
1274
- setFooter(ctx, `Rig ${state.runId} \xB7 bridge disabled (protocol mismatch)`);
1275
- setWidget(ctx, "rig-run", [message]);
1276
- }
1648
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 bridge disabled (protocol mismatch)`);
1277
1649
  return;
1278
1650
  }
1279
- setStatus(ctx, "rig", `Rig ${state.runId}`);
1651
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 connecting\u2026`);
1280
1652
  const live = gateResult.status === "compatible" ? startOperatorBridge(state, ctx) : undefined;
1281
- startOperatorRunWidget(state, ctx, live);
1653
+ startOperatorRunStatusLine(state, ctx, live);
1282
1654
  if (state.operatorSession && gateResult.status === "compatible") {
1283
1655
  startWorkerSessionMirror(pi, state, ctx);
1284
- registerWorkerCommands(pi, state, ctx);
1656
+ startWorkerCommandRegistration(pi, state, ctx);
1285
1657
  }
1286
1658
  await consumeQueuedSteering(pi, state, ctx, gate, deliveredSteeringIds);
1287
1659
  });