@h-rig/cli 0.0.6-alpha.74 → 0.0.6-alpha.75

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/bin/rig.js CHANGED
@@ -7891,14 +7891,13 @@ function buildOperatorPiEnv(input) {
7891
7891
  ...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
7892
7892
  };
7893
7893
  }
7894
- async function attachRunBundledPiFrontend(context, input) {
7895
- const tempSessionDir = mkdtempSync(join3(tmpdir(), "rig-pi-frontend-sessions-"));
7894
+ async function prepareOperatorConsole(context, runId, tempSessionDir) {
7896
7895
  const server = await ensureServerForCli(context.projectRoot);
7897
7896
  let sessionFileArg = [];
7898
7897
  try {
7899
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(input.runId)}/session-file`);
7898
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/session-file`);
7900
7899
  if (payload?.ok && typeof payload.content === "string" && payload.content.trim()) {
7901
- const fileName = typeof payload.fileName === "string" && payload.fileName.trim() ? payload.fileName.replace(/[^A-Za-z0-9._-]/g, "_") : `rig-run-${input.runId}.jsonl`;
7900
+ const fileName = typeof payload.fileName === "string" && payload.fileName.trim() ? payload.fileName.replace(/[^A-Za-z0-9._-]/g, "_") : `rig-run-${runId}.jsonl`;
7902
7901
  const localSessionPath = join3(tempSessionDir, fileName);
7903
7902
  const content = payload.content.split(`
7904
7903
  `).map((line, index) => {
@@ -7917,6 +7916,11 @@ async function attachRunBundledPiFrontend(context, input) {
7917
7916
  sessionFileArg = ["--session", localSessionPath];
7918
7917
  }
7919
7918
  } catch {}
7919
+ return { server, sessionFileArg };
7920
+ }
7921
+ async function attachRunBundledPiFrontend(context, input) {
7922
+ const tempSessionDir = mkdtempSync(join3(tmpdir(), "rig-pi-frontend-sessions-"));
7923
+ const { server, sessionFileArg } = await withSpinner(`Opening Pi console for run ${input.runId}\u2026`, () => prepareOperatorConsole(context, input.runId, tempSessionDir), { outputMode: context.outputMode });
7920
7924
  const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
7921
7925
  runId: input.runId,
7922
7926
  serverUrl: server.baseUrl,
@@ -8747,6 +8751,18 @@ import { computeRigStats } from "@rig/runtime/control-plane/native/run-stats";
8747
8751
  import { intro as intro3, log as log4, note as note4, outro as outro3 } from "@clack/prompts";
8748
8752
  import pc5 from "picocolors";
8749
8753
  var TOP_LEVEL_SECTIONS = [
8754
+ {
8755
+ title: "Pi console",
8756
+ subtitle: "the operator surface \u2014 every attach opens the run's FULL Pi session",
8757
+ commands: [
8758
+ { command: "rig run attach <run-id>", description: "Open the run's Pi console: complete transcript (live AND finished runs), worker turns streaming in live." },
8759
+ { command: "type a message", description: "Inside the console, plain text steers the worker mid-turn \u2014 it lands in the agent's context, not a local model." },
8760
+ { command: "!<command>", description: "Inside the console, runs shell in the WORKER's workspace; output streams back into the transcript." },
8761
+ { command: "/<command>", description: "The palette includes the WORKER session's slash commands ([worker]-tagged) next to /rig." },
8762
+ { command: "/rig abort | /rig stop", description: "Abort the worker's current turn, or stop the whole run, from inside the console." },
8763
+ { command: "rig run steer <id> --message <text>", description: "Steer without attaching \u2014 queue a message; the worker picks it up within seconds." }
8764
+ ]
8765
+ },
8750
8766
  {
8751
8767
  title: "Start here",
8752
8768
  subtitle: "one-time setup, pick a server",
@@ -8761,9 +8777,9 @@ var TOP_LEVEL_SECTIONS = [
8761
8777
  subtitle: "find a task, put an agent on it, answer what it asks",
8762
8778
  commands: [
8763
8779
  { command: "rig task list", description: "What's on the board (from the selected source/server)." },
8764
- { command: "rig task run --next", description: "Dispatch an agent; interactive mode opens the native Pi session." },
8780
+ { command: "rig task run --next", description: "Dispatch an agent on the next ready task; interactive mode drops you into its Pi console." },
8765
8781
  { command: "rig run status", description: "Active and recent runs at a glance." },
8766
- { command: "rig run attach <id> --follow", description: "Join the live Pi session (worker keeps running if you /detach)." },
8782
+ { command: "rig run attach <id> --follow", description: "Same as plain attach \u2014 the Pi console is always the interactive surface." },
8767
8783
  { command: "rig inbox approvals", description: "Approvals workers are waiting on (then `rig inbox approve \u2026`)." }
8768
8784
  ]
8769
8785
  },
@@ -8842,29 +8858,35 @@ var PRIMARY_GROUPS = [
8842
8858
  },
8843
8859
  {
8844
8860
  name: "run",
8845
- summary: "Observe, attach to, and control Rig runs.",
8846
- usage: ["rig run <list|status|show|attach|stop> [options]"],
8861
+ summary: "Observe, attach to, and control Rig runs. `attach` opens the Pi console \u2014 the full worker session, live.",
8862
+ usage: ["rig run <list|status|show|attach|steer|stop|resume|restart> [options]"],
8847
8863
  commands: [
8848
8864
  { command: "list", description: "List recent runs from the selected server or local state.", primary: true },
8849
8865
  { command: "status", description: "Render active and recent run groups.", primary: true },
8850
8866
  { command: "show <id>|--run <id> [--raw]", description: "Show a human run summary; --raw prints the full payload.", primary: true },
8851
- { command: "attach <run-id>|--run <id> [--follow]", description: "Attach to the run; --follow opens the enriched bundled Pi (worker brain).", primary: true },
8867
+ { command: "attach <run-id>|--run <id>", description: "Open the run's Pi console \u2014 see 'Inside the console' below. Works on live AND finished runs.", primary: true },
8868
+ { command: "steer <run-id> --message <text>", description: "Queue a steering message into a live worker without attaching \u2014 delivered into the agent's context within seconds.", primary: true },
8852
8869
  { command: "stop [<run-id>|--run <id>]", description: "Request stop for one run or local active runs.", primary: true },
8853
- { command: "steer <run-id> --message <text>", description: "Queue a steering message into a live worker without attaching." },
8854
- { command: "timeline --run <id> [--follow]", description: "Stream raw run timeline events." },
8870
+ { command: "resume [<run-id>]", description: "Resume an interrupted run on the selected server (defaults to the most recent resumable)." },
8871
+ { command: "restart [<run-id>]", description: "Re-dispatch a run from a clean runtime, reopening its Pi session where possible." },
8872
+ { command: "timeline --run <id> [--follow]", description: "Stream raw run timeline events (scripts; humans should attach)." },
8855
8873
  { command: "replay <run-id>|--run <id> [--with-session]", description: "Print the run's consolidated run.jsonl as a merged timeline; --with-session interleaves the Pi session log." },
8856
- { command: "resume", description: "Resume the most recent interrupted local run." },
8857
- { command: "restart", description: "Restart the most recent local run from a clean runtime." },
8858
8874
  { command: "delete|cleanup", description: "Remove completed run records/artifacts." }
8859
8875
  ],
8860
8876
  examples: [
8877
+ "rig run attach <run-id> # full session console \u2014 live mirror, steering, worker shell",
8878
+ "rig run steer <run-id> --message 'focus on the failing test first'",
8861
8879
  "rig run list",
8862
- "rig run status",
8863
8880
  "rig run show <run-id>",
8864
- "rig run attach <run-id> --follow",
8865
8881
  "rig run stop <run-id>"
8866
8882
  ],
8867
- next: ["Use `rig task run --next` to create a new run.", "Use `--json` when scripts need the full structured record."]
8883
+ next: [
8884
+ "Inside the console: the run's COMPLETE transcript loads on open (finished runs too), and new worker turns stream in live.",
8885
+ "Inside the console: plain text = steering into the worker's context \xB7 !<cmd> = shell in the WORKER workspace \xB7 /<cmd> includes the worker session's commands \xB7 /rig abort stops the current turn.",
8886
+ "The console is read-write but remote-only: nothing runs on your machine; the worker keeps running when you exit.",
8887
+ "Use `rig task run --next` to create a new run; interactive mode drops you straight into its console.",
8888
+ "Use `--json` when scripts need the full structured record."
8889
+ ]
8868
8890
  },
8869
8891
  {
8870
8892
  name: "inbox",
@@ -9087,7 +9109,7 @@ function renderGroup(group) {
9087
9109
  function renderTopLevelHelp() {
9088
9110
  return [
9089
9111
  `${heading("rig")} ${pc5.dim("\u2014 server-owned task/run control plane for Pi-backed engineering work")}`,
9090
- pc5.dim("Current path: select a server, choose a task, submit a run, attach with native Pi, clear inbox gates."),
9112
+ pc5.dim("The loop: pick a task, dispatch an agent, open its Pi console (`rig run attach <id>`) to watch and steer it live, clear inbox gates, merge."),
9091
9113
  "",
9092
9114
  ...TOP_LEVEL_SECTIONS.flatMap((section) => [
9093
9115
  `${pc5.bold(pc5.magenta(`\u25C7 ${section.title}`))} \u2014 ${pc5.dim(section.subtitle)}`,
@@ -9171,7 +9193,7 @@ function printTopLevelHelp(state = {}) {
9171
9193
  commandLine("--dry-run", "Print the command plan without mutating state.")
9172
9194
  ].join(`
9173
9195
  `), "Global options");
9174
- outro3("init \u2192 task run \u2192 watch \u2192 inbox \u2192 merged.");
9196
+ outro3("init \u2192 task run \u2192 attach (Pi console: watch + steer live) \u2192 inbox \u2192 merged.");
9175
9197
  }
9176
9198
  function printAdvancedHelp() {
9177
9199
  if (!shouldUseClackOutput2()) {
@@ -3,6 +3,18 @@
3
3
  import { intro, log, note, outro } from "@clack/prompts";
4
4
  import pc from "picocolors";
5
5
  var TOP_LEVEL_SECTIONS = [
6
+ {
7
+ title: "Pi console",
8
+ subtitle: "the operator surface \u2014 every attach opens the run's FULL Pi session",
9
+ commands: [
10
+ { command: "rig run attach <run-id>", description: "Open the run's Pi console: complete transcript (live AND finished runs), worker turns streaming in live." },
11
+ { command: "type a message", description: "Inside the console, plain text steers the worker mid-turn \u2014 it lands in the agent's context, not a local model." },
12
+ { command: "!<command>", description: "Inside the console, runs shell in the WORKER's workspace; output streams back into the transcript." },
13
+ { command: "/<command>", description: "The palette includes the WORKER session's slash commands ([worker]-tagged) next to /rig." },
14
+ { command: "/rig abort | /rig stop", description: "Abort the worker's current turn, or stop the whole run, from inside the console." },
15
+ { command: "rig run steer <id> --message <text>", description: "Steer without attaching \u2014 queue a message; the worker picks it up within seconds." }
16
+ ]
17
+ },
6
18
  {
7
19
  title: "Start here",
8
20
  subtitle: "one-time setup, pick a server",
@@ -17,9 +29,9 @@ var TOP_LEVEL_SECTIONS = [
17
29
  subtitle: "find a task, put an agent on it, answer what it asks",
18
30
  commands: [
19
31
  { command: "rig task list", description: "What's on the board (from the selected source/server)." },
20
- { command: "rig task run --next", description: "Dispatch an agent; interactive mode opens the native Pi session." },
32
+ { command: "rig task run --next", description: "Dispatch an agent on the next ready task; interactive mode drops you into its Pi console." },
21
33
  { command: "rig run status", description: "Active and recent runs at a glance." },
22
- { command: "rig run attach <id> --follow", description: "Join the live Pi session (worker keeps running if you /detach)." },
34
+ { command: "rig run attach <id> --follow", description: "Same as plain attach \u2014 the Pi console is always the interactive surface." },
23
35
  { command: "rig inbox approvals", description: "Approvals workers are waiting on (then `rig inbox approve \u2026`)." }
24
36
  ]
25
37
  },
@@ -98,29 +110,35 @@ var PRIMARY_GROUPS = [
98
110
  },
99
111
  {
100
112
  name: "run",
101
- summary: "Observe, attach to, and control Rig runs.",
102
- usage: ["rig run <list|status|show|attach|stop> [options]"],
113
+ summary: "Observe, attach to, and control Rig runs. `attach` opens the Pi console \u2014 the full worker session, live.",
114
+ usage: ["rig run <list|status|show|attach|steer|stop|resume|restart> [options]"],
103
115
  commands: [
104
116
  { command: "list", description: "List recent runs from the selected server or local state.", primary: true },
105
117
  { command: "status", description: "Render active and recent run groups.", primary: true },
106
118
  { command: "show <id>|--run <id> [--raw]", description: "Show a human run summary; --raw prints the full payload.", primary: true },
107
- { command: "attach <run-id>|--run <id> [--follow]", description: "Attach to the run; --follow opens the enriched bundled Pi (worker brain).", primary: true },
119
+ { command: "attach <run-id>|--run <id>", description: "Open the run's Pi console \u2014 see 'Inside the console' below. Works on live AND finished runs.", primary: true },
120
+ { command: "steer <run-id> --message <text>", description: "Queue a steering message into a live worker without attaching \u2014 delivered into the agent's context within seconds.", primary: true },
108
121
  { command: "stop [<run-id>|--run <id>]", description: "Request stop for one run or local active runs.", primary: true },
109
- { command: "steer <run-id> --message <text>", description: "Queue a steering message into a live worker without attaching." },
110
- { command: "timeline --run <id> [--follow]", description: "Stream raw run timeline events." },
122
+ { command: "resume [<run-id>]", description: "Resume an interrupted run on the selected server (defaults to the most recent resumable)." },
123
+ { command: "restart [<run-id>]", description: "Re-dispatch a run from a clean runtime, reopening its Pi session where possible." },
124
+ { command: "timeline --run <id> [--follow]", description: "Stream raw run timeline events (scripts; humans should attach)." },
111
125
  { command: "replay <run-id>|--run <id> [--with-session]", description: "Print the run's consolidated run.jsonl as a merged timeline; --with-session interleaves the Pi session log." },
112
- { command: "resume", description: "Resume the most recent interrupted local run." },
113
- { command: "restart", description: "Restart the most recent local run from a clean runtime." },
114
126
  { command: "delete|cleanup", description: "Remove completed run records/artifacts." }
115
127
  ],
116
128
  examples: [
129
+ "rig run attach <run-id> # full session console \u2014 live mirror, steering, worker shell",
130
+ "rig run steer <run-id> --message 'focus on the failing test first'",
117
131
  "rig run list",
118
- "rig run status",
119
132
  "rig run show <run-id>",
120
- "rig run attach <run-id> --follow",
121
133
  "rig run stop <run-id>"
122
134
  ],
123
- next: ["Use `rig task run --next` to create a new run.", "Use `--json` when scripts need the full structured record."]
135
+ next: [
136
+ "Inside the console: the run's COMPLETE transcript loads on open (finished runs too), and new worker turns stream in live.",
137
+ "Inside the console: plain text = steering into the worker's context \xB7 !<cmd> = shell in the WORKER workspace \xB7 /<cmd> includes the worker session's commands \xB7 /rig abort stops the current turn.",
138
+ "The console is read-write but remote-only: nothing runs on your machine; the worker keeps running when you exit.",
139
+ "Use `rig task run --next` to create a new run; interactive mode drops you straight into its console.",
140
+ "Use `--json` when scripts need the full structured record."
141
+ ]
124
142
  },
125
143
  {
126
144
  name: "inbox",
@@ -343,7 +361,7 @@ function renderGroup(group) {
343
361
  function renderTopLevelHelp() {
344
362
  return [
345
363
  `${heading("rig")} ${pc.dim("\u2014 server-owned task/run control plane for Pi-backed engineering work")}`,
346
- pc.dim("Current path: select a server, choose a task, submit a run, attach with native Pi, clear inbox gates."),
364
+ pc.dim("The loop: pick a task, dispatch an agent, open its Pi console (`rig run attach <id>`) to watch and steer it live, clear inbox gates, merge."),
347
365
  "",
348
366
  ...TOP_LEVEL_SECTIONS.flatMap((section) => [
349
367
  `${pc.bold(pc.magenta(`\u25C7 ${section.title}`))} \u2014 ${pc.dim(section.subtitle)}`,
@@ -430,7 +448,7 @@ function printTopLevelHelp(state = {}) {
430
448
  commandLine("--dry-run", "Print the command plan without mutating state.")
431
449
  ].join(`
432
450
  `), "Global options");
433
- outro("init \u2192 task run \u2192 watch \u2192 inbox \u2192 merged.");
451
+ outro("init \u2192 task run \u2192 attach (Pi console: watch + steer live) \u2192 inbox \u2192 merged.");
434
452
  }
435
453
  function printAdvancedHelp() {
436
454
  if (!shouldUseClackOutput()) {
@@ -574,98 +574,6 @@ import { tmpdir } from "os";
574
574
  import { join } from "path";
575
575
  import { main as runPiMain } from "@earendil-works/pi-coding-agent";
576
576
  import createPiRigExtension from "@rig/pi-rig";
577
- function setTemporaryEnv(updates) {
578
- const previous = new Map;
579
- for (const [key, value] of Object.entries(updates)) {
580
- previous.set(key, process.env[key]);
581
- process.env[key] = value;
582
- }
583
- return () => {
584
- for (const [key, value] of previous) {
585
- if (value === undefined)
586
- delete process.env[key];
587
- else
588
- process.env[key] = value;
589
- }
590
- };
591
- }
592
- function buildOperatorPiEnv(input) {
593
- return {
594
- PI_CODING_AGENT_SESSION_DIR: input.sessionDir,
595
- PI_SKIP_VERSION_CHECK: "1",
596
- RIG_PI_OPERATOR_SESSION: "1",
597
- RIG_RUN_ID: input.runId,
598
- RIG_SERVER_URL: input.serverUrl,
599
- ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {},
600
- ...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
601
- };
602
- }
603
- async function attachRunBundledPiFrontend(context, input) {
604
- const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
605
- const server = await ensureServerForCli(context.projectRoot);
606
- let sessionFileArg = [];
607
- try {
608
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(input.runId)}/session-file`);
609
- if (payload?.ok && typeof payload.content === "string" && payload.content.trim()) {
610
- const fileName = typeof payload.fileName === "string" && payload.fileName.trim() ? payload.fileName.replace(/[^A-Za-z0-9._-]/g, "_") : `rig-run-${input.runId}.jsonl`;
611
- const localSessionPath = join(tempSessionDir, fileName);
612
- const content = payload.content.split(`
613
- `).map((line, index) => {
614
- if (index > 0 || !line.trim())
615
- return line;
616
- try {
617
- const header = JSON.parse(line);
618
- if (header.type === "session" && typeof header.cwd === "string") {
619
- return JSON.stringify({ ...header, cwd: process.cwd() });
620
- }
621
- } catch {}
622
- return line;
623
- }).join(`
624
- `);
625
- writeFileSync2(localSessionPath, content);
626
- sessionFileArg = ["--session", localSessionPath];
627
- }
628
- } catch {}
629
- const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
630
- runId: input.runId,
631
- serverUrl: server.baseUrl,
632
- authToken: server.authToken,
633
- serverProjectRoot: server.serverProjectRoot,
634
- sessionDir: tempSessionDir
635
- }));
636
- const piRigExtensionFactory = (pi) => {
637
- createPiRigExtension(pi);
638
- };
639
- let detached = false;
640
- try {
641
- await runPiMain([
642
- "--no-extensions",
643
- "--no-skills",
644
- "--no-prompt-templates",
645
- "--no-context-files",
646
- ...sessionFileArg
647
- ], {
648
- extensionFactories: [piRigExtensionFactory]
649
- });
650
- detached = true;
651
- } finally {
652
- restoreEnv();
653
- rmSync(tempSessionDir, { recursive: true, force: true });
654
- }
655
- let run = { runId: input.runId, status: "unknown" };
656
- try {
657
- run = await getRunDetailsViaServer(context, input.runId);
658
- } catch {}
659
- return {
660
- run,
661
- logs: [],
662
- timeline: [],
663
- timelineCursor: null,
664
- steered: input.steered === true,
665
- detached,
666
- rendered: "stock Pi operator console with the pi-rig extension"
667
- };
668
- }
669
577
 
670
578
  // packages/cli/src/commands/_async-ui.ts
671
579
  import pc from "picocolors";
@@ -788,6 +696,104 @@ async function withSpinner(label, work, options = {}) {
788
696
  }
789
697
  }
790
698
 
699
+ // packages/cli/src/commands/_pi-frontend.ts
700
+ function setTemporaryEnv(updates) {
701
+ const previous = new Map;
702
+ for (const [key, value] of Object.entries(updates)) {
703
+ previous.set(key, process.env[key]);
704
+ process.env[key] = value;
705
+ }
706
+ return () => {
707
+ for (const [key, value] of previous) {
708
+ if (value === undefined)
709
+ delete process.env[key];
710
+ else
711
+ process.env[key] = value;
712
+ }
713
+ };
714
+ }
715
+ function buildOperatorPiEnv(input) {
716
+ return {
717
+ PI_CODING_AGENT_SESSION_DIR: input.sessionDir,
718
+ PI_SKIP_VERSION_CHECK: "1",
719
+ RIG_PI_OPERATOR_SESSION: "1",
720
+ RIG_RUN_ID: input.runId,
721
+ RIG_SERVER_URL: input.serverUrl,
722
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {},
723
+ ...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
724
+ };
725
+ }
726
+ async function prepareOperatorConsole(context, runId, tempSessionDir) {
727
+ const server = await ensureServerForCli(context.projectRoot);
728
+ let sessionFileArg = [];
729
+ try {
730
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/session-file`);
731
+ if (payload?.ok && typeof payload.content === "string" && payload.content.trim()) {
732
+ const fileName = typeof payload.fileName === "string" && payload.fileName.trim() ? payload.fileName.replace(/[^A-Za-z0-9._-]/g, "_") : `rig-run-${runId}.jsonl`;
733
+ const localSessionPath = join(tempSessionDir, fileName);
734
+ const content = payload.content.split(`
735
+ `).map((line, index) => {
736
+ if (index > 0 || !line.trim())
737
+ return line;
738
+ try {
739
+ const header = JSON.parse(line);
740
+ if (header.type === "session" && typeof header.cwd === "string") {
741
+ return JSON.stringify({ ...header, cwd: process.cwd() });
742
+ }
743
+ } catch {}
744
+ return line;
745
+ }).join(`
746
+ `);
747
+ writeFileSync2(localSessionPath, content);
748
+ sessionFileArg = ["--session", localSessionPath];
749
+ }
750
+ } catch {}
751
+ return { server, sessionFileArg };
752
+ }
753
+ async function attachRunBundledPiFrontend(context, input) {
754
+ const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
755
+ const { server, sessionFileArg } = await withSpinner(`Opening Pi console for run ${input.runId}\u2026`, () => prepareOperatorConsole(context, input.runId, tempSessionDir), { outputMode: context.outputMode });
756
+ const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
757
+ runId: input.runId,
758
+ serverUrl: server.baseUrl,
759
+ authToken: server.authToken,
760
+ serverProjectRoot: server.serverProjectRoot,
761
+ sessionDir: tempSessionDir
762
+ }));
763
+ const piRigExtensionFactory = (pi) => {
764
+ createPiRigExtension(pi);
765
+ };
766
+ let detached = false;
767
+ try {
768
+ await runPiMain([
769
+ "--no-extensions",
770
+ "--no-skills",
771
+ "--no-prompt-templates",
772
+ "--no-context-files",
773
+ ...sessionFileArg
774
+ ], {
775
+ extensionFactories: [piRigExtensionFactory]
776
+ });
777
+ detached = true;
778
+ } finally {
779
+ restoreEnv();
780
+ rmSync(tempSessionDir, { recursive: true, force: true });
781
+ }
782
+ let run = { runId: input.runId, status: "unknown" };
783
+ try {
784
+ run = await getRunDetailsViaServer(context, input.runId);
785
+ } catch {}
786
+ return {
787
+ run,
788
+ logs: [],
789
+ timeline: [],
790
+ timelineCursor: null,
791
+ steered: input.steered === true,
792
+ detached,
793
+ rendered: "stock Pi operator console with the pi-rig extension"
794
+ };
795
+ }
796
+
791
797
  // packages/cli/src/commands/_operator-view.ts
792
798
  var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
793
799
  function runStatusFromPayload(payload) {
@@ -129,6 +129,11 @@ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
129
129
  // packages/cli/src/commands/_server-client.ts
130
130
  var scopedGitHubBearerTokens = new Map;
131
131
  var serverPhaseListener = null;
132
+ function setServerPhaseListener(listener) {
133
+ const previous = serverPhaseListener;
134
+ serverPhaseListener = listener;
135
+ return previous;
136
+ }
132
137
  function reportServerPhase(label) {
133
138
  serverPhaseListener?.(label);
134
139
  }
@@ -336,6 +341,127 @@ var RESUMABLE_RUN_STATUSES = new Set([
336
341
  "needs_attention"
337
342
  ]);
338
343
 
344
+ // packages/cli/src/commands/_async-ui.ts
345
+ import pc from "picocolors";
346
+
347
+ // packages/cli/src/commands/_spinner.ts
348
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
349
+ function createTtySpinner(input) {
350
+ const output = input.output ?? process.stdout;
351
+ const isTty = output.isTTY === true;
352
+ const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
353
+ let label = input.label;
354
+ let frame = 0;
355
+ let paused = false;
356
+ let stopped = false;
357
+ let lastPrintedLabel = "";
358
+ const render = () => {
359
+ if (stopped || paused)
360
+ return;
361
+ if (!isTty) {
362
+ if (label !== lastPrintedLabel) {
363
+ output.write(`${label}
364
+ `);
365
+ lastPrintedLabel = label;
366
+ }
367
+ return;
368
+ }
369
+ frame = (frame + 1) % frames.length;
370
+ const glyph = frames[frame] ?? frames[0] ?? "";
371
+ output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
372
+ };
373
+ const clearLine = () => {
374
+ if (isTty)
375
+ output.write("\r\x1B[2K");
376
+ };
377
+ render();
378
+ const timer = isTty ? setInterval(render, input.intervalMs ?? 120) : null;
379
+ return {
380
+ setLabel(next) {
381
+ label = next;
382
+ render();
383
+ },
384
+ pause() {
385
+ paused = true;
386
+ clearLine();
387
+ },
388
+ resume() {
389
+ if (stopped)
390
+ return;
391
+ paused = false;
392
+ render();
393
+ },
394
+ stop(finalLine) {
395
+ if (stopped)
396
+ return;
397
+ stopped = true;
398
+ if (timer)
399
+ clearInterval(timer);
400
+ clearLine();
401
+ if (finalLine)
402
+ output.write(`${finalLine}
403
+ `);
404
+ }
405
+ };
406
+ }
407
+
408
+ // packages/cli/src/commands/_async-ui.ts
409
+ var CLACK_SPINNER_FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
410
+ var DONE_SYMBOL = pc.green("\u25C7");
411
+ var FAIL_SYMBOL = pc.red("\u25A0");
412
+ var activeUpdate = null;
413
+ async function withSpinner(label, work, options = {}) {
414
+ if (options.outputMode === "json") {
415
+ return work(() => {});
416
+ }
417
+ if (activeUpdate) {
418
+ const outer = activeUpdate;
419
+ outer(label);
420
+ return work(outer);
421
+ }
422
+ const output = options.output ?? process.stderr;
423
+ const isTty = output.isTTY === true;
424
+ let lastLabel = label;
425
+ if (!isTty) {
426
+ output.write(`${label}
427
+ `);
428
+ const update2 = (next) => {
429
+ lastLabel = next;
430
+ };
431
+ activeUpdate = update2;
432
+ const previousListener2 = setServerPhaseListener(update2);
433
+ try {
434
+ return await work(update2);
435
+ } finally {
436
+ activeUpdate = null;
437
+ setServerPhaseListener(previousListener2);
438
+ }
439
+ }
440
+ const spinner = createTtySpinner({
441
+ label,
442
+ output,
443
+ frames: CLACK_SPINNER_FRAMES,
444
+ styleFrame: (frame) => pc.magenta(frame)
445
+ });
446
+ const update = (next) => {
447
+ lastLabel = next;
448
+ spinner.setLabel(next);
449
+ };
450
+ activeUpdate = update;
451
+ const previousListener = setServerPhaseListener(update);
452
+ try {
453
+ const result = await work(update);
454
+ spinner.stop(options.doneLabel ? `${DONE_SYMBOL} ${options.doneLabel}` : undefined);
455
+ return result;
456
+ } catch (error) {
457
+ spinner.stop(`${FAIL_SYMBOL} ${lastLabel}`);
458
+ throw error;
459
+ } finally {
460
+ activeUpdate = null;
461
+ setServerPhaseListener(previousListener);
462
+ }
463
+ }
464
+
339
465
  // packages/cli/src/commands/_pi-frontend.ts
340
466
  function setTemporaryEnv(updates) {
341
467
  const previous = new Map;
@@ -363,14 +489,13 @@ function buildOperatorPiEnv(input) {
363
489
  ...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
364
490
  };
365
491
  }
366
- async function attachRunBundledPiFrontend(context, input) {
367
- const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
492
+ async function prepareOperatorConsole(context, runId, tempSessionDir) {
368
493
  const server = await ensureServerForCli(context.projectRoot);
369
494
  let sessionFileArg = [];
370
495
  try {
371
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(input.runId)}/session-file`);
496
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/session-file`);
372
497
  if (payload?.ok && typeof payload.content === "string" && payload.content.trim()) {
373
- const fileName = typeof payload.fileName === "string" && payload.fileName.trim() ? payload.fileName.replace(/[^A-Za-z0-9._-]/g, "_") : `rig-run-${input.runId}.jsonl`;
498
+ const fileName = typeof payload.fileName === "string" && payload.fileName.trim() ? payload.fileName.replace(/[^A-Za-z0-9._-]/g, "_") : `rig-run-${runId}.jsonl`;
374
499
  const localSessionPath = join(tempSessionDir, fileName);
375
500
  const content = payload.content.split(`
376
501
  `).map((line, index) => {
@@ -389,6 +514,11 @@ async function attachRunBundledPiFrontend(context, input) {
389
514
  sessionFileArg = ["--session", localSessionPath];
390
515
  }
391
516
  } catch {}
517
+ return { server, sessionFileArg };
518
+ }
519
+ async function attachRunBundledPiFrontend(context, input) {
520
+ const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
521
+ const { server, sessionFileArg } = await withSpinner(`Opening Pi console for run ${input.runId}\u2026`, () => prepareOperatorConsole(context, input.runId, tempSessionDir), { outputMode: context.outputMode });
392
522
  const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
393
523
  runId: input.runId,
394
524
  serverUrl: server.baseUrl,