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

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.
@@ -285,6 +285,32 @@ class RigBridgeClient {
285
285
  const payload = await this.piProxy("abort", { method: "POST" }, runId);
286
286
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
287
287
  }
288
+ async workerCapabilities(runId = this.context.runId) {
289
+ const payload = await this.piProxy("capabilities", undefined, runId);
290
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
291
+ }
292
+ async workerRespondExtensionUi(requestId, response, runId = this.context.runId) {
293
+ const payload = await this.piProxy("extension-ui/respond", {
294
+ method: "POST",
295
+ headers: { "content-type": "application/json" },
296
+ body: JSON.stringify({ requestId, response })
297
+ }, runId);
298
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
299
+ }
300
+ async fetchRunSessionFile(runId = this.context.runId) {
301
+ if (!runId)
302
+ return null;
303
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/session-file`);
304
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
305
+ return null;
306
+ const record = payload;
307
+ if (record.ok !== true || typeof record.content !== "string" || !record.content.trim())
308
+ return null;
309
+ return {
310
+ fileName: typeof record.fileName === "string" && record.fileName.trim() ? record.fileName : `rig-run-${runId}.jsonl`,
311
+ content: record.content
312
+ };
313
+ }
288
314
  }
289
315
  function buildRigWebSocketUrl(serverUrl, authToken) {
290
316
  const url = new URL(serverUrl);
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";
@@ -285,6 +287,32 @@ class RigBridgeClient {
285
287
  const payload = await this.piProxy("abort", { method: "POST" }, runId);
286
288
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
287
289
  }
290
+ async workerCapabilities(runId = this.context.runId) {
291
+ const payload = await this.piProxy("capabilities", undefined, runId);
292
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
293
+ }
294
+ async workerRespondExtensionUi(requestId, response, runId = this.context.runId) {
295
+ const payload = await this.piProxy("extension-ui/respond", {
296
+ method: "POST",
297
+ headers: { "content-type": "application/json" },
298
+ body: JSON.stringify({ requestId, response })
299
+ }, runId);
300
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
301
+ }
302
+ async fetchRunSessionFile(runId = this.context.runId) {
303
+ if (!runId)
304
+ return null;
305
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/session-file`);
306
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
307
+ return null;
308
+ const record = payload;
309
+ if (record.ok !== true || typeof record.content !== "string" || !record.content.trim())
310
+ return null;
311
+ return {
312
+ fileName: typeof record.fileName === "string" && record.fileName.trim() ? record.fileName : `rig-run-${runId}.jsonl`,
313
+ content: record.content
314
+ };
315
+ }
288
316
  }
289
317
  function buildRigWebSocketUrl(serverUrl, authToken) {
290
318
  const url = new URL(serverUrl);
@@ -675,6 +703,215 @@ function createRigSlashCommands(input) {
675
703
  };
676
704
  }
677
705
 
706
+ // packages/pi-rig/src/live-mirror.ts
707
+ async function loadPiModules() {
708
+ const components = await import("@earendil-works/pi-coding-agent");
709
+ return { components };
710
+ }
711
+ function createRootContainer() {
712
+ const children = [];
713
+ return {
714
+ addChild(child) {
715
+ children.push(child);
716
+ },
717
+ removeChild(child) {
718
+ const index = children.indexOf(child);
719
+ if (index >= 0)
720
+ children.splice(index, 1);
721
+ },
722
+ clear() {
723
+ children.length = 0;
724
+ },
725
+ render(width) {
726
+ return children.flatMap((child) => child.render(width));
727
+ }
728
+ };
729
+ }
730
+ var DRONE_MESSAGE_TYPE = "rig-drone";
731
+ var DRONE_USER_MESSAGE_TYPE = "rig-drone-user";
732
+ function recordOf(value) {
733
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
734
+ }
735
+ function messageRole(event) {
736
+ const message = recordOf(event.message);
737
+ return message && typeof message.role === "string" ? message.role : null;
738
+ }
739
+ function userMessageText(message) {
740
+ const content = message.content;
741
+ if (typeof content === "string")
742
+ return content;
743
+ if (Array.isArray(content)) {
744
+ return content.flatMap((block) => {
745
+ const record = recordOf(block);
746
+ return record && record.type === "text" && typeof record.text === "string" ? [record.text] : [];
747
+ }).join(`
748
+ `);
749
+ }
750
+ return "";
751
+ }
752
+ async function createLiveMirror(input) {
753
+ const { pi } = input;
754
+ const modules = input.modules ?? await loadPiModules();
755
+ const cwd = input.workerCwd ?? process.cwd();
756
+ const turns = new Map;
757
+ const toolOwners = new Map;
758
+ const userComponents = new Map;
759
+ let streamingTurn = null;
760
+ let sequence = 0;
761
+ let tui = null;
762
+ const requestRender = () => {
763
+ tui?.requestRender?.();
764
+ };
765
+ pi.registerMessageRenderer?.(DRONE_MESSAGE_TYPE, (message) => {
766
+ const details = recordOf(recordOf(message)?.details);
767
+ const key = details && typeof details.key === "string" ? details.key : null;
768
+ return key ? turns.get(key)?.root : undefined;
769
+ });
770
+ pi.registerMessageRenderer?.(DRONE_USER_MESSAGE_TYPE, (message) => {
771
+ const details = recordOf(recordOf(message)?.details);
772
+ const key = details && typeof details.key === "string" ? details.key : null;
773
+ return key ? userComponents.get(key) : undefined;
774
+ });
775
+ const ensureToolComponent = (turn, toolCallId, toolName, args) => {
776
+ const existing = turn.tools.get(toolCallId);
777
+ if (existing)
778
+ return existing;
779
+ const component = new modules.components.ToolExecutionComponent(toolName, toolCallId, args, {}, undefined, tui, cwd);
780
+ turn.root.addChild(component);
781
+ turn.tools.set(toolCallId, component);
782
+ toolOwners.set(toolCallId, turn);
783
+ return component;
784
+ };
785
+ const startAssistantTurn = (message) => {
786
+ const key = `turn-${++sequence}`;
787
+ const root = createRootContainer();
788
+ const assistant = new modules.components.AssistantMessageComponent;
789
+ root.addChild(assistant);
790
+ const turn = { root, assistant, tools: new Map };
791
+ assistant.updateContent(message);
792
+ turns.set(key, turn);
793
+ streamingTurn = turn;
794
+ pi.sendMessage?.({ customType: DRONE_MESSAGE_TYPE, content: "drone turn", display: true, details: { key } }, { triggerTurn: false });
795
+ requestRender();
796
+ };
797
+ const updateAssistantTurn = (message) => {
798
+ const turn = streamingTurn;
799
+ if (!turn)
800
+ return;
801
+ turn.assistant.updateContent(message);
802
+ const content = Array.isArray(message.content) ? message.content : [];
803
+ for (const block of content) {
804
+ const record = recordOf(block);
805
+ if (!record || record.type !== "toolCall")
806
+ continue;
807
+ const id = typeof record.id === "string" ? record.id : null;
808
+ const name = typeof record.name === "string" ? record.name : "tool";
809
+ if (!id)
810
+ continue;
811
+ const component = ensureToolComponent(turn, id, name, record.arguments);
812
+ component.updateArgs(record.arguments);
813
+ }
814
+ requestRender();
815
+ };
816
+ const endAssistantTurn = (message) => {
817
+ const turn = streamingTurn;
818
+ if (!turn)
819
+ return;
820
+ turn.assistant.updateContent(message);
821
+ const stopReason = typeof message.stopReason === "string" ? message.stopReason : "stop";
822
+ if (stopReason === "aborted" || stopReason === "error") {
823
+ const errorText = typeof message.errorMessage === "string" && message.errorMessage ? message.errorMessage : stopReason === "aborted" ? "Operation aborted" : "Error";
824
+ for (const component of turn.tools.values()) {
825
+ component.updateResult({ content: [{ type: "text", text: errorText }], isError: true });
826
+ }
827
+ } else {
828
+ for (const component of turn.tools.values()) {
829
+ component.setArgsComplete();
830
+ }
831
+ }
832
+ streamingTurn = null;
833
+ requestRender();
834
+ };
835
+ const mirrorUserMessage = (message) => {
836
+ const text = userMessageText(message).trim();
837
+ if (!text)
838
+ return;
839
+ const key = `user-${++sequence}`;
840
+ userComponents.set(key, new modules.components.UserMessageComponent(text));
841
+ pi.sendMessage?.({ customType: DRONE_USER_MESSAGE_TYPE, content: text, display: true, details: { key } }, { triggerTurn: false });
842
+ requestRender();
843
+ };
844
+ return {
845
+ captureTui(capturedTui) {
846
+ tui = recordOf(capturedTui);
847
+ return { render: () => [] };
848
+ },
849
+ handleWorkerEvent(event) {
850
+ const type = typeof event.type === "string" ? event.type : null;
851
+ switch (type) {
852
+ case "message_start": {
853
+ const message = recordOf(event.message);
854
+ if (!message)
855
+ return;
856
+ if (messageRole(event) === "assistant")
857
+ startAssistantTurn(message);
858
+ else if (messageRole(event) === "user")
859
+ mirrorUserMessage(message);
860
+ return;
861
+ }
862
+ case "message_update": {
863
+ const message = recordOf(event.message);
864
+ if (message && messageRole(event) === "assistant")
865
+ updateAssistantTurn(message);
866
+ return;
867
+ }
868
+ case "message_end": {
869
+ const message = recordOf(event.message);
870
+ if (message && messageRole(event) === "assistant")
871
+ endAssistantTurn(message);
872
+ return;
873
+ }
874
+ case "tool_execution_start": {
875
+ const id = typeof event.toolCallId === "string" ? event.toolCallId : null;
876
+ const name = typeof event.toolName === "string" ? event.toolName : "tool";
877
+ if (!id)
878
+ return;
879
+ const turn = toolOwners.get(id) ?? streamingTurn;
880
+ if (!turn)
881
+ return;
882
+ ensureToolComponent(turn, id, name, event.args).markExecutionStarted();
883
+ requestRender();
884
+ return;
885
+ }
886
+ case "tool_execution_update": {
887
+ const id = typeof event.toolCallId === "string" ? event.toolCallId : null;
888
+ const component = id ? toolOwners.get(id)?.tools.get(id) : undefined;
889
+ const partial = recordOf(event.partialResult);
890
+ if (component && partial) {
891
+ const content = Array.isArray(partial.content) ? partial.content : [];
892
+ component.updateResult({ ...partial, content, isError: false }, true);
893
+ requestRender();
894
+ }
895
+ return;
896
+ }
897
+ case "tool_execution_end": {
898
+ const id = typeof event.toolCallId === "string" ? event.toolCallId : null;
899
+ const component = id ? toolOwners.get(id)?.tools.get(id) : undefined;
900
+ const result = recordOf(event.result);
901
+ if (component && result) {
902
+ const content = Array.isArray(result.content) ? result.content : [];
903
+ component.updateResult({ ...result, content, isError: event.isError === true });
904
+ requestRender();
905
+ }
906
+ return;
907
+ }
908
+ default:
909
+ return;
910
+ }
911
+ }
912
+ };
913
+ }
914
+
678
915
  // packages/pi-rig/src/tools.ts
679
916
  function textResult(text, details) {
680
917
  return { content: [{ type: "text", text }], ...details ? { details } : {} };
@@ -766,20 +1003,49 @@ function setStatus(ctx, id, text) {
766
1003
  setStatusFn.call(ui, id, text);
767
1004
  }
768
1005
  }
769
- function setFooter(ctx, line) {
1006
+ function uiOf(ctx) {
770
1007
  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
- }));
1008
+ return ui && typeof ui === "object" ? ui : null;
1009
+ }
1010
+ function setTitle(ctx, title) {
1011
+ const ui = uiOf(ctx);
1012
+ const setTitleFn = ui?.setTitle;
1013
+ if (typeof setTitleFn === "function")
1014
+ setTitleFn.call(ui, title);
1015
+ }
1016
+ function shutdownPi(ctx) {
1017
+ const shutdownFn = ctx && typeof ctx === "object" ? ctx.shutdown : null;
1018
+ if (typeof shutdownFn === "function")
1019
+ shutdownFn.call(ctx);
1020
+ }
1021
+ async function askNativeDialog(ctx, request) {
1022
+ const ui = uiOf(ctx);
1023
+ if (!ui)
1024
+ return null;
1025
+ const { method, prompt, choices } = request;
1026
+ try {
1027
+ if (method === "confirm" && typeof ui.confirm === "function") {
1028
+ const confirmed = await ui.confirm("Worker request", prompt);
1029
+ return { value: confirmed, confirmed };
1030
+ }
1031
+ if (choices.length > 0 && typeof ui.select === "function") {
1032
+ const selected = await ui.select(prompt, choices);
1033
+ return selected === undefined ? { cancelled: true } : { value: selected };
1034
+ }
1035
+ if (typeof ui.input === "function") {
1036
+ const value = await ui.input("Worker request", prompt);
1037
+ return value === undefined ? { cancelled: true } : { value };
1038
+ }
1039
+ } catch {
1040
+ return { cancelled: true };
1041
+ }
1042
+ return null;
1043
+ }
1044
+ function shortPath(path, segments = 3) {
1045
+ const parts = path.split("/").filter(Boolean);
1046
+ if (parts.length <= segments)
1047
+ return path;
1048
+ return `\u2026/${parts.slice(-segments).join("/")}`;
783
1049
  }
784
1050
  function createBridgeGate(state) {
785
1051
  let pending = null;
@@ -893,12 +1159,6 @@ async function handleOperatorInput(event, state, ctx, gate) {
893
1159
  }
894
1160
  return { action: "handled" };
895
1161
  }
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
1162
  function runLocation(run) {
903
1163
  const worktree = typeof run.worktreePath === "string" && run.worktreePath.trim() ? run.worktreePath.trim() : null;
904
1164
  const projectRoot = typeof run.projectRoot === "string" && run.projectRoot.trim() ? run.projectRoot.trim() : null;
@@ -908,42 +1168,23 @@ function runPayload(payload) {
908
1168
  return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
909
1169
  }
910
1170
  var OPERATOR_WIDGET_WS_FALLBACK_MS = 1e4;
911
- function startOperatorRunWidget(state, ctx, live) {
1171
+ function startOperatorRunStatusLine(state, ctx, live) {
912
1172
  if (!state.operatorSession || !state.active || !state.runId)
913
1173
  return;
1174
+ const shortId = state.runId.slice(0, 8);
914
1175
  let inFlight = false;
915
- let frame = 0;
916
1176
  let lastRefreshAt = 0;
917
1177
  const refresh = async () => {
918
1178
  if (inFlight)
919
1179
  return;
920
1180
  inFlight = true;
921
1181
  lastRefreshAt = Date.now();
922
- const spinner = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length] ?? "\u2022";
923
1182
  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);
1183
+ const run = runPayload(await state.client.attach(state.runId));
929
1184
  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);
1185
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 ${status} \xB7 ${shortPath(runLocation(run))}`);
942
1186
  } 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]);
1187
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 server unreachable: ${error instanceof Error ? error.message : String(error)}`);
947
1188
  } finally {
948
1189
  inFlight = false;
949
1190
  }
@@ -955,7 +1196,7 @@ function startOperatorRunWidget(state, ctx, live) {
955
1196
  return;
956
1197
  }
957
1198
  refresh();
958
- }, 1000);
1199
+ }, 5000);
959
1200
  unrefTimer(timer);
960
1201
  }
961
1202
  function operatorInboxNotification(event) {
@@ -1012,47 +1253,6 @@ function startOperatorBridge(state, ctx) {
1012
1253
  }
1013
1254
  };
1014
1255
  }
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
1256
  function workerStatusLine(status) {
1057
1257
  const model = status.model && typeof status.model === "object" && !Array.isArray(status.model) ? status.model : null;
1058
1258
  const modelId = model && typeof model.id === "string" ? model.id : null;
@@ -1062,23 +1262,129 @@ function workerStatusLine(status) {
1062
1262
  const parts = [modelId, percent, streaming].filter((value) => Boolean(value));
1063
1263
  return parts.length > 0 ? `worker ${parts.join(" \xB7 ")}` : "worker session connected";
1064
1264
  }
1265
+ function applyRigTheme(ctx) {
1266
+ const ui = uiOf(ctx);
1267
+ if (!ui || typeof ui.getTheme !== "function" || typeof ui.setTheme !== "function")
1268
+ return;
1269
+ try {
1270
+ const theme = ui.getTheme("rig");
1271
+ if (theme)
1272
+ ui.setTheme(theme);
1273
+ } catch {}
1274
+ }
1275
+ var MICRO_DRONE_BLADES = ["---", "\\\\\\", "|||", "///"];
1276
+ var MICRO_DRONE_EYES = ["@", "o", "."];
1277
+ function applyDroneWorkingIndicator(ctx) {
1278
+ const ui = uiOf(ctx);
1279
+ if (!ui || typeof ui.setWorkingIndicator !== "function")
1280
+ return;
1281
+ const frames = Array.from({ length: 12 }, (_, index) => {
1282
+ const blade = MICRO_DRONE_BLADES[index % MICRO_DRONE_BLADES.length];
1283
+ const eye = MICRO_DRONE_EYES[Math.floor(index / 2) % MICRO_DRONE_EYES.length];
1284
+ return `(${blade})${eye}(${blade})`;
1285
+ });
1286
+ try {
1287
+ ui.setWorkingIndicator({ frames, intervalMs: 120 });
1288
+ } catch {}
1289
+ }
1290
+ function renderWorkerCapabilities(capabilities) {
1291
+ const names = (value, key = "name") => Array.isArray(value) ? value.flatMap((entry) => {
1292
+ if (typeof entry === "string")
1293
+ return [entry];
1294
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
1295
+ const name = entry[key];
1296
+ return typeof name === "string" ? [name] : [];
1297
+ }
1298
+ return [];
1299
+ }) : [];
1300
+ const lines = ["Drone capabilities (in effect for this run)"];
1301
+ const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
1302
+ const activeTools = tools.flatMap((tool) => {
1303
+ const record = tool && typeof tool === "object" && !Array.isArray(tool) ? tool : null;
1304
+ return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
1305
+ });
1306
+ const inactiveCount = tools.length - activeTools.length;
1307
+ lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
1308
+ const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
1309
+ const extensionLabels = extensions.flatMap((entry) => {
1310
+ const record = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
1311
+ if (!record)
1312
+ return [];
1313
+ const path = typeof record.path === "string" ? record.path : "";
1314
+ const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
1315
+ return [short];
1316
+ });
1317
+ lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
1318
+ lines.push(` hooks ${names(capabilities.hookEvents).join(", ") || "(none)"}`);
1319
+ lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
1320
+ lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
1321
+ const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
1322
+ const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
1323
+ lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
1324
+ if (typeof capabilities.cwd === "string" && capabilities.cwd)
1325
+ lines.push(` cwd ${capabilities.cwd}`);
1326
+ lines.push(" (drone commands are in your palette \xB7 /worker toggles this panel)");
1327
+ return lines;
1328
+ }
1329
+ function registerOperatorConsoleCommands(pi, state, tryRegister) {
1330
+ if (typeof pi.registerCommand !== "function")
1331
+ return;
1332
+ let capabilitiesShown = false;
1333
+ tryRegister("command:detach", () => pi.registerCommand?.("detach", {
1334
+ description: "Detach from this run; the drone keeps flying",
1335
+ handler: async (_args, ctx) => {
1336
+ notify(ctx, "Detached. The drone continues on the worker.");
1337
+ shutdownPi(ctx);
1338
+ }
1339
+ }));
1340
+ tryRegister("command:stop", () => pi.registerCommand?.("stop", {
1341
+ description: "Abort the drone's current turn and detach",
1342
+ handler: async (_args, ctx) => {
1343
+ try {
1344
+ await state.client.workerAbort(state.runId);
1345
+ notify(ctx, "Abort requested; detaching.");
1346
+ } catch (error) {
1347
+ notify(ctx, `Abort failed: ${error instanceof Error ? error.message : String(error)}`, "error");
1348
+ }
1349
+ shutdownPi(ctx);
1350
+ }
1351
+ }));
1352
+ tryRegister("command:worker", () => pi.registerCommand?.("worker", {
1353
+ description: "Toggle the drone's capability panel (tools, extensions, hooks, skills, model)",
1354
+ handler: async (_args, ctx) => {
1355
+ if (capabilitiesShown) {
1356
+ const ui = uiOf(ctx);
1357
+ if (ui && typeof ui.setWidget === "function")
1358
+ ui.setWidget("rig-worker-capabilities", undefined);
1359
+ capabilitiesShown = false;
1360
+ return;
1361
+ }
1362
+ try {
1363
+ const capabilities = await state.client.workerCapabilities(state.runId);
1364
+ setWidget(ctx, "rig-worker-capabilities", renderWorkerCapabilities(capabilities));
1365
+ capabilitiesShown = true;
1366
+ } catch (error) {
1367
+ notify(ctx, `Drone capabilities unavailable: ${error instanceof Error ? error.message : String(error)}`, "error");
1368
+ }
1369
+ }
1370
+ }));
1371
+ }
1065
1372
  function startWorkerSessionMirror(pi, state, ctx) {
1066
1373
  if (!state.operatorSession || !state.active || !state.runId)
1067
1374
  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);
1375
+ let mirror = null;
1376
+ const pendingEvents = [];
1377
+ createLiveMirror({ pi }).then((created) => {
1378
+ mirror = created;
1379
+ const ui = uiOf(ctx);
1380
+ if (ui && typeof ui.setWidget === "function") {
1381
+ ui.setWidget("rig-tui-capture", (tui) => created.captureTui(tui));
1079
1382
  }
1080
- return true;
1081
- };
1383
+ for (const event of pendingEvents.splice(0))
1384
+ created.handleWorkerEvent(event);
1385
+ }).catch((error) => {
1386
+ notify(ctx, `Live drone transcript unavailable: ${error instanceof Error ? error.message : String(error)}`, "error");
1387
+ });
1082
1388
  const socket = new RigWorkerEventsSocket({
1083
1389
  context: state,
1084
1390
  webSocketFactory: state.webSocketFactory,
@@ -1093,29 +1399,51 @@ function startWorkerSessionMirror(pi, state, ctx) {
1093
1399
  if (frame.type !== "pi.event")
1094
1400
  return;
1095
1401
  const event = frame.event && typeof frame.event === "object" && !Array.isArray(frame.event) ? frame.event : null;
1096
- if (!event || event.type !== "message_end")
1097
- return;
1098
- const message = event.message && typeof event.message === "object" && !Array.isArray(event.message) ? event.message : null;
1099
- if (!message)
1100
- return;
1101
- const key = typeof message.id === "string" && message.id.trim() ? message.id : `${String(message.role ?? "message")}:${String(message.timestamp ?? "")}`;
1102
- if (!rememberMirrored(key))
1402
+ if (!event)
1103
1403
  return;
1104
- const text = workerMirrorText(message);
1105
- if (!text)
1404
+ if (event.type === "extension_ui_request") {
1405
+ forwardWorkerUiRequest(state, ctx, event);
1106
1406
  return;
1107
- pi.sendMessage?.({ customType: "rig-worker-mirror", content: text, display: true }, { triggerTurn: false });
1407
+ }
1408
+ if (mirror)
1409
+ mirror.handleWorkerEvent(event);
1410
+ else
1411
+ pendingEvents.push(event);
1108
1412
  },
1109
1413
  onConnect: () => {
1110
- setStatus(ctx, "rig-worker", "worker session connected");
1414
+ setStatus(ctx, "rig-worker", "drone link live");
1111
1415
  },
1112
1416
  onDisconnect: () => {
1113
- setStatus(ctx, "rig-worker", "worker session disconnected (reconnecting\u2026)");
1417
+ setStatus(ctx, "rig-worker", "drone link down (reconnecting\u2026)");
1114
1418
  }
1115
1419
  }
1116
1420
  });
1117
1421
  socket.start();
1118
1422
  }
1423
+ async function forwardWorkerUiRequest(state, ctx, event) {
1424
+ const request = event.request && typeof event.request === "object" && !Array.isArray(event.request) ? event.request : event;
1425
+ const requestId = String(request.requestId ?? request.id ?? `ui-${state.runId}-${event.timestamp ?? ""}`);
1426
+ const method = String(request.method ?? request.type ?? "input");
1427
+ 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;
1428
+ const rawChoices = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
1429
+ const choices = rawChoices.map((option) => {
1430
+ if (typeof option === "string")
1431
+ return option;
1432
+ if (option && typeof option === "object" && !Array.isArray(option)) {
1433
+ const record = option;
1434
+ return String(record.label ?? record.value ?? record.name ?? "");
1435
+ }
1436
+ return "";
1437
+ }).filter(Boolean);
1438
+ const answer = await askNativeDialog(ctx, { method, prompt, choices });
1439
+ if (!answer)
1440
+ return;
1441
+ try {
1442
+ await state.client.workerRespondExtensionUi(requestId, answer, state.runId);
1443
+ } catch (error) {
1444
+ notify(ctx, `Failed to answer the drone's question: ${error instanceof Error ? error.message : String(error)}`, "error");
1445
+ }
1446
+ }
1119
1447
  async function registerWorkerCommands(pi, state, ctx) {
1120
1448
  if (!state.operatorSession || !state.active || !state.runId)
1121
1449
  return;
@@ -1243,6 +1571,9 @@ function createPiRigExtension(pi, options = {}) {
1243
1571
  }
1244
1572
  }));
1245
1573
  }
1574
+ if (state.operatorSession && state.active && state.runId) {
1575
+ registerOperatorConsoleCommands(pi, state, tryRegister);
1576
+ }
1246
1577
  if (state.active && state.runId) {
1247
1578
  for (const tool of createRigTools({ context: state, client: state.client })) {
1248
1579
  tryRegister(`tool:${String(tool.name ?? "rig-tool")}`, () => pi.registerTool?.({
@@ -1262,23 +1593,21 @@ function createPiRigExtension(pi, options = {}) {
1262
1593
  pi.on?.("session_start", async (_event, ctx) => {
1263
1594
  if (!state.active || !state.runId)
1264
1595
  return;
1265
- setStatus(ctx, "rig", `Rig ${state.runId} \xB7 connecting\u2026`);
1596
+ const shortId = state.runId.slice(0, 8);
1597
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 waiting for worker daemon\u2026`);
1266
1598
  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`]);
1599
+ setTitle(ctx, `Rig \xB7 run ${shortId}`);
1600
+ applyRigTheme(ctx);
1601
+ applyDroneWorkingIndicator(ctx);
1269
1602
  }
1270
1603
  const gateResult = await gate(ctx);
1271
1604
  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
- }
1605
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 bridge disabled (protocol mismatch)`);
1277
1606
  return;
1278
1607
  }
1279
- setStatus(ctx, "rig", `Rig ${state.runId}`);
1608
+ setStatus(ctx, "rig", `drone ${shortId} \xB7 connecting\u2026`);
1280
1609
  const live = gateResult.status === "compatible" ? startOperatorBridge(state, ctx) : undefined;
1281
- startOperatorRunWidget(state, ctx, live);
1610
+ startOperatorRunStatusLine(state, ctx, live);
1282
1611
  if (state.operatorSession && gateResult.status === "compatible") {
1283
1612
  startWorkerSessionMirror(pi, state, ctx);
1284
1613
  registerWorkerCommands(pi, state, ctx);
@@ -0,0 +1,216 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // packages/pi-rig/src/live-mirror.ts
5
+ async function loadPiModules() {
6
+ const components = await import("@earendil-works/pi-coding-agent");
7
+ return { components };
8
+ }
9
+ function createRootContainer() {
10
+ const children = [];
11
+ return {
12
+ addChild(child) {
13
+ children.push(child);
14
+ },
15
+ removeChild(child) {
16
+ const index = children.indexOf(child);
17
+ if (index >= 0)
18
+ children.splice(index, 1);
19
+ },
20
+ clear() {
21
+ children.length = 0;
22
+ },
23
+ render(width) {
24
+ return children.flatMap((child) => child.render(width));
25
+ }
26
+ };
27
+ }
28
+ var DRONE_MESSAGE_TYPE = "rig-drone";
29
+ var DRONE_USER_MESSAGE_TYPE = "rig-drone-user";
30
+ function recordOf(value) {
31
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
32
+ }
33
+ function messageRole(event) {
34
+ const message = recordOf(event.message);
35
+ return message && typeof message.role === "string" ? message.role : null;
36
+ }
37
+ function userMessageText(message) {
38
+ const content = message.content;
39
+ if (typeof content === "string")
40
+ return content;
41
+ if (Array.isArray(content)) {
42
+ return content.flatMap((block) => {
43
+ const record = recordOf(block);
44
+ return record && record.type === "text" && typeof record.text === "string" ? [record.text] : [];
45
+ }).join(`
46
+ `);
47
+ }
48
+ return "";
49
+ }
50
+ async function createLiveMirror(input) {
51
+ const { pi } = input;
52
+ const modules = input.modules ?? await loadPiModules();
53
+ const cwd = input.workerCwd ?? process.cwd();
54
+ const turns = new Map;
55
+ const toolOwners = new Map;
56
+ const userComponents = new Map;
57
+ let streamingTurn = null;
58
+ let sequence = 0;
59
+ let tui = null;
60
+ const requestRender = () => {
61
+ tui?.requestRender?.();
62
+ };
63
+ pi.registerMessageRenderer?.(DRONE_MESSAGE_TYPE, (message) => {
64
+ const details = recordOf(recordOf(message)?.details);
65
+ const key = details && typeof details.key === "string" ? details.key : null;
66
+ return key ? turns.get(key)?.root : undefined;
67
+ });
68
+ pi.registerMessageRenderer?.(DRONE_USER_MESSAGE_TYPE, (message) => {
69
+ const details = recordOf(recordOf(message)?.details);
70
+ const key = details && typeof details.key === "string" ? details.key : null;
71
+ return key ? userComponents.get(key) : undefined;
72
+ });
73
+ const ensureToolComponent = (turn, toolCallId, toolName, args) => {
74
+ const existing = turn.tools.get(toolCallId);
75
+ if (existing)
76
+ return existing;
77
+ const component = new modules.components.ToolExecutionComponent(toolName, toolCallId, args, {}, undefined, tui, cwd);
78
+ turn.root.addChild(component);
79
+ turn.tools.set(toolCallId, component);
80
+ toolOwners.set(toolCallId, turn);
81
+ return component;
82
+ };
83
+ const startAssistantTurn = (message) => {
84
+ const key = `turn-${++sequence}`;
85
+ const root = createRootContainer();
86
+ const assistant = new modules.components.AssistantMessageComponent;
87
+ root.addChild(assistant);
88
+ const turn = { root, assistant, tools: new Map };
89
+ assistant.updateContent(message);
90
+ turns.set(key, turn);
91
+ streamingTurn = turn;
92
+ pi.sendMessage?.({ customType: DRONE_MESSAGE_TYPE, content: "drone turn", display: true, details: { key } }, { triggerTurn: false });
93
+ requestRender();
94
+ };
95
+ const updateAssistantTurn = (message) => {
96
+ const turn = streamingTurn;
97
+ if (!turn)
98
+ return;
99
+ turn.assistant.updateContent(message);
100
+ const content = Array.isArray(message.content) ? message.content : [];
101
+ for (const block of content) {
102
+ const record = recordOf(block);
103
+ if (!record || record.type !== "toolCall")
104
+ continue;
105
+ const id = typeof record.id === "string" ? record.id : null;
106
+ const name = typeof record.name === "string" ? record.name : "tool";
107
+ if (!id)
108
+ continue;
109
+ const component = ensureToolComponent(turn, id, name, record.arguments);
110
+ component.updateArgs(record.arguments);
111
+ }
112
+ requestRender();
113
+ };
114
+ const endAssistantTurn = (message) => {
115
+ const turn = streamingTurn;
116
+ if (!turn)
117
+ return;
118
+ turn.assistant.updateContent(message);
119
+ const stopReason = typeof message.stopReason === "string" ? message.stopReason : "stop";
120
+ if (stopReason === "aborted" || stopReason === "error") {
121
+ const errorText = typeof message.errorMessage === "string" && message.errorMessage ? message.errorMessage : stopReason === "aborted" ? "Operation aborted" : "Error";
122
+ for (const component of turn.tools.values()) {
123
+ component.updateResult({ content: [{ type: "text", text: errorText }], isError: true });
124
+ }
125
+ } else {
126
+ for (const component of turn.tools.values()) {
127
+ component.setArgsComplete();
128
+ }
129
+ }
130
+ streamingTurn = null;
131
+ requestRender();
132
+ };
133
+ const mirrorUserMessage = (message) => {
134
+ const text = userMessageText(message).trim();
135
+ if (!text)
136
+ return;
137
+ const key = `user-${++sequence}`;
138
+ userComponents.set(key, new modules.components.UserMessageComponent(text));
139
+ pi.sendMessage?.({ customType: DRONE_USER_MESSAGE_TYPE, content: text, display: true, details: { key } }, { triggerTurn: false });
140
+ requestRender();
141
+ };
142
+ return {
143
+ captureTui(capturedTui) {
144
+ tui = recordOf(capturedTui);
145
+ return { render: () => [] };
146
+ },
147
+ handleWorkerEvent(event) {
148
+ const type = typeof event.type === "string" ? event.type : null;
149
+ switch (type) {
150
+ case "message_start": {
151
+ const message = recordOf(event.message);
152
+ if (!message)
153
+ return;
154
+ if (messageRole(event) === "assistant")
155
+ startAssistantTurn(message);
156
+ else if (messageRole(event) === "user")
157
+ mirrorUserMessage(message);
158
+ return;
159
+ }
160
+ case "message_update": {
161
+ const message = recordOf(event.message);
162
+ if (message && messageRole(event) === "assistant")
163
+ updateAssistantTurn(message);
164
+ return;
165
+ }
166
+ case "message_end": {
167
+ const message = recordOf(event.message);
168
+ if (message && messageRole(event) === "assistant")
169
+ endAssistantTurn(message);
170
+ return;
171
+ }
172
+ case "tool_execution_start": {
173
+ const id = typeof event.toolCallId === "string" ? event.toolCallId : null;
174
+ const name = typeof event.toolName === "string" ? event.toolName : "tool";
175
+ if (!id)
176
+ return;
177
+ const turn = toolOwners.get(id) ?? streamingTurn;
178
+ if (!turn)
179
+ return;
180
+ ensureToolComponent(turn, id, name, event.args).markExecutionStarted();
181
+ requestRender();
182
+ return;
183
+ }
184
+ case "tool_execution_update": {
185
+ const id = typeof event.toolCallId === "string" ? event.toolCallId : null;
186
+ const component = id ? toolOwners.get(id)?.tools.get(id) : undefined;
187
+ const partial = recordOf(event.partialResult);
188
+ if (component && partial) {
189
+ const content = Array.isArray(partial.content) ? partial.content : [];
190
+ component.updateResult({ ...partial, content, isError: false }, true);
191
+ requestRender();
192
+ }
193
+ return;
194
+ }
195
+ case "tool_execution_end": {
196
+ const id = typeof event.toolCallId === "string" ? event.toolCallId : null;
197
+ const component = id ? toolOwners.get(id)?.tools.get(id) : undefined;
198
+ const result = recordOf(event.result);
199
+ if (component && result) {
200
+ const content = Array.isArray(result.content) ? result.content : [];
201
+ component.updateResult({ ...result, content, isError: event.isError === true });
202
+ requestRender();
203
+ }
204
+ return;
205
+ }
206
+ default:
207
+ return;
208
+ }
209
+ }
210
+ };
211
+ }
212
+ export {
213
+ createLiveMirror,
214
+ DRONE_USER_MESSAGE_TYPE,
215
+ DRONE_MESSAGE_TYPE
216
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h-rig/pi-rig",
3
- "version": "0.0.6-alpha.77",
3
+ "version": "0.0.6-alpha.78",
4
4
  "type": "module",
5
5
  "description": "Rig package",
6
6
  "license": "UNLICENSED",
@@ -33,7 +33,7 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.77"
36
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.78"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": ">=0.79.0",