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.
- package/CHANGELOG.md +35 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/docs/agents.md +2 -0
- package/docs/configuration.md +47 -3
- package/docs/extension-api.md +36 -0
- package/docs/missions.md +1 -1
- package/docs/observability.md +16 -0
- package/docs/tool-reference.md +19 -2
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +10 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +12 -6
- package/src/extension/config.ts +33 -0
- package/src/extension/index.ts +48 -5
- package/src/extension/public-execution.ts +3 -1
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +4 -2
- package/src/inspectors/herdr/actions.ts +3 -7
- package/src/inspectors/herdr/project-panes.ts +2 -6
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +19 -42
- package/src/missions/goal-driver.ts +3 -1
- package/src/runs/background/active-run-index.ts +71 -1
- package/src/runs/background/async-execution.ts +49 -7
- package/src/runs/background/async-job-tracker.ts +1 -0
- package/src/runs/background/async-resume.ts +3 -5
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/result-files.ts +437 -0
- package/src/runs/background/result-watcher.ts +188 -41
- package/src/runs/background/retained-children.ts +69 -20
- package/src/runs/background/run-id-resolver.ts +30 -24
- package/src/runs/background/run-status.ts +7 -2
- package/src/runs/background/scheduled-runs.ts +54 -28
- package/src/runs/background/stale-run-reconciler.ts +27 -13
- package/src/runs/background/subagent-runner.ts +294 -32
- package/src/runs/background/subagent-wait.ts +2 -0
- package/src/runs/background/wait-completions.ts +5 -2
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/chain-execution.ts +16 -0
- package/src/runs/foreground/execution.ts +219 -15
- package/src/runs/foreground/subagent-executor.ts +114 -28
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/model-fallback.ts +16 -2
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +375 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/subagent-control.ts +15 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/shared/types.ts +29 -2
- package/src/slash/slash-commands.ts +34 -25
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +125 -15
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +6 -4
- 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 = [...
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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.
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
:
|
|
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.
|
|
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(
|
|
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}${
|
|
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",
|
|
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 === "
|
|
131
|
-
? "
|
|
132
|
-
: "
|
|
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;
|