pi-subagents 0.49.0 → 0.50.0

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/agents/oracle.md +7 -5
  3. package/agents/researcher.md +1 -1
  4. package/agents/reviewer.md +2 -2
  5. package/agents/scout.md +1 -1
  6. package/agents/worker.md +1 -1
  7. package/docs/agents.md +2 -0
  8. package/docs/configuration.md +47 -3
  9. package/docs/extension-api.md +36 -0
  10. package/docs/missions.md +1 -1
  11. package/docs/observability.md +16 -0
  12. package/docs/tool-reference.md +19 -2
  13. package/docs/workflows.md +2 -2
  14. package/package.json +1 -1
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  16. package/skills/pi-subagents/references/execution-controls.md +5 -4
  17. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +10 -0
  21. package/src/api/external-runs.ts +174 -84
  22. package/src/api/preflight.ts +12 -6
  23. package/src/extension/config.ts +33 -0
  24. package/src/extension/index.ts +48 -5
  25. package/src/extension/public-execution.ts +3 -1
  26. package/src/extension/rpc.ts +5 -1
  27. package/src/extension/schemas.ts +2 -1
  28. package/src/extension/tool-description.ts +4 -2
  29. package/src/inspectors/herdr/actions.ts +3 -7
  30. package/src/inspectors/herdr/project-panes.ts +2 -6
  31. package/src/inspectors/herdr/shell-command.ts +16 -0
  32. package/src/intercom/intercom-bridge.ts +2 -3
  33. package/src/intercom/native-supervisor-channel.ts +19 -42
  34. package/src/missions/goal-driver.ts +3 -1
  35. package/src/runs/background/active-run-index.ts +71 -1
  36. package/src/runs/background/async-execution.ts +49 -7
  37. package/src/runs/background/async-job-tracker.ts +1 -0
  38. package/src/runs/background/async-resume.ts +3 -5
  39. package/src/runs/background/async-status-snapshot.ts +277 -0
  40. package/src/runs/background/async-status.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +2 -2
  42. package/src/runs/background/completion-replay.ts +11 -1
  43. package/src/runs/background/fleet-view.ts +3 -1
  44. package/src/runs/background/result-files.ts +437 -0
  45. package/src/runs/background/result-watcher.ts +188 -41
  46. package/src/runs/background/retained-children.ts +69 -20
  47. package/src/runs/background/run-id-resolver.ts +30 -24
  48. package/src/runs/background/run-status.ts +7 -2
  49. package/src/runs/background/scheduled-runs.ts +54 -28
  50. package/src/runs/background/stale-run-reconciler.ts +27 -13
  51. package/src/runs/background/subagent-runner.ts +294 -32
  52. package/src/runs/background/subagent-wait.ts +2 -0
  53. package/src/runs/background/wait-completions.ts +5 -2
  54. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  55. package/src/runs/foreground/chain-execution.ts +16 -0
  56. package/src/runs/foreground/execution.ts +219 -15
  57. package/src/runs/foreground/subagent-executor.ts +114 -28
  58. package/src/runs/shared/completion-guard.ts +96 -6
  59. package/src/runs/shared/external-cli-runner.ts +4 -0
  60. package/src/runs/shared/model-fallback.ts +16 -2
  61. package/src/runs/shared/nested-events.ts +66 -62
  62. package/src/runs/shared/orca-progress-tabs.ts +375 -0
  63. package/src/runs/shared/parallel-utils.ts +2 -0
  64. package/src/runs/shared/subagent-control.ts +15 -0
  65. package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  66. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  67. package/src/runs/shared/tool-timeout.ts +93 -0
  68. package/src/shared/types.ts +29 -2
  69. package/src/slash/slash-commands.ts +34 -25
  70. package/src/slash/slash-live-state.ts +3 -0
  71. package/src/tui/fleet-status.ts +160 -45
  72. package/src/tui/fleet.ts +125 -15
  73. package/src/tui/render.ts +41 -10
  74. package/src/workflows/chat-progress.ts +6 -4
  75. package/src/workflows/scripted-workflow.ts +227 -70
package/src/tui/fleet.ts CHANGED
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import { getMarkdownTheme, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type MarkdownTheme } from "@earendil-works/pi-tui";
6
+ import { snapshotExternalRuns, type ExternalRun } from "../api/external-runs.ts";
6
7
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
7
8
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../shared/formatters.ts";
8
9
  import { DIRS, type AsyncJobState, type Details, type FleetKeybindingAction, type FleetKeybindingsConfig, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
@@ -20,6 +21,7 @@ import { handleHerdrInspectorAction } from "../inspectors/herdr/actions.ts";
20
21
  import { getLivePromptAudit, type LivePromptAudit, type PromptAuditView } from "../runs/foreground/prompt-audit.ts";
21
22
 
22
23
  const REFRESH_MS = 750;
24
+ const MIN_REFRESH_MS = 250;
23
25
  const MAX_RECENT_ASYNC_RUNS = 20;
24
26
  const MAX_FLEET_HISTORY_CANDIDATES = 100;
25
27
  const TRANSCRIPT_LINES = 200;
@@ -77,6 +79,7 @@ export type FleetItem = (
77
79
  | { key: string; kind: "foreground-active"; runId: string; index?: number; agent: string; state: "running"; updatedAt: number; control: ForegroundRunControl; activeChild?: ForegroundChildControl }
78
80
  | { key: string; kind: "foreground-recent"; runId: string; index: number; agent: string; state: ForegroundResumeChild["status"]; updatedAt: number; run: ForegroundResumeRun; child: ForegroundResumeChild }
79
81
  | { key: string; kind: "async"; runId: string; index?: number; agent: string; state: string; updatedAt: number; run: AsyncRunSummary; step?: AsyncStep }
82
+ | { key: string; kind: "external"; runId: string; agent: string; state: ExternalRun["state"]; updatedAt: number; run: ExternalRun }
80
83
  ) & { description?: string };
81
84
 
82
85
  export interface FleetSnapshot {
@@ -186,8 +189,13 @@ export function collectFleetSnapshot(
186
189
  ): FleetSnapshot {
187
190
  const items: FleetItem[] = [];
188
191
  const activeForegroundIds = new Set<string>();
192
+ const trackedJobs = state.fleetJobs ?? state.asyncJobs;
193
+ const workflowParentIds = new Set([...trackedJobs.values()]
194
+ .filter((job) => job.mode === "workflow" && belongsToCurrentSession(job.sessionId, state.currentSessionId))
195
+ .map((job) => job.asyncId));
189
196
  for (const control of [...state.foregroundControls.values()].sort((left, right) => right.updatedAt - left.updatedAt)) {
190
197
  activeForegroundIds.add(control.runId);
198
+ if (control.parentWorkflowRunId && workflowParentIds.has(control.parentWorkflowRunId)) continue;
191
199
  if (control.activeChildren) {
192
200
  for (const child of [...control.activeChildren.values()].sort((left, right) => left.index - right.index)) {
193
201
  items.push({
@@ -222,7 +230,7 @@ export function collectFleetSnapshot(
222
230
  try {
223
231
  let runs: AsyncRunSummary[];
224
232
  const descriptions = new Map<string, string>();
225
- const tracked = [...(state.fleetJobs ?? state.asyncJobs).values()]
233
+ const tracked = [...trackedJobs.values()]
226
234
  .filter((job) => belongsToCurrentSession(job.sessionId, state.currentSessionId));
227
235
  const byUpdate = (left: AsyncJobState, right: AsyncJobState) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0);
228
236
  const active = tracked.filter((job) => job.status === "queued" || job.status === "running").sort(byUpdate);
@@ -255,6 +263,26 @@ export function collectFleetSnapshot(
255
263
  error = cause instanceof Error ? cause.message : String(cause);
256
264
  }
257
265
 
266
+ if (state.currentSessionId) {
267
+ try {
268
+ for (const run of snapshotExternalRuns(state.currentSessionId, { ignoreMalformed: true, onMalformedRecord: (message) => console.warn(`[pi-subagents] Removed ${message}`) })) {
269
+ items.push({
270
+ key: `external:${run.id}`,
271
+ kind: "external",
272
+ runId: run.id,
273
+ agent: run.label,
274
+ state: run.state,
275
+ updatedAt: run.updatedAt ?? run.endedAt ?? run.startedAt,
276
+ run,
277
+ ...(run.currentAction ? { description: run.currentAction } : {}),
278
+ });
279
+ }
280
+ } catch (cause) {
281
+ const message = `Failed to inspect external jobs: ${cause instanceof Error ? cause.message : String(cause)}`;
282
+ error = error ? `${error}; ${message}` : message;
283
+ }
284
+ }
285
+
258
286
  const recentForeground = [...(state.foregroundRuns?.values() ?? [])]
259
287
  .filter((run) => belongsToCurrentSession(run.sessionId, state.currentSessionId) && !activeForegroundIds.has(run.runId))
260
288
  .sort((left, right) => right.updatedAt - left.updatedAt);
@@ -276,6 +304,27 @@ export function collectFleetSnapshot(
276
304
  return { items, ...(error ? { error } : {}) };
277
305
  }
278
306
 
307
+ function visibleWorkflowParentKeyForForegroundKey(state: SubagentState, key: string, items: FleetItem[]): string | undefined {
308
+ for (const control of state.foregroundControls.values()) {
309
+ if (!control.parentWorkflowRunId) continue;
310
+ let matches = false;
311
+ if (control.activeChildren) {
312
+ for (const child of control.activeChildren.values()) {
313
+ if (`foreground-active:${control.runId}:${child.index}` === key) {
314
+ matches = true;
315
+ break;
316
+ }
317
+ }
318
+ } else {
319
+ matches = `foreground-active:${control.runId}:${control.currentIndex ?? 0}` === key;
320
+ }
321
+ if (!matches) continue;
322
+ const parentKey = `async:${control.parentWorkflowRunId}`;
323
+ return items.some((item) => item.key === parentKey) ? parentKey : undefined;
324
+ }
325
+ return undefined;
326
+ }
327
+
279
328
  function statusGlyph(item: FleetItem, theme: Theme): string {
280
329
  if (item.state === "running") return theme.fg("accent", "●");
281
330
  if (item.state === "queued" || item.state === "pending") return theme.fg("muted", "◦");
@@ -424,6 +473,11 @@ function foregroundRecentDetail(item: Extract<FleetItem, { kind: "foreground-rec
424
473
  return lines.filter((line): line is string => line !== undefined);
425
474
  }
426
475
 
476
+ function externalElapsedEnd(run: ExternalRun): number {
477
+ const terminal = run.state !== "queued" && run.state !== "running";
478
+ return terminal ? (run.endedAt ?? run.updatedAt ?? Date.now()) : Date.now();
479
+ }
480
+
427
481
  function asyncDetail(item: Extract<FleetItem, { kind: "async" }>): string[] {
428
482
  const status = readStatus(item.run.asyncDir);
429
483
  if (status) {
@@ -444,13 +498,36 @@ function asyncDetail(item: Extract<FleetItem, { kind: "async" }>): string[] {
444
498
  ].filter((line): line is string => line !== undefined);
445
499
  }
446
500
 
501
+ function externalDetail(item: Extract<FleetItem, { kind: "external" }>): string[] {
502
+ return [
503
+ `Run: ${item.runId}`,
504
+ "Source: external · display-only",
505
+ `Owner: ${item.run.source}`,
506
+ `State: ${item.state}`,
507
+ `Started: ${new Date(item.run.startedAt).toISOString()}`,
508
+ item.run.updatedAt !== undefined ? `Updated: ${new Date(item.run.updatedAt).toISOString()}` : undefined,
509
+ item.run.endedAt !== undefined ? `Ended: ${new Date(item.run.endedAt).toISOString()}` : undefined,
510
+ `Elapsed: ${formatDuration(Math.max(0, externalElapsedEnd(item.run) - item.run.startedAt))}`,
511
+ item.run.currentAction ? `Current action: ${item.run.currentAction}` : undefined,
512
+ item.run.reportPath ? `Report path: ${item.run.reportPath}` : undefined,
513
+ item.run.transcriptPath ? `Transcript path: ${item.run.transcriptPath}` : undefined,
514
+ "",
515
+ "Preview",
516
+ item.run.preview ?? "(no preview supplied)",
517
+ "",
518
+ "The owning extension controls execution, persistence, cancellation, and results.",
519
+ ].filter((line): line is string => line !== undefined);
520
+ }
521
+
447
522
  function detailLines(item: FleetItem | undefined, error: string | undefined, state: SubagentState): string[] {
448
- if (!item) return [error ? `Fleet scan failed: ${error}` : "No current-session foreground or recent async children.", "", "New runs appear here automatically while this inspector remains open."];
523
+ if (!item) return [error ? `Fleet scan failed: ${error}` : "No current-session Fleet jobs.", "", "New jobs appear here automatically while this inspector remains open."];
449
524
  const lines = item.kind === "foreground-active"
450
525
  ? foregroundActiveDetail(item, state)
451
526
  : item.kind === "foreground-recent"
452
527
  ? foregroundRecentDetail(item, state)
453
- : asyncDetail(item);
528
+ : item.kind === "external"
529
+ ? externalDetail(item)
530
+ : asyncDetail(item);
454
531
  if (error) lines.unshift(`Fleet scan warning: ${error}`, "");
455
532
  return lines;
456
533
  }
@@ -478,6 +555,7 @@ function fleetArtifactsRoot(state: SubagentState, cwd: string): string {
478
555
  }
479
556
 
480
557
  function transcriptTarget(item: FleetItem, state: SubagentState): { path: string; trustedRoots: string[] } | undefined {
558
+ if (item.kind === "external") return undefined;
481
559
  if (item.kind === "foreground-active") {
482
560
  const artifactsRoot = fleetArtifactsRoot(state, item.control.cwd ?? state.baseCwd);
483
561
  return {
@@ -522,10 +600,12 @@ function itemContext(item: FleetItem): string | undefined {
522
600
  }
523
601
 
524
602
  function itemMode(item: FleetItem): string {
603
+ if (item.kind === "external") return "display-only";
525
604
  return item.kind === "foreground-active" ? item.control.mode : item.run.mode;
526
605
  }
527
606
 
528
607
  function itemSource(item: FleetItem): string {
608
+ if (item.kind === "external") return `external · ${item.run.source}`;
529
609
  if (item.kind === "async") return "background";
530
610
  return item.kind === "foreground-active" ? "foreground · live" : "foreground · recent";
531
611
  }
@@ -545,6 +625,8 @@ function itemStats(item: FleetItem): string[] {
545
625
  model = formatModelThinking(item.child.model, item.child.thinking) || undefined;
546
626
  tokens = item.child.tokens;
547
627
  tools = item.child.toolCount;
628
+ } else if (item.kind === "external") {
629
+ durationMs = Math.max(0, externalElapsedEnd(item.run) - item.run.startedAt);
548
630
  } else {
549
631
  model = formatModelThinking(item.step?.model, item.step?.thinking) || undefined;
550
632
  tokens = item.step?.tokens?.total ?? (item.index === undefined ? item.run.totalTokens?.total : undefined);
@@ -564,7 +646,7 @@ function itemStats(item: FleetItem): string[] {
564
646
  function structuredHeader(item: FleetItem, width: number, theme: Theme, conversationState: string, promptSummary?: string): string[] {
565
647
  const lines: string[] = [];
566
648
  lines.push(rightAligned(` ${statusGlyph(item, theme)} ${theme.bold(item.agent)}`, theme.fg("dim", item.state), width));
567
- const child = item.index !== undefined ? ` · child ${item.index + 1}` : "";
649
+ const child = "index" in item && item.index !== undefined ? ` · child ${item.index + 1}` : "";
568
650
  const context = itemContext(item);
569
651
  const identity = `${itemSource(item)} · ${item.runId.slice(0, 8)}${child} · ${itemMode(item)}${context ? ` ${context}` : ""}`;
570
652
  lines.push(` ${theme.fg("dim", identity)}`);
@@ -629,7 +711,8 @@ export class SubagentFleetComponent implements Component {
629
711
  private actionBusy = false;
630
712
  private transcriptCache: FleetTranscriptCache | undefined;
631
713
  private disposed = false;
632
- private readonly timer: ReturnType<typeof setInterval>;
714
+ private refreshTimer: ReturnType<typeof setTimeout> | undefined;
715
+ private readonly refreshMs: number;
633
716
  private readonly tui: FleetTui;
634
717
  private readonly theme: Theme;
635
718
  private readonly markdownTheme: MarkdownTheme;
@@ -652,20 +735,41 @@ export class SubagentFleetComponent implements Component {
652
735
  this.done = done;
653
736
  this.options = options;
654
737
  this.keybindings = resolveFleetKeybindings(options.fleetKeybindings);
738
+ this.refreshMs = Math.max(MIN_REFRESH_MS, options.refreshMs ?? REFRESH_MS);
655
739
  this.selectedKey = options.initialKey;
656
740
  this.refresh();
657
- this.timer = setInterval(() => {
741
+ this.scheduleRefresh();
742
+ }
743
+
744
+ private scheduleRefresh(): void {
745
+ if (this.disposed || this.refreshTimer) return;
746
+ this.refreshTimer = setTimeout(() => {
747
+ this.refreshTimer = undefined;
658
748
  if (this.disposed) return;
659
- this.invalidate();
660
- this.tui.requestRender();
661
- }, options.refreshMs ?? REFRESH_MS);
662
- this.timer.unref?.();
749
+ try {
750
+ this.invalidate();
751
+ this.tui.requestRender();
752
+ } finally {
753
+ this.scheduleRefresh();
754
+ }
755
+ }, this.refreshMs);
756
+ this.refreshTimer.unref?.();
757
+ }
758
+
759
+ private stopRefresh(): void {
760
+ this.disposed = true;
761
+ if (this.refreshTimer) clearTimeout(this.refreshTimer);
762
+ this.refreshTimer = undefined;
663
763
  }
664
764
 
665
765
  private refresh(): void {
666
766
  const previousKey = this.snapshot.items[this.selected]?.key ?? this.selectedKey;
667
767
  this.snapshot = collectFleetSnapshot(this.state, this.options);
668
- const preserved = previousKey ? this.snapshot.items.findIndex((item) => item.key === previousKey) : -1;
768
+ let preserved = previousKey ? this.snapshot.items.findIndex((item) => item.key === previousKey) : -1;
769
+ if (preserved < 0 && previousKey) {
770
+ const parentKey = visibleWorkflowParentKeyForForegroundKey(this.state, previousKey, this.snapshot.items);
771
+ if (parentKey) preserved = this.snapshot.items.findIndex((item) => item.key === parentKey);
772
+ }
669
773
  this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.snapshot.items.length - 1));
670
774
  this.selectedKey = this.snapshot.items[this.selected]?.key;
671
775
  }
@@ -725,6 +829,7 @@ export class SubagentFleetComponent implements Component {
725
829
  private selectedAsyncAction(): { item: Extract<FleetItem, { kind: "async" }> } | { reason: string } {
726
830
  const item = this.snapshot.items[this.selected];
727
831
  if (!item) return { reason: "No child is selected." };
832
+ if (item.kind === "external") return { reason: "External jobs are display-only and remain controlled by their owning extension." };
728
833
  if (item.kind !== "async") return { reason: "Fleet controls are available for current-session top-level async runs only." };
729
834
  if (!isActionableAsyncState(item.run.state) || !isActionableAsyncState(item.state)) return { reason: `Selected child is ${item.state}; controls require a running or queued async child.` };
730
835
  return { item };
@@ -733,6 +838,7 @@ export class SubagentFleetComponent implements Component {
733
838
  private selectedHerdrInspectAction(): { runId: string; asyncDir: string; index?: number } | { reason: string } {
734
839
  const item = this.snapshot.items[this.selected];
735
840
  if (!item) return { reason: "No child is selected." };
841
+ if (item.kind === "external") return { reason: "External jobs are display-only and have no Herdr controls." };
736
842
  if (item.kind === "async") {
737
843
  if (!isActionableAsyncState(item.run.state) || !isActionableAsyncState(item.state)) return { reason: `Selected child is ${item.state}; controls require a running or queued async child.` };
738
844
  return { runId: item.runId, asyncDir: item.run.asyncDir, ...(item.index !== undefined ? { index: item.index } : {}) };
@@ -928,6 +1034,7 @@ export class SubagentFleetComponent implements Component {
928
1034
  return;
929
1035
  }
930
1036
  if (matchesFleetAction(data, this.keybindings, "close")) {
1037
+ this.stopRefresh();
931
1038
  this.done(undefined);
932
1039
  return;
933
1040
  }
@@ -1117,7 +1224,9 @@ export class SubagentFleetComponent implements Component {
1117
1224
  ];
1118
1225
  const lines = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
1119
1226
  const selected = this.snapshot.items[this.selected];
1120
- const title = ` ${this.theme.bold("Subagent fleet inspector")} ${this.theme.fg("dim", "· live controls")}`;
1227
+ const title = selected?.kind === "external"
1228
+ ? ` ${this.theme.bold("Fleet inspector")} ${this.theme.fg("dim", "· external display-only")}`
1229
+ : ` ${this.theme.bold("Subagent fleet inspector")} ${this.theme.fg("dim", "· live controls")}`;
1121
1230
  const selectedStatus = selected
1122
1231
  ? `${statusGlyph(selected, this.theme)} ${selected.agent} · ${selected.state} `
1123
1232
  : this.theme.fg("dim", "no children ");
@@ -1136,7 +1245,9 @@ export class SubagentFleetComponent implements Component {
1136
1245
  const position = this.snapshot.items.length ? `${this.selected + 1}/${this.snapshot.items.length}` : "0/0";
1137
1246
  const footer = this.promptAuditOpen
1138
1247
  ? ` j/k child · 1/2/3 view · g redo with guidance · c copy · Esc close Prompt Audit · ${position}`
1139
- : ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} agent · p Prompt Audit · ${bindingLabel(this.keybindings, "inspect")} Herdr · ${bindingLabel(this.keybindings, "steer")} steer · ${bindingLabel(this.keybindings, "stop")} stop · ${bindingLabel(this.keybindings, "toggleTools")} tools · ${bindingLabel(this.keybindings, "refresh")} refresh · ${bindingLabel(this.keybindings, "close")} close · ${position}`;
1248
+ : selected?.kind === "external"
1249
+ ? ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} job · display-only · ${bindingLabel(this.keybindings, "refresh")} refresh · ${bindingLabel(this.keybindings, "close")} close · ${position}`
1250
+ : ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} agent · p Prompt Audit · ${bindingLabel(this.keybindings, "inspect")} Herdr · ${bindingLabel(this.keybindings, "steer")} steer · ${bindingLabel(this.keybindings, "stop")} stop · ${bindingLabel(this.keybindings, "toggleTools")} tools · ${bindingLabel(this.keybindings, "refresh")} refresh · ${bindingLabel(this.keybindings, "close")} close · ${position}`;
1140
1251
  lines.push(this.theme.fg("border", "│") + fit(this.theme.fg("dim", footer), innerWidth) + this.theme.fg("border", "│"));
1141
1252
  lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
1142
1253
  return lines.map((line) => truncateToWidth(line, width));
@@ -1148,8 +1259,7 @@ export class SubagentFleetComponent implements Component {
1148
1259
  }
1149
1260
 
1150
1261
  dispose(): void {
1151
- this.disposed = true;
1152
- clearInterval(this.timer);
1262
+ this.stopRefresh();
1153
1263
  }
1154
1264
  }
1155
1265
 
package/src/tui/render.ts CHANGED
@@ -28,6 +28,7 @@ import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
28
28
  import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
29
29
  import { contextModeBadge, contextModePrefix } from "../runs/shared/context-mode.ts";
30
30
  import { buildWorkflowChatProgressRows, type WorkflowChatProgressRow } from "../workflows/chat-progress.ts";
31
+ import { encodeAsyncStatusSnapshotWidget } from "../runs/background/async-status-snapshot.ts";
31
32
 
32
33
  type Theme = ExtensionContext["ui"]["theme"];
33
34
 
@@ -71,6 +72,22 @@ function liveDetailHintText(): string {
71
72
  return `Press ${liveDetailKeyText()} for live detail`;
72
73
  }
73
74
 
75
+ function foregroundSingleHintText(shortcut?: string): string {
76
+ if (!shortcut) return liveDetailHintText();
77
+ const label = shortcut
78
+ .split("+")
79
+ .map((part) => {
80
+ const normalized = part.trim().toLowerCase();
81
+ if (normalized === "ctrl") return "Ctrl";
82
+ if (normalized === "alt") return "Alt";
83
+ if (normalized === "shift") return "Shift";
84
+ if (normalized === "super") return "Super";
85
+ return normalized.length === 1 ? normalized.toUpperCase() : part.trim();
86
+ })
87
+ .join("+");
88
+ return `${liveDetailHintText()} · ${label} to run in background`;
89
+ }
90
+
74
91
  function getTermWidth(): number {
75
92
  return process.stdout.columns || 120;
76
93
  }
@@ -341,7 +358,7 @@ function visibleWorkflowRows(rows: WorkflowChatProgressRow[]): { rows: WorkflowC
341
358
  selected.add(row.key);
342
359
  };
343
360
  for (const row of [...rows].reverse()) {
344
- if (row.state === "failed") add(row);
361
+ if (row.state === "failed" || row.state === "detached") add(row);
345
362
  }
346
363
  for (const row of [...rows].reverse()) add(row);
347
364
  return {
@@ -1585,10 +1602,21 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
1585
1602
  return;
1586
1603
  }
1587
1604
  if (!ctx.hasUI) return;
1605
+ if ((ctx as { mode?: string }).mode === "rpc") {
1606
+ ctx.ui.setWidget(WIDGET_KEY, encodeAsyncStatusSnapshotWidget(jobs));
1607
+ return;
1608
+ }
1588
1609
  ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
1589
1610
  }
1590
1611
 
1591
- function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
1612
+ function renderSingleCompact(
1613
+ d: Details,
1614
+ r: Details["results"][number],
1615
+ theme: Theme,
1616
+ layout: MainWindowRenderLayout,
1617
+ frame?: number,
1618
+ foregroundDetachShortcut?: string,
1619
+ ): Component {
1592
1620
  const output = r.truncation?.text || getSingleResultOutput(r);
1593
1621
  const progress = r.progress || r.progressSummary;
1594
1622
  const isRunning = isResultRunning(r);
@@ -1613,7 +1641,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1613
1641
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, progressSnapshotNow)) {
1614
1642
  c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1615
1643
  }
1616
- c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}${liveDetailHintText()}`), width), 0, 0));
1644
+ c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}${foregroundSingleHintText(foregroundDetachShortcut)}`), width), 0, 0));
1617
1645
  if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1618
1646
  return c;
1619
1647
  }
@@ -1635,7 +1663,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1635
1663
  function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: number): string {
1636
1664
  if (row.state === "running") return theme.fg("accent", runningGlyph(frame));
1637
1665
  if (row.state === "complete") return theme.fg("success", "✓");
1638
- if (row.state === "stopped") return theme.fg("warning", "■");
1666
+ if (row.state === "detached" || row.state === "stopped") return theme.fg("warning", "■");
1639
1667
  return theme.fg("error", "✗");
1640
1668
  }
1641
1669
 
@@ -1643,12 +1671,13 @@ function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): stri
1643
1671
  const label = (row.state === "complete" ? "complete" : row.state).padEnd(8);
1644
1672
  if (row.state === "running") return theme.fg("accent", label);
1645
1673
  if (row.state === "complete") return theme.fg("success", label);
1646
- if (row.state === "stopped") return theme.fg("warning", label);
1674
+ if (row.state === "detached" || row.state === "stopped") return theme.fg("warning", label);
1647
1675
  return theme.fg("error", label);
1648
1676
  }
1649
1677
 
1650
- function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue: boolean, isError?: boolean): "running" | "complete" | "failed" {
1678
+ function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue: boolean, isError?: boolean): "running" | "complete" | "failed" | "paused" {
1651
1679
  if (isError || rows.some((row) => row.state === "failed")) return "failed";
1680
+ if (rows.some((row) => row.state === "detached")) return "paused";
1652
1681
  if ((rows.length > 0 && rows.every((row) => row.state === "complete")) || hasTerminalValue) return "complete";
1653
1682
  return "running";
1654
1683
  }
@@ -1657,7 +1686,7 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1657
1686
  const workflow = d.workflow;
1658
1687
  const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace) : [];
1659
1688
  const state = workflowOverallState(rows, workflow?.value !== undefined, result.isError);
1660
- const glyph = state === "running" ? theme.fg("accent", runningGlyph(frame)) : state === "complete" ? theme.fg("success", "✓") : theme.fg("error", "✗");
1689
+ const glyph = state === "running" ? theme.fg("accent", runningGlyph(frame)) : state === "complete" ? theme.fg("success", "✓") : state === "paused" ? theme.fg("warning", "■") : theme.fg("error", "✗");
1661
1690
  const width = getTermWidth() - 4;
1662
1691
  const runId = d.runId ? d.runId.slice(0, 12) : "workflow";
1663
1692
  const repoLabel = d.chatProgress?.repoLabel ?? (d.chatProgress?.repoRelation === "same" ? "same repo" : "other repo");
@@ -1678,7 +1707,7 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1678
1707
  const label = row.label && row.label !== row.key ? ` ${oneLine(row.label)}` : "";
1679
1708
  const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
1680
1709
  const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
1681
- const error = row.error ? ` ${theme.fg("error", `· ${compactWorkflowError(row.error)}`)}` : "";
1710
+ const error = row.error ? ` ${theme.fg(row.state === "detached" ? "warning" : "error", `· ${compactWorkflowError(row.error)}`)}` : "";
1682
1711
  c.addChild(new Text(truncLine(`${rowIndent}${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
1683
1712
  }
1684
1713
  if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Emits ${workflow.emits.length}`), width), 0, 0));
@@ -1844,6 +1873,7 @@ export function renderSubagentResult(
1844
1873
  theme: Theme,
1845
1874
  frame?: number,
1846
1875
  rendererConfig?: MainWindowRendererConfig,
1876
+ foregroundDetachShortcut?: string,
1847
1877
  ): Component {
1848
1878
  const layout = resolveMainWindowRenderLayout(rendererConfig);
1849
1879
  const compact = (component: Component): Component => capCompactMainWindowResult(component, layout, theme, !options.expanded);
@@ -1877,8 +1907,9 @@ export function renderSubagentResult(
1877
1907
 
1878
1908
  if (d.mode === "single" && d.results.length === 1) {
1879
1909
  const r = d.results[0];
1910
+ const detachableShortcut = d.asyncId || d.background ? undefined : foregroundDetachShortcut;
1880
1911
  if (!r) return compact(renderMultiCompact(d, theme, layout, frame));
1881
- if (!expanded) return compact(renderSingleCompact(d, r, theme, layout, frame));
1912
+ if (!expanded) return compact(renderSingleCompact(d, r, theme, layout, frame, detachableShortcut));
1882
1913
  const isRunning = isResultRunning(r);
1883
1914
  const contextBadge = contextModeBadge(theme, r.context ?? d.context);
1884
1915
  const output = r.truncation?.text || getSingleResultOutput(r);
@@ -1918,7 +1949,7 @@ export function renderSubagentResult(
1918
1949
  if (liveStatusLine) {
1919
1950
  c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
1920
1951
  }
1921
- c.addChild(new Text(fit(theme.fg("accent", liveDetailHintText())), 0, 0));
1952
+ c.addChild(new Text(fit(theme.fg("accent", foregroundSingleHintText(detachableShortcut))), 0, 0));
1922
1953
  if (r.artifactPaths) {
1923
1954
  c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
1924
1955
  }
@@ -96,7 +96,7 @@ export function resolveWorkflowChatProgress(input: ResolveWorkflowChatProgressIn
96
96
 
97
97
  export interface WorkflowChatProgressRow {
98
98
  key: string;
99
- state: "running" | "complete" | "failed" | "stopped";
99
+ state: "running" | "complete" | "failed" | "detached" | "stopped";
100
100
  label?: string;
101
101
  phase?: string;
102
102
  runId?: string;
@@ -127,9 +127,11 @@ export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workfl
127
127
  ? "complete"
128
128
  : entry.state === "failed"
129
129
  ? "failed"
130
- : entry.state === "stopped"
131
- ? "stopped"
132
- : "running";
130
+ : entry.state === "detached"
131
+ ? "detached"
132
+ : entry.state === "stopped"
133
+ ? "stopped"
134
+ : "running";
133
135
  const label = cleanLabel(entry.label);
134
136
  const phase = cleanLabel(entry.phase);
135
137
  if (label) next.label = label;