@h-rig/cli 0.0.6-alpha.41 → 0.0.6-alpha.43

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.
@@ -217,6 +217,10 @@ async function getRunPiCommandsViaServer(context, runId) {
217
217
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
218
218
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
219
219
  }
220
+ async function getRunPiCapabilitiesViaServer(context, runId) {
221
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
222
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
223
+ }
220
224
  async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
221
225
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
222
226
  method: "POST",
@@ -263,7 +267,7 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
263
267
  }
264
268
 
265
269
  // packages/cli/src/commands/_pi-remote-session.ts
266
- import { AgentSession as PiAgentSession } from "@earendil-works/pi-coding-agent";
270
+ import { AgentSession as PiAgentSession, expandPromptTemplate } from "@earendil-works/pi-coding-agent";
267
271
  var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
268
272
  function defaultTransport() {
269
273
  return {
@@ -271,6 +275,7 @@ function defaultTransport() {
271
275
  getMessages: getRunPiMessagesViaServer,
272
276
  getStatus: getRunPiStatusViaServer,
273
277
  getCommands: getRunPiCommandsViaServer,
278
+ getCapabilities: getRunPiCapabilitiesViaServer,
274
279
  sendPrompt: sendRunPiPromptViaServer,
275
280
  sendShell: sendRunPiShellViaServer,
276
281
  runCommand: runRunPiCommandViaServer,
@@ -358,16 +363,18 @@ class RigRemoteSessionController {
358
363
  this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
359
364
  };
360
365
  try {
361
- const [messagesPayload, statusPayload, commandsPayload] = await Promise.all([
366
+ const [messagesPayload, statusPayload, commandsPayload, capabilitiesPayload] = await Promise.all([
362
367
  this.transport.getMessages(this.context, this.runId),
363
368
  this.transport.getStatus(this.context, this.runId),
364
- this.transport.getCommands(this.context, this.runId)
369
+ this.transport.getCommands(this.context, this.runId),
370
+ this.transport.getCapabilities(this.context, this.runId).catch(() => ({ capabilities: null }))
365
371
  ]);
366
372
  const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
367
373
  this.applyStatusPayload(statusPayload);
368
374
  this.session?.replaceRemoteMessages(messages);
369
375
  const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
370
- this.hooks.onCatchUp?.(messages, commands);
376
+ const capabilities = capabilitiesPayload.capabilities && typeof capabilitiesPayload.capabilities === "object" && !Array.isArray(capabilitiesPayload.capabilities) ? capabilitiesPayload.capabilities : null;
377
+ this.hooks.onCatchUp?.(messages, commands, capabilities);
371
378
  catchupDone = true;
372
379
  for (const payload of buffered.splice(0))
373
380
  this.applyEnvelope(payload);
@@ -544,6 +551,20 @@ class RigRemoteAgentSession extends PiAgentSession {
544
551
  replaceRemoteMessages(messages) {
545
552
  this.agent.state.messages = messages;
546
553
  }
554
+ async expandLocalInput(text) {
555
+ let current = text;
556
+ const runner = this.extensionRunner;
557
+ if (runner.hasHandlers("input")) {
558
+ const result = await runner.emitInput(current, undefined, "interactive");
559
+ if (result.action === "handled")
560
+ return null;
561
+ if (result.action === "transform")
562
+ current = result.text;
563
+ }
564
+ current = this._expandSkillCommand(current);
565
+ current = expandPromptTemplate(current, [...this.promptTemplates]);
566
+ return current;
567
+ }
547
568
  async prompt(text, options) {
548
569
  const trimmed = text.trim();
549
570
  if (!trimmed)
@@ -551,6 +572,15 @@ class RigRemoteAgentSession extends PiAgentSession {
551
572
  if (trimmed.startsWith("/")) {
552
573
  if (await this._tryExecuteExtensionCommand(trimmed))
553
574
  return;
575
+ const expanded2 = await this.expandLocalInput(trimmed);
576
+ if (expanded2 === null)
577
+ return;
578
+ if (expanded2 !== trimmed) {
579
+ const behavior2 = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
580
+ options?.preflightResult?.(true);
581
+ await this.remote.sendPrompt(expanded2, behavior2);
582
+ return;
583
+ }
554
584
  await this.remote.sendCommand(trimmed);
555
585
  return;
556
586
  }
@@ -558,15 +588,30 @@ class RigRemoteAgentSession extends PiAgentSession {
558
588
  await this.remote.sendShell(trimmed);
559
589
  return;
560
590
  }
591
+ const expanded = await this.expandLocalInput(trimmed);
592
+ if (expanded === null)
593
+ return;
561
594
  const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
562
595
  options?.preflightResult?.(true);
563
- await this.remote.sendPrompt(trimmed, behavior);
596
+ await this.remote.sendPrompt(expanded, behavior);
564
597
  }
565
598
  async steer(text) {
566
- await this.remote.sendPrompt(text, "steer");
599
+ const trimmed = text.trim();
600
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
601
+ return;
602
+ const expanded = await this.expandLocalInput(trimmed);
603
+ if (expanded === null)
604
+ return;
605
+ await this.remote.sendPrompt(expanded, "steer");
567
606
  }
568
607
  async followUp(text) {
569
- await this.remote.sendPrompt(text, "followUp");
608
+ const trimmed = text.trim();
609
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
610
+ return;
611
+ const expanded = await this.expandLocalInput(trimmed);
612
+ if (expanded === null)
613
+ return;
614
+ await this.remote.sendPrompt(expanded, "followUp");
570
615
  }
571
616
  async abort() {
572
617
  await this.remote.abort();
@@ -679,6 +724,9 @@ function createRemoteBashOperations(controller, excludeFromContext) {
679
724
  };
680
725
  }
681
726
 
727
+ // packages/cli/src/commands/_spinner.ts
728
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
729
+
682
730
  // packages/cli/src/commands/_pi-worker-bridge-extension.ts
683
731
  function recordOf2(value) {
684
732
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -696,6 +744,50 @@ function asText(value) {
696
744
  return String(value);
697
745
  }
698
746
  }
747
+ function names(value, key = "name") {
748
+ if (!Array.isArray(value))
749
+ return [];
750
+ return value.flatMap((entry) => {
751
+ if (typeof entry === "string")
752
+ return [entry];
753
+ const record = recordOf2(entry);
754
+ const name = record?.[key];
755
+ return typeof name === "string" ? [name] : [];
756
+ });
757
+ }
758
+ function renderWorkerCapabilities(capabilities) {
759
+ const lines = ["Worker session capabilities (in effect for this run)"];
760
+ const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
761
+ const activeTools = tools.flatMap((tool) => {
762
+ const record = recordOf2(tool);
763
+ return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
764
+ });
765
+ const inactiveCount = tools.length - activeTools.length;
766
+ lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
767
+ const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
768
+ const extensionLabels = extensions.flatMap((entry) => {
769
+ const record = recordOf2(entry);
770
+ if (!record)
771
+ return [];
772
+ const path = typeof record.path === "string" ? record.path : "";
773
+ const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
774
+ return [short];
775
+ });
776
+ lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
777
+ const hookEvents = names(capabilities.hookEvents);
778
+ const hookList = Array.isArray(capabilities.hookEvents) ? capabilities.hookEvents.map(asText).filter(Boolean) : hookEvents;
779
+ lines.push(` hooks ${hookList.join(", ") || "(none)"}`);
780
+ lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
781
+ lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
782
+ const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
783
+ const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
784
+ const cwd = typeof capabilities.cwd === "string" ? capabilities.cwd : "";
785
+ lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
786
+ if (cwd)
787
+ lines.push(` cwd ${cwd}`);
788
+ lines.push(" (worker commands are in your palette as [worker \u2026] \xB7 /worker shows this again)");
789
+ return lines;
790
+ }
699
791
  async function answerExtensionUiRequest(options, ctx, request) {
700
792
  const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
701
793
  const method = String(request.method ?? request.type ?? "input");
@@ -722,7 +814,7 @@ async function answerExtensionUiRequest(options, ctx, request) {
722
814
  ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
723
815
  }
724
816
  }
725
- var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
817
+ var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "worker", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
726
818
  function registerDaemonCommands(pi, options, ctx, commands, registered) {
727
819
  for (const command of commands) {
728
820
  const record = recordOf2(command);
@@ -750,6 +842,21 @@ function registerDaemonCommands(pi, options, ctx, commands, registered) {
750
842
  function createRigWorkerPiBridgeExtension(options) {
751
843
  return (pi) => {
752
844
  const registeredDaemonCommands = new Set;
845
+ let capabilityLines = null;
846
+ let statusText = "connecting to worker session";
847
+ let busy = true;
848
+ let connected = false;
849
+ let frame = 0;
850
+ let spinnerTimer = null;
851
+ const renderStatus = (ctx) => {
852
+ const prefix = busy ? `${SPINNER_FRAMES[frame]} ` : connected ? "\u25CF " : "\u25CB ";
853
+ ctx.ui.setStatus("rig-worker-pi", `${prefix}${statusText}`);
854
+ };
855
+ const setStatus = (ctx, text, isBusy) => {
856
+ statusText = text;
857
+ busy = isBusy;
858
+ renderStatus(ctx);
859
+ };
753
860
  pi.registerCommand("detach", {
754
861
  description: "Detach from this run; the worker keeps going",
755
862
  handler: async (_args, ctx) => {
@@ -765,31 +872,57 @@ function createRigWorkerPiBridgeExtension(options) {
765
872
  ctx.shutdown();
766
873
  }
767
874
  });
875
+ pi.registerCommand("worker", {
876
+ description: "Show the worker session's real capabilities (tools, extensions, hooks, skills)",
877
+ handler: async (_args, ctx) => {
878
+ if (capabilityLines)
879
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
880
+ else
881
+ ctx.ui.notify("Worker capabilities are not available yet (still connecting).", "info");
882
+ }
883
+ });
768
884
  pi.on("user_bash", (event) => ({
769
885
  operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
770
886
  }));
771
887
  pi.on("session_start", async (_event, ctx) => {
772
- ctx.ui.setTitle("Rig \xB7 worker session");
773
- ctx.ui.setStatus("rig-worker-pi", "connecting to worker session");
774
- ctx.ui.notify(`Attached to Rig run ${options.runId}. Native Pi, remote brain \u2014 /detach exits, /stop cancels the run.`, "info");
888
+ ctx.ui.setTitle("Rig \xB7 enriched bundled Pi");
889
+ setStatus(ctx, "waiting for worker Pi daemon", true);
890
+ spinnerTimer = setInterval(() => {
891
+ frame = (frame + 1) % SPINNER_FRAMES.length;
892
+ if (busy)
893
+ renderStatus(ctx);
894
+ }, 150);
895
+ ctx.ui.notify(`Enriched bundled Pi \u2014 native UI + Rig layers, worker brain. Attached to run ${options.runId}. /worker shows live capabilities \xB7 /detach exits \xB7 /stop cancels.`, "info");
775
896
  if (options.initialMessageSent)
776
897
  ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
777
898
  const nativeUi = ctx.ui;
778
899
  options.controller.setUiHooks({
779
- onStatusText: (text) => ctx.ui.setStatus("rig-worker-pi", text),
780
- onActivity: (label, detail) => ctx.ui.setStatus("rig-worker-pi", [label, detail].filter(Boolean).join(" \u2014 ")),
781
- onConnectionChange: (connected) => ctx.ui.setStatus("rig-worker-pi", connected ? "worker session live" : "worker session disconnected"),
900
+ onStatusText: (text) => setStatus(ctx, text, !connected),
901
+ onActivity: (label, detail) => {
902
+ const active = label !== "idle" && !/complete|ready/i.test(label);
903
+ setStatus(ctx, [label, detail].filter(Boolean).join(" \u2014 "), active);
904
+ if (/agent running|tool:/.test(label))
905
+ ctx.ui.setWidget("rig-worker-capabilities", undefined);
906
+ },
907
+ onConnectionChange: (nextConnected) => {
908
+ connected = nextConnected;
909
+ setStatus(ctx, nextConnected ? "worker session live" : "worker session disconnected", false);
910
+ },
782
911
  onError: (message) => ctx.ui.notify(message, "error"),
783
912
  onExtensionUiRequest: (request) => {
784
913
  answerExtensionUiRequest(options, ctx, request);
785
914
  },
786
- onCatchUp: (messages, commands) => {
915
+ onCatchUp: (messages, commands, capabilities) => {
787
916
  if (nativeUi.appendSessionMessages)
788
917
  nativeUi.appendSessionMessages(messages);
789
918
  const cwd = options.controller.status.cwd;
790
919
  if (nativeUi.setDisplayCwd && cwd)
791
920
  nativeUi.setDisplayCwd(cwd);
792
921
  registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
922
+ if (capabilities) {
923
+ capabilityLines = renderWorkerCapabilities(capabilities);
924
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
925
+ }
793
926
  }
794
927
  });
795
928
  options.controller.connect().catch((error) => {
@@ -797,6 +930,8 @@ function createRigWorkerPiBridgeExtension(options) {
797
930
  });
798
931
  });
799
932
  pi.on("session_shutdown", () => {
933
+ if (spinnerTimer)
934
+ clearInterval(spinnerTimer);
800
935
  options.controller.close();
801
936
  });
802
937
  };
@@ -838,6 +973,9 @@ async function attachRunBundledPiFrontend(context, input) {
838
973
  initialMessageSent: input.steered === true
839
974
  })
840
975
  ],
976
+ noExtensions: true,
977
+ noSkills: true,
978
+ noPromptTemplates: true,
841
979
  noContextFiles: true
842
980
  }
843
981
  });
@@ -872,7 +1010,7 @@ async function attachRunBundledPiFrontend(context, input) {
872
1010
  timelineCursor: null,
873
1011
  steered: input.steered === true,
874
1012
  detached,
875
- rendered: "native bundled Pi frontend with remote worker session runtime"
1013
+ rendered: "enriched bundled Pi frontend with remote worker session runtime"
876
1014
  };
877
1015
  }
878
1016
  export {
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // packages/cli/src/commands/_pi-remote-session.ts
3
- import { AgentSession as PiAgentSession } from "@earendil-works/pi-coding-agent";
3
+ import { AgentSession as PiAgentSession, expandPromptTemplate } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  // packages/cli/src/commands/_server-client.ts
6
6
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
@@ -206,6 +206,10 @@ async function getRunPiCommandsViaServer(context, runId) {
206
206
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
207
207
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
208
208
  }
209
+ async function getRunPiCapabilitiesViaServer(context, runId) {
210
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
211
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
212
+ }
209
213
  async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
210
214
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
211
215
  method: "POST",
@@ -251,6 +255,7 @@ function defaultTransport() {
251
255
  getMessages: getRunPiMessagesViaServer,
252
256
  getStatus: getRunPiStatusViaServer,
253
257
  getCommands: getRunPiCommandsViaServer,
258
+ getCapabilities: getRunPiCapabilitiesViaServer,
254
259
  sendPrompt: sendRunPiPromptViaServer,
255
260
  sendShell: sendRunPiShellViaServer,
256
261
  runCommand: runRunPiCommandViaServer,
@@ -338,16 +343,18 @@ class RigRemoteSessionController {
338
343
  this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
339
344
  };
340
345
  try {
341
- const [messagesPayload, statusPayload, commandsPayload] = await Promise.all([
346
+ const [messagesPayload, statusPayload, commandsPayload, capabilitiesPayload] = await Promise.all([
342
347
  this.transport.getMessages(this.context, this.runId),
343
348
  this.transport.getStatus(this.context, this.runId),
344
- this.transport.getCommands(this.context, this.runId)
349
+ this.transport.getCommands(this.context, this.runId),
350
+ this.transport.getCapabilities(this.context, this.runId).catch(() => ({ capabilities: null }))
345
351
  ]);
346
352
  const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
347
353
  this.applyStatusPayload(statusPayload);
348
354
  this.session?.replaceRemoteMessages(messages);
349
355
  const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
350
- this.hooks.onCatchUp?.(messages, commands);
356
+ const capabilities = capabilitiesPayload.capabilities && typeof capabilitiesPayload.capabilities === "object" && !Array.isArray(capabilitiesPayload.capabilities) ? capabilitiesPayload.capabilities : null;
357
+ this.hooks.onCatchUp?.(messages, commands, capabilities);
351
358
  catchupDone = true;
352
359
  for (const payload of buffered.splice(0))
353
360
  this.applyEnvelope(payload);
@@ -524,6 +531,20 @@ class RigRemoteAgentSession extends PiAgentSession {
524
531
  replaceRemoteMessages(messages) {
525
532
  this.agent.state.messages = messages;
526
533
  }
534
+ async expandLocalInput(text) {
535
+ let current = text;
536
+ const runner = this.extensionRunner;
537
+ if (runner.hasHandlers("input")) {
538
+ const result = await runner.emitInput(current, undefined, "interactive");
539
+ if (result.action === "handled")
540
+ return null;
541
+ if (result.action === "transform")
542
+ current = result.text;
543
+ }
544
+ current = this._expandSkillCommand(current);
545
+ current = expandPromptTemplate(current, [...this.promptTemplates]);
546
+ return current;
547
+ }
527
548
  async prompt(text, options) {
528
549
  const trimmed = text.trim();
529
550
  if (!trimmed)
@@ -531,6 +552,15 @@ class RigRemoteAgentSession extends PiAgentSession {
531
552
  if (trimmed.startsWith("/")) {
532
553
  if (await this._tryExecuteExtensionCommand(trimmed))
533
554
  return;
555
+ const expanded2 = await this.expandLocalInput(trimmed);
556
+ if (expanded2 === null)
557
+ return;
558
+ if (expanded2 !== trimmed) {
559
+ const behavior2 = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
560
+ options?.preflightResult?.(true);
561
+ await this.remote.sendPrompt(expanded2, behavior2);
562
+ return;
563
+ }
534
564
  await this.remote.sendCommand(trimmed);
535
565
  return;
536
566
  }
@@ -538,15 +568,30 @@ class RigRemoteAgentSession extends PiAgentSession {
538
568
  await this.remote.sendShell(trimmed);
539
569
  return;
540
570
  }
571
+ const expanded = await this.expandLocalInput(trimmed);
572
+ if (expanded === null)
573
+ return;
541
574
  const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
542
575
  options?.preflightResult?.(true);
543
- await this.remote.sendPrompt(trimmed, behavior);
576
+ await this.remote.sendPrompt(expanded, behavior);
544
577
  }
545
578
  async steer(text) {
546
- await this.remote.sendPrompt(text, "steer");
579
+ const trimmed = text.trim();
580
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
581
+ return;
582
+ const expanded = await this.expandLocalInput(trimmed);
583
+ if (expanded === null)
584
+ return;
585
+ await this.remote.sendPrompt(expanded, "steer");
547
586
  }
548
587
  async followUp(text) {
549
- await this.remote.sendPrompt(text, "followUp");
588
+ const trimmed = text.trim();
589
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
590
+ return;
591
+ const expanded = await this.expandLocalInput(trimmed);
592
+ if (expanded === null)
593
+ return;
594
+ await this.remote.sendPrompt(expanded, "followUp");
550
595
  }
551
596
  async abort() {
552
597
  await this.remote.abort();
@@ -203,6 +203,10 @@ async function getRunPiCommandsViaServer(context, runId) {
203
203
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
204
204
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
205
205
  }
206
+ async function getRunPiCapabilitiesViaServer(context, runId) {
207
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
208
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
209
+ }
206
210
  async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
207
211
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
208
212
  method: "POST",
@@ -249,7 +253,7 @@ async function buildRunPiEventsWebSocketUrl(context, runId) {
249
253
  }
250
254
 
251
255
  // packages/cli/src/commands/_pi-remote-session.ts
252
- import { AgentSession as PiAgentSession } from "@earendil-works/pi-coding-agent";
256
+ import { AgentSession as PiAgentSession, expandPromptTemplate } from "@earendil-works/pi-coding-agent";
253
257
  var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
254
258
  function defaultTransport() {
255
259
  return {
@@ -257,6 +261,7 @@ function defaultTransport() {
257
261
  getMessages: getRunPiMessagesViaServer,
258
262
  getStatus: getRunPiStatusViaServer,
259
263
  getCommands: getRunPiCommandsViaServer,
264
+ getCapabilities: getRunPiCapabilitiesViaServer,
260
265
  sendPrompt: sendRunPiPromptViaServer,
261
266
  sendShell: sendRunPiShellViaServer,
262
267
  runCommand: runRunPiCommandViaServer,
@@ -344,16 +349,18 @@ class RigRemoteSessionController {
344
349
  this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
345
350
  };
346
351
  try {
347
- const [messagesPayload, statusPayload, commandsPayload] = await Promise.all([
352
+ const [messagesPayload, statusPayload, commandsPayload, capabilitiesPayload] = await Promise.all([
348
353
  this.transport.getMessages(this.context, this.runId),
349
354
  this.transport.getStatus(this.context, this.runId),
350
- this.transport.getCommands(this.context, this.runId)
355
+ this.transport.getCommands(this.context, this.runId),
356
+ this.transport.getCapabilities(this.context, this.runId).catch(() => ({ capabilities: null }))
351
357
  ]);
352
358
  const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
353
359
  this.applyStatusPayload(statusPayload);
354
360
  this.session?.replaceRemoteMessages(messages);
355
361
  const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
356
- this.hooks.onCatchUp?.(messages, commands);
362
+ const capabilities = capabilitiesPayload.capabilities && typeof capabilitiesPayload.capabilities === "object" && !Array.isArray(capabilitiesPayload.capabilities) ? capabilitiesPayload.capabilities : null;
363
+ this.hooks.onCatchUp?.(messages, commands, capabilities);
357
364
  catchupDone = true;
358
365
  for (const payload of buffered.splice(0))
359
366
  this.applyEnvelope(payload);
@@ -549,6 +556,9 @@ function createRemoteBashOperations(controller, excludeFromContext) {
549
556
  };
550
557
  }
551
558
 
559
+ // packages/cli/src/commands/_spinner.ts
560
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
561
+
552
562
  // packages/cli/src/commands/_pi-worker-bridge-extension.ts
553
563
  function recordOf2(value) {
554
564
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -566,6 +576,50 @@ function asText(value) {
566
576
  return String(value);
567
577
  }
568
578
  }
579
+ function names(value, key = "name") {
580
+ if (!Array.isArray(value))
581
+ return [];
582
+ return value.flatMap((entry) => {
583
+ if (typeof entry === "string")
584
+ return [entry];
585
+ const record = recordOf2(entry);
586
+ const name = record?.[key];
587
+ return typeof name === "string" ? [name] : [];
588
+ });
589
+ }
590
+ function renderWorkerCapabilities(capabilities) {
591
+ const lines = ["Worker session capabilities (in effect for this run)"];
592
+ const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
593
+ const activeTools = tools.flatMap((tool) => {
594
+ const record = recordOf2(tool);
595
+ return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
596
+ });
597
+ const inactiveCount = tools.length - activeTools.length;
598
+ lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
599
+ const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
600
+ const extensionLabels = extensions.flatMap((entry) => {
601
+ const record = recordOf2(entry);
602
+ if (!record)
603
+ return [];
604
+ const path = typeof record.path === "string" ? record.path : "";
605
+ const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
606
+ return [short];
607
+ });
608
+ lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
609
+ const hookEvents = names(capabilities.hookEvents);
610
+ const hookList = Array.isArray(capabilities.hookEvents) ? capabilities.hookEvents.map(asText).filter(Boolean) : hookEvents;
611
+ lines.push(` hooks ${hookList.join(", ") || "(none)"}`);
612
+ lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
613
+ lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
614
+ const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
615
+ const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
616
+ const cwd = typeof capabilities.cwd === "string" ? capabilities.cwd : "";
617
+ lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
618
+ if (cwd)
619
+ lines.push(` cwd ${cwd}`);
620
+ lines.push(" (worker commands are in your palette as [worker \u2026] \xB7 /worker shows this again)");
621
+ return lines;
622
+ }
569
623
  async function answerExtensionUiRequest(options, ctx, request) {
570
624
  const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
571
625
  const method = String(request.method ?? request.type ?? "input");
@@ -592,7 +646,7 @@ async function answerExtensionUiRequest(options, ctx, request) {
592
646
  ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
593
647
  }
594
648
  }
595
- var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
649
+ var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "worker", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
596
650
  function registerDaemonCommands(pi, options, ctx, commands, registered) {
597
651
  for (const command of commands) {
598
652
  const record = recordOf2(command);
@@ -620,6 +674,21 @@ function registerDaemonCommands(pi, options, ctx, commands, registered) {
620
674
  function createRigWorkerPiBridgeExtension(options) {
621
675
  return (pi) => {
622
676
  const registeredDaemonCommands = new Set;
677
+ let capabilityLines = null;
678
+ let statusText = "connecting to worker session";
679
+ let busy = true;
680
+ let connected = false;
681
+ let frame = 0;
682
+ let spinnerTimer = null;
683
+ const renderStatus = (ctx) => {
684
+ const prefix = busy ? `${SPINNER_FRAMES[frame]} ` : connected ? "\u25CF " : "\u25CB ";
685
+ ctx.ui.setStatus("rig-worker-pi", `${prefix}${statusText}`);
686
+ };
687
+ const setStatus = (ctx, text, isBusy) => {
688
+ statusText = text;
689
+ busy = isBusy;
690
+ renderStatus(ctx);
691
+ };
623
692
  pi.registerCommand("detach", {
624
693
  description: "Detach from this run; the worker keeps going",
625
694
  handler: async (_args, ctx) => {
@@ -635,31 +704,57 @@ function createRigWorkerPiBridgeExtension(options) {
635
704
  ctx.shutdown();
636
705
  }
637
706
  });
707
+ pi.registerCommand("worker", {
708
+ description: "Show the worker session's real capabilities (tools, extensions, hooks, skills)",
709
+ handler: async (_args, ctx) => {
710
+ if (capabilityLines)
711
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
712
+ else
713
+ ctx.ui.notify("Worker capabilities are not available yet (still connecting).", "info");
714
+ }
715
+ });
638
716
  pi.on("user_bash", (event) => ({
639
717
  operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
640
718
  }));
641
719
  pi.on("session_start", async (_event, ctx) => {
642
- ctx.ui.setTitle("Rig \xB7 worker session");
643
- ctx.ui.setStatus("rig-worker-pi", "connecting to worker session");
644
- ctx.ui.notify(`Attached to Rig run ${options.runId}. Native Pi, remote brain \u2014 /detach exits, /stop cancels the run.`, "info");
720
+ ctx.ui.setTitle("Rig \xB7 enriched bundled Pi");
721
+ setStatus(ctx, "waiting for worker Pi daemon", true);
722
+ spinnerTimer = setInterval(() => {
723
+ frame = (frame + 1) % SPINNER_FRAMES.length;
724
+ if (busy)
725
+ renderStatus(ctx);
726
+ }, 150);
727
+ ctx.ui.notify(`Enriched bundled Pi \u2014 native UI + Rig layers, worker brain. Attached to run ${options.runId}. /worker shows live capabilities \xB7 /detach exits \xB7 /stop cancels.`, "info");
645
728
  if (options.initialMessageSent)
646
729
  ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
647
730
  const nativeUi = ctx.ui;
648
731
  options.controller.setUiHooks({
649
- onStatusText: (text) => ctx.ui.setStatus("rig-worker-pi", text),
650
- onActivity: (label, detail) => ctx.ui.setStatus("rig-worker-pi", [label, detail].filter(Boolean).join(" \u2014 ")),
651
- onConnectionChange: (connected) => ctx.ui.setStatus("rig-worker-pi", connected ? "worker session live" : "worker session disconnected"),
732
+ onStatusText: (text) => setStatus(ctx, text, !connected),
733
+ onActivity: (label, detail) => {
734
+ const active = label !== "idle" && !/complete|ready/i.test(label);
735
+ setStatus(ctx, [label, detail].filter(Boolean).join(" \u2014 "), active);
736
+ if (/agent running|tool:/.test(label))
737
+ ctx.ui.setWidget("rig-worker-capabilities", undefined);
738
+ },
739
+ onConnectionChange: (nextConnected) => {
740
+ connected = nextConnected;
741
+ setStatus(ctx, nextConnected ? "worker session live" : "worker session disconnected", false);
742
+ },
652
743
  onError: (message) => ctx.ui.notify(message, "error"),
653
744
  onExtensionUiRequest: (request) => {
654
745
  answerExtensionUiRequest(options, ctx, request);
655
746
  },
656
- onCatchUp: (messages, commands) => {
747
+ onCatchUp: (messages, commands, capabilities) => {
657
748
  if (nativeUi.appendSessionMessages)
658
749
  nativeUi.appendSessionMessages(messages);
659
750
  const cwd = options.controller.status.cwd;
660
751
  if (nativeUi.setDisplayCwd && cwd)
661
752
  nativeUi.setDisplayCwd(cwd);
662
753
  registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
754
+ if (capabilities) {
755
+ capabilityLines = renderWorkerCapabilities(capabilities);
756
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
757
+ }
663
758
  }
664
759
  });
665
760
  options.controller.connect().catch((error) => {
@@ -667,6 +762,8 @@ function createRigWorkerPiBridgeExtension(options) {
667
762
  });
668
763
  });
669
764
  pi.on("session_shutdown", () => {
765
+ if (spinnerTimer)
766
+ clearInterval(spinnerTimer);
670
767
  options.controller.close();
671
768
  });
672
769
  };
@@ -392,6 +392,10 @@ async function getRunPiCommandsViaServer(context, runId) {
392
392
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
393
393
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
394
394
  }
395
+ async function getRunPiCapabilitiesViaServer(context, runId) {
396
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
397
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
398
+ }
395
399
  async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
396
400
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
397
401
  method: "POST",
@@ -492,6 +496,7 @@ export {
492
496
  getRunPiSessionViaServer,
493
497
  getRunPiMessagesViaServer,
494
498
  getRunPiCommandsViaServer,
499
+ getRunPiCapabilitiesViaServer,
495
500
  getRunLogsViaServer,
496
501
  getRunDetailsViaServer,
497
502
  getGitHubProjectStatusFieldViaServer,