pi-subagents 0.49.0 → 0.51.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 +97 -0
- package/agents/gpt-pro.md +17 -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/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +37 -2
- package/docs/configuration.md +76 -14
- package/docs/extension-api.md +78 -1
- package/docs/missions.md +1 -1
- package/docs/observability.md +20 -4
- package/docs/tool-reference.md +55 -39
- package/docs/workflows.md +171 -5
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
- package/skills/pi-subagents/references/execution-controls.md +22 -18
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +135 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +42 -11
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +36 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +210 -90
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +14 -36
- package/src/extension/tool-description.ts +37 -24
- package/src/inspectors/herdr/actions.ts +5 -9
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +4 -8
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +49 -51
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +94 -1
- package/src/runs/background/async-execution.ts +98 -38
- package/src/runs/background/async-job-tracker.ts +21 -4
- package/src/runs/background/async-resume.ts +30 -17
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +47 -56
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +505 -0
- package/src/runs/background/result-watcher.ts +250 -51
- package/src/runs/background/retained-children.ts +79 -20
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +37 -29
- package/src/runs/background/run-status.ts +34 -20
- package/src/runs/background/scheduled-runs.ts +71 -27
- package/src/runs/background/stale-run-reconciler.ts +33 -16
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +535 -161
- package/src/runs/background/subagent-wait.ts +9 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +22 -2
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +221 -15
- package/src/runs/foreground/subagent-executor.ts +529 -1519
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/external-job-bridge.ts +450 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +34 -3
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +437 -0
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +6 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +41 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +88 -27
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +45 -28
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +186 -28
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +7 -5
- package/src/workflows/scripted-workflow.ts +424 -125
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1565
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";
|
|
@@ -13,13 +14,16 @@ import { listAsyncRuns, type AsyncRunSummary } from "../runs/background/async-st
|
|
|
13
14
|
import { steerAsyncRun } from "../runs/foreground/async-steering-action.ts";
|
|
14
15
|
import type { SteerDeliveryMode } from "../runs/background/control-channel.ts";
|
|
15
16
|
import { stopAsyncRun } from "../runs/foreground/async-stop-action.ts";
|
|
17
|
+
import { resolveWorkflowForegroundSteeringTarget, steerWorkflowForegroundTarget } from "../runs/foreground/workflow-foreground-steering.ts";
|
|
16
18
|
import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.ts";
|
|
17
19
|
import { FLEET_STATUS_WIDGET_KEY } from "./fleet-status.ts";
|
|
18
20
|
import { readFleetTranscript, renderFleetTranscript, type FleetTranscript } from "./fleet-transcript.ts";
|
|
19
21
|
import { handleHerdrInspectorAction } from "../inspectors/herdr/actions.ts";
|
|
22
|
+
import type { HerdrClient } from "../inspectors/herdr/client.ts";
|
|
20
23
|
import { getLivePromptAudit, type LivePromptAudit, type PromptAuditView } from "../runs/foreground/prompt-audit.ts";
|
|
21
24
|
|
|
22
25
|
const REFRESH_MS = 750;
|
|
26
|
+
const MIN_REFRESH_MS = 250;
|
|
23
27
|
const MAX_RECENT_ASYNC_RUNS = 20;
|
|
24
28
|
const MAX_FLEET_HISTORY_CANDIDATES = 100;
|
|
25
29
|
const TRANSCRIPT_LINES = 200;
|
|
@@ -77,6 +81,7 @@ export type FleetItem = (
|
|
|
77
81
|
| { key: string; kind: "foreground-active"; runId: string; index?: number; agent: string; state: "running"; updatedAt: number; control: ForegroundRunControl; activeChild?: ForegroundChildControl }
|
|
78
82
|
| { key: string; kind: "foreground-recent"; runId: string; index: number; agent: string; state: ForegroundResumeChild["status"]; updatedAt: number; run: ForegroundResumeRun; child: ForegroundResumeChild }
|
|
79
83
|
| { key: string; kind: "async"; runId: string; index?: number; agent: string; state: string; updatedAt: number; run: AsyncRunSummary; step?: AsyncStep }
|
|
84
|
+
| { key: string; kind: "external"; runId: string; agent: string; state: ExternalRun["state"]; updatedAt: number; run: ExternalRun }
|
|
80
85
|
) & { description?: string };
|
|
81
86
|
|
|
82
87
|
export interface FleetSnapshot {
|
|
@@ -105,6 +110,7 @@ export interface FleetViewOptions {
|
|
|
105
110
|
fleetKeybindings?: FleetKeybindingsConfig;
|
|
106
111
|
actions?: FleetActionHandlers;
|
|
107
112
|
copyText?: (text: string) => Promise<void> | void;
|
|
113
|
+
herdrClient?: HerdrClient;
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
function belongsToCurrentSession(sessionId: string | undefined, currentSessionId: string | null): boolean {
|
|
@@ -186,8 +192,26 @@ export function collectFleetSnapshot(
|
|
|
186
192
|
): FleetSnapshot {
|
|
187
193
|
const items: FleetItem[] = [];
|
|
188
194
|
const activeForegroundIds = new Set<string>();
|
|
195
|
+
const trackedJobs = state.fleetJobs ?? state.asyncJobs;
|
|
196
|
+
const workflowParentIds = new Set([...trackedJobs.values()]
|
|
197
|
+
.filter((job) => job.mode === "workflow" && belongsToCurrentSession(job.sessionId, state.currentSessionId))
|
|
198
|
+
.map((job) => job.asyncId));
|
|
199
|
+
const workflowForegroundChildCounts = new Map<string, number>();
|
|
200
|
+
const liveWorkflowForegroundControls = new Set<ForegroundRunControl>();
|
|
201
|
+
for (const control of state.foregroundControls.values()) {
|
|
202
|
+
const activeChildCount = control.activeChildren?.size ?? 0;
|
|
203
|
+
if (!control.parentWorkflowRunId
|
|
204
|
+
|| !workflowParentIds.has(control.parentWorkflowRunId)
|
|
205
|
+
|| !belongsToCurrentSession(control.sessionId, state.currentSessionId)
|
|
206
|
+
|| !control.workflowSteeringDir
|
|
207
|
+
|| activeChildCount === 0) continue;
|
|
208
|
+
liveWorkflowForegroundControls.add(control);
|
|
209
|
+
workflowForegroundChildCounts.set(control.parentWorkflowRunId, (workflowForegroundChildCounts.get(control.parentWorkflowRunId) ?? 0) + activeChildCount);
|
|
210
|
+
}
|
|
189
211
|
for (const control of [...state.foregroundControls.values()].sort((left, right) => right.updatedAt - left.updatedAt)) {
|
|
190
212
|
activeForegroundIds.add(control.runId);
|
|
213
|
+
if (control.parentWorkflowRunId && workflowParentIds.has(control.parentWorkflowRunId)
|
|
214
|
+
&& ((workflowForegroundChildCounts.get(control.parentWorkflowRunId) ?? 0) <= 1 || !liveWorkflowForegroundControls.has(control))) continue;
|
|
191
215
|
if (control.activeChildren) {
|
|
192
216
|
for (const child of [...control.activeChildren.values()].sort((left, right) => left.index - right.index)) {
|
|
193
217
|
items.push({
|
|
@@ -222,7 +246,7 @@ export function collectFleetSnapshot(
|
|
|
222
246
|
try {
|
|
223
247
|
let runs: AsyncRunSummary[];
|
|
224
248
|
const descriptions = new Map<string, string>();
|
|
225
|
-
const tracked = [...
|
|
249
|
+
const tracked = [...trackedJobs.values()]
|
|
226
250
|
.filter((job) => belongsToCurrentSession(job.sessionId, state.currentSessionId));
|
|
227
251
|
const byUpdate = (left: AsyncJobState, right: AsyncJobState) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0);
|
|
228
252
|
const active = tracked.filter((job) => job.status === "queued" || job.status === "running").sort(byUpdate);
|
|
@@ -255,6 +279,26 @@ export function collectFleetSnapshot(
|
|
|
255
279
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
256
280
|
}
|
|
257
281
|
|
|
282
|
+
if (state.currentSessionId) {
|
|
283
|
+
try {
|
|
284
|
+
for (const run of snapshotExternalRuns(state.currentSessionId, { ignoreMalformed: true, onMalformedRecord: (message) => console.warn(`[pi-subagents] Removed ${message}`) })) {
|
|
285
|
+
items.push({
|
|
286
|
+
key: `external:${run.id}`,
|
|
287
|
+
kind: "external",
|
|
288
|
+
runId: run.id,
|
|
289
|
+
agent: run.label,
|
|
290
|
+
state: run.state,
|
|
291
|
+
updatedAt: run.updatedAt ?? run.endedAt ?? run.startedAt,
|
|
292
|
+
run,
|
|
293
|
+
...(run.currentAction ? { description: run.currentAction } : {}),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
} catch (cause) {
|
|
297
|
+
const message = `Failed to inspect external jobs: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
298
|
+
error = error ? `${error}; ${message}` : message;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
258
302
|
const recentForeground = [...(state.foregroundRuns?.values() ?? [])]
|
|
259
303
|
.filter((run) => belongsToCurrentSession(run.sessionId, state.currentSessionId) && !activeForegroundIds.has(run.runId))
|
|
260
304
|
.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
@@ -276,6 +320,27 @@ export function collectFleetSnapshot(
|
|
|
276
320
|
return { items, ...(error ? { error } : {}) };
|
|
277
321
|
}
|
|
278
322
|
|
|
323
|
+
function visibleWorkflowParentKeyForForegroundKey(state: SubagentState, key: string, items: FleetItem[]): string | undefined {
|
|
324
|
+
for (const control of state.foregroundControls.values()) {
|
|
325
|
+
if (!control.parentWorkflowRunId) continue;
|
|
326
|
+
let matches = false;
|
|
327
|
+
if (control.activeChildren) {
|
|
328
|
+
for (const child of control.activeChildren.values()) {
|
|
329
|
+
if (`foreground-active:${control.runId}:${child.index}` === key) {
|
|
330
|
+
matches = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
matches = `foreground-active:${control.runId}:${control.currentIndex ?? 0}` === key;
|
|
336
|
+
}
|
|
337
|
+
if (!matches) continue;
|
|
338
|
+
const parentKey = `async:${control.parentWorkflowRunId}`;
|
|
339
|
+
return items.some((item) => item.key === parentKey) ? parentKey : undefined;
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
279
344
|
function statusGlyph(item: FleetItem, theme: Theme): string {
|
|
280
345
|
if (item.state === "running") return theme.fg("accent", "●");
|
|
281
346
|
if (item.state === "queued" || item.state === "pending") return theme.fg("muted", "◦");
|
|
@@ -424,10 +489,20 @@ function foregroundRecentDetail(item: Extract<FleetItem, { kind: "foreground-rec
|
|
|
424
489
|
return lines.filter((line): line is string => line !== undefined);
|
|
425
490
|
}
|
|
426
491
|
|
|
427
|
-
function
|
|
492
|
+
function externalElapsedEnd(run: ExternalRun): number {
|
|
493
|
+
const terminal = run.state !== "queued" && run.state !== "running";
|
|
494
|
+
return terminal ? (run.endedAt ?? run.updatedAt ?? Date.now()) : Date.now();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function asyncDetail(item: Extract<FleetItem, { kind: "async" }>, state: SubagentState): string[] {
|
|
428
498
|
const status = readStatus(item.run.asyncDir);
|
|
429
499
|
if (status) {
|
|
430
|
-
|
|
500
|
+
const trackedJob = state.fleetJobs?.get(item.runId) ?? state.asyncJobs.get(item.runId);
|
|
501
|
+
return formatAsyncRunTranscript(status, item.run.asyncDir, {
|
|
502
|
+
index: item.index,
|
|
503
|
+
lines: TRANSCRIPT_LINES,
|
|
504
|
+
sessionRoots: uniquePaths([...(state.trustedSessionRoots ?? []), trackedJob?.sessionRoot]),
|
|
505
|
+
}).split("\n");
|
|
431
506
|
}
|
|
432
507
|
const outputPath = item.index !== undefined ? path.join(item.run.asyncDir, `output-${item.index}.log`) : undefined;
|
|
433
508
|
return [
|
|
@@ -444,13 +519,36 @@ function asyncDetail(item: Extract<FleetItem, { kind: "async" }>): string[] {
|
|
|
444
519
|
].filter((line): line is string => line !== undefined);
|
|
445
520
|
}
|
|
446
521
|
|
|
522
|
+
function externalDetail(item: Extract<FleetItem, { kind: "external" }>): string[] {
|
|
523
|
+
return [
|
|
524
|
+
`Run: ${item.runId}`,
|
|
525
|
+
"Source: external · display-only",
|
|
526
|
+
`Owner: ${item.run.source}`,
|
|
527
|
+
`State: ${item.state}`,
|
|
528
|
+
`Started: ${new Date(item.run.startedAt).toISOString()}`,
|
|
529
|
+
item.run.updatedAt !== undefined ? `Updated: ${new Date(item.run.updatedAt).toISOString()}` : undefined,
|
|
530
|
+
item.run.endedAt !== undefined ? `Ended: ${new Date(item.run.endedAt).toISOString()}` : undefined,
|
|
531
|
+
`Elapsed: ${formatDuration(Math.max(0, externalElapsedEnd(item.run) - item.run.startedAt))}`,
|
|
532
|
+
item.run.currentAction ? `Current action: ${item.run.currentAction}` : undefined,
|
|
533
|
+
item.run.reportPath ? `Report path: ${item.run.reportPath}` : undefined,
|
|
534
|
+
item.run.transcriptPath ? `Transcript path: ${item.run.transcriptPath}` : undefined,
|
|
535
|
+
"",
|
|
536
|
+
"Preview",
|
|
537
|
+
item.run.preview ?? "(no preview supplied)",
|
|
538
|
+
"",
|
|
539
|
+
"The owning extension controls execution, persistence, cancellation, and results.",
|
|
540
|
+
].filter((line): line is string => line !== undefined);
|
|
541
|
+
}
|
|
542
|
+
|
|
447
543
|
function detailLines(item: FleetItem | undefined, error: string | undefined, state: SubagentState): string[] {
|
|
448
|
-
if (!item) return [error ? `Fleet scan failed: ${error}` : "No current-session
|
|
544
|
+
if (!item) return [error ? `Fleet scan failed: ${error}` : "No current-session Fleet jobs.", "", "New jobs appear here automatically while this inspector remains open."];
|
|
449
545
|
const lines = item.kind === "foreground-active"
|
|
450
546
|
? foregroundActiveDetail(item, state)
|
|
451
547
|
: item.kind === "foreground-recent"
|
|
452
548
|
? foregroundRecentDetail(item, state)
|
|
453
|
-
:
|
|
549
|
+
: item.kind === "external"
|
|
550
|
+
? externalDetail(item)
|
|
551
|
+
: asyncDetail(item, state);
|
|
454
552
|
if (error) lines.unshift(`Fleet scan warning: ${error}`, "");
|
|
455
553
|
return lines;
|
|
456
554
|
}
|
|
@@ -478,6 +576,7 @@ function fleetArtifactsRoot(state: SubagentState, cwd: string): string {
|
|
|
478
576
|
}
|
|
479
577
|
|
|
480
578
|
function transcriptTarget(item: FleetItem, state: SubagentState): { path: string; trustedRoots: string[] } | undefined {
|
|
579
|
+
if (item.kind === "external") return undefined;
|
|
481
580
|
if (item.kind === "foreground-active") {
|
|
482
581
|
const artifactsRoot = fleetArtifactsRoot(state, item.control.cwd ?? state.baseCwd);
|
|
483
582
|
return {
|
|
@@ -522,10 +621,12 @@ function itemContext(item: FleetItem): string | undefined {
|
|
|
522
621
|
}
|
|
523
622
|
|
|
524
623
|
function itemMode(item: FleetItem): string {
|
|
624
|
+
if (item.kind === "external") return "display-only";
|
|
525
625
|
return item.kind === "foreground-active" ? item.control.mode : item.run.mode;
|
|
526
626
|
}
|
|
527
627
|
|
|
528
628
|
function itemSource(item: FleetItem): string {
|
|
629
|
+
if (item.kind === "external") return `external · ${item.run.source}`;
|
|
529
630
|
if (item.kind === "async") return "background";
|
|
530
631
|
return item.kind === "foreground-active" ? "foreground · live" : "foreground · recent";
|
|
531
632
|
}
|
|
@@ -545,6 +646,8 @@ function itemStats(item: FleetItem): string[] {
|
|
|
545
646
|
model = formatModelThinking(item.child.model, item.child.thinking) || undefined;
|
|
546
647
|
tokens = item.child.tokens;
|
|
547
648
|
tools = item.child.toolCount;
|
|
649
|
+
} else if (item.kind === "external") {
|
|
650
|
+
durationMs = Math.max(0, externalElapsedEnd(item.run) - item.run.startedAt);
|
|
548
651
|
} else {
|
|
549
652
|
model = formatModelThinking(item.step?.model, item.step?.thinking) || undefined;
|
|
550
653
|
tokens = item.step?.tokens?.total ?? (item.index === undefined ? item.run.totalTokens?.total : undefined);
|
|
@@ -564,7 +667,7 @@ function itemStats(item: FleetItem): string[] {
|
|
|
564
667
|
function structuredHeader(item: FleetItem, width: number, theme: Theme, conversationState: string, promptSummary?: string): string[] {
|
|
565
668
|
const lines: string[] = [];
|
|
566
669
|
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}` : "";
|
|
670
|
+
const child = "index" in item && item.index !== undefined ? ` · child ${item.index + 1}` : "";
|
|
568
671
|
const context = itemContext(item);
|
|
569
672
|
const identity = `${itemSource(item)} · ${item.runId.slice(0, 8)}${child} · ${itemMode(item)}${context ? ` ${context}` : ""}`;
|
|
570
673
|
lines.push(` ${theme.fg("dim", identity)}`);
|
|
@@ -629,7 +732,8 @@ export class SubagentFleetComponent implements Component {
|
|
|
629
732
|
private actionBusy = false;
|
|
630
733
|
private transcriptCache: FleetTranscriptCache | undefined;
|
|
631
734
|
private disposed = false;
|
|
632
|
-
private
|
|
735
|
+
private refreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
736
|
+
private readonly refreshMs: number;
|
|
633
737
|
private readonly tui: FleetTui;
|
|
634
738
|
private readonly theme: Theme;
|
|
635
739
|
private readonly markdownTheme: MarkdownTheme;
|
|
@@ -652,20 +756,41 @@ export class SubagentFleetComponent implements Component {
|
|
|
652
756
|
this.done = done;
|
|
653
757
|
this.options = options;
|
|
654
758
|
this.keybindings = resolveFleetKeybindings(options.fleetKeybindings);
|
|
759
|
+
this.refreshMs = Math.max(MIN_REFRESH_MS, options.refreshMs ?? REFRESH_MS);
|
|
655
760
|
this.selectedKey = options.initialKey;
|
|
656
761
|
this.refresh();
|
|
657
|
-
this.
|
|
762
|
+
this.scheduleRefresh();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private scheduleRefresh(): void {
|
|
766
|
+
if (this.disposed || this.refreshTimer) return;
|
|
767
|
+
this.refreshTimer = setTimeout(() => {
|
|
768
|
+
this.refreshTimer = undefined;
|
|
658
769
|
if (this.disposed) return;
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
770
|
+
try {
|
|
771
|
+
this.invalidate();
|
|
772
|
+
this.tui.requestRender();
|
|
773
|
+
} finally {
|
|
774
|
+
this.scheduleRefresh();
|
|
775
|
+
}
|
|
776
|
+
}, this.refreshMs);
|
|
777
|
+
this.refreshTimer.unref?.();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private stopRefresh(): void {
|
|
781
|
+
this.disposed = true;
|
|
782
|
+
if (this.refreshTimer) clearTimeout(this.refreshTimer);
|
|
783
|
+
this.refreshTimer = undefined;
|
|
663
784
|
}
|
|
664
785
|
|
|
665
786
|
private refresh(): void {
|
|
666
787
|
const previousKey = this.snapshot.items[this.selected]?.key ?? this.selectedKey;
|
|
667
788
|
this.snapshot = collectFleetSnapshot(this.state, this.options);
|
|
668
|
-
|
|
789
|
+
let preserved = previousKey ? this.snapshot.items.findIndex((item) => item.key === previousKey) : -1;
|
|
790
|
+
if (preserved < 0 && previousKey) {
|
|
791
|
+
const parentKey = visibleWorkflowParentKeyForForegroundKey(this.state, previousKey, this.snapshot.items);
|
|
792
|
+
if (parentKey) preserved = this.snapshot.items.findIndex((item) => item.key === parentKey);
|
|
793
|
+
}
|
|
669
794
|
this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.snapshot.items.length - 1));
|
|
670
795
|
this.selectedKey = this.snapshot.items[this.selected]?.key;
|
|
671
796
|
}
|
|
@@ -725,14 +850,28 @@ export class SubagentFleetComponent implements Component {
|
|
|
725
850
|
private selectedAsyncAction(): { item: Extract<FleetItem, { kind: "async" }> } | { reason: string } {
|
|
726
851
|
const item = this.snapshot.items[this.selected];
|
|
727
852
|
if (!item) return { reason: "No child is selected." };
|
|
853
|
+
if (item.kind === "external") return { reason: "External jobs are display-only and remain controlled by their owning extension." };
|
|
728
854
|
if (item.kind !== "async") return { reason: "Fleet controls are available for current-session top-level async runs only." };
|
|
729
855
|
if (!isActionableAsyncState(item.run.state) || !isActionableAsyncState(item.state)) return { reason: `Selected child is ${item.state}; controls require a running or queued async child.` };
|
|
730
856
|
return { item };
|
|
731
857
|
}
|
|
732
858
|
|
|
859
|
+
private selectedSteerAction(): { runId: string; asyncDir: string; index?: number } | { reason: string } {
|
|
860
|
+
const item = this.snapshot.items[this.selected];
|
|
861
|
+
if (item?.kind === "foreground-active" && item.control.parentWorkflowRunId) {
|
|
862
|
+
const parent = this.state.asyncJobs.get(item.control.parentWorkflowRunId) ?? this.state.fleetJobs?.get(item.control.parentWorkflowRunId);
|
|
863
|
+
if (!parent || !isActionableAsyncState(parent.status)) return { reason: "The parent workflow is no longer available for steering." };
|
|
864
|
+
return { runId: item.runId, asyncDir: parent.asyncDir, ...(item.index !== undefined ? { index: item.index } : {}) };
|
|
865
|
+
}
|
|
866
|
+
const target = this.selectedAsyncAction();
|
|
867
|
+
if ("reason" in target) return target;
|
|
868
|
+
return { runId: target.item.runId, asyncDir: target.item.run.asyncDir, ...(target.item.index !== undefined ? { index: target.item.index } : {}) };
|
|
869
|
+
}
|
|
870
|
+
|
|
733
871
|
private selectedHerdrInspectAction(): { runId: string; asyncDir: string; index?: number } | { reason: string } {
|
|
734
872
|
const item = this.snapshot.items[this.selected];
|
|
735
873
|
if (!item) return { reason: "No child is selected." };
|
|
874
|
+
if (item.kind === "external") return { reason: "External jobs are display-only and have no Herdr controls." };
|
|
736
875
|
if (item.kind === "async") {
|
|
737
876
|
if (!isActionableAsyncState(item.run.state) || !isActionableAsyncState(item.state)) return { reason: `Selected child is ${item.state}; controls require a running or queued async child.` };
|
|
738
877
|
return { runId: item.runId, asyncDir: item.run.asyncDir, ...(item.index !== undefined ? { index: item.index } : {}) };
|
|
@@ -886,12 +1025,12 @@ export class SubagentFleetComponent implements Component {
|
|
|
886
1025
|
this.setActionNotice({ text: "Steer message cannot be empty.", isError: true });
|
|
887
1026
|
return;
|
|
888
1027
|
}
|
|
889
|
-
const target = this.
|
|
1028
|
+
const target = this.selectedSteerAction();
|
|
890
1029
|
if ("reason" in target || !this.options.actions) {
|
|
891
1030
|
this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
|
|
892
1031
|
return;
|
|
893
1032
|
}
|
|
894
|
-
this.runAction(() => this.options.actions!.steer({
|
|
1033
|
+
this.runAction(() => this.options.actions!.steer({ ...target, message, mode: this.steerMode }));
|
|
895
1034
|
return;
|
|
896
1035
|
}
|
|
897
1036
|
if (matchesKey(data, "tab") || data === "\t") {
|
|
@@ -928,6 +1067,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
928
1067
|
return;
|
|
929
1068
|
}
|
|
930
1069
|
if (matchesFleetAction(data, this.keybindings, "close")) {
|
|
1070
|
+
this.stopRefresh();
|
|
931
1071
|
this.done(undefined);
|
|
932
1072
|
return;
|
|
933
1073
|
}
|
|
@@ -958,7 +1098,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
958
1098
|
return;
|
|
959
1099
|
}
|
|
960
1100
|
if (matchesFleetAction(data, this.keybindings, "steer")) {
|
|
961
|
-
const target = this.
|
|
1101
|
+
const target = this.selectedSteerAction();
|
|
962
1102
|
if ("reason" in target || !this.options.actions) this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
|
|
963
1103
|
else {
|
|
964
1104
|
this.actionNotice = undefined;
|
|
@@ -1117,7 +1257,9 @@ export class SubagentFleetComponent implements Component {
|
|
|
1117
1257
|
];
|
|
1118
1258
|
const lines = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
|
|
1119
1259
|
const selected = this.snapshot.items[this.selected];
|
|
1120
|
-
const title =
|
|
1260
|
+
const title = selected?.kind === "external"
|
|
1261
|
+
? ` ${this.theme.bold("Fleet inspector")} ${this.theme.fg("dim", "· external display-only")}`
|
|
1262
|
+
: ` ${this.theme.bold("Subagent fleet inspector")} ${this.theme.fg("dim", "· live controls")}`;
|
|
1121
1263
|
const selectedStatus = selected
|
|
1122
1264
|
? `${statusGlyph(selected, this.theme)} ${selected.agent} · ${selected.state} `
|
|
1123
1265
|
: this.theme.fg("dim", "no children ");
|
|
@@ -1136,7 +1278,9 @@ export class SubagentFleetComponent implements Component {
|
|
|
1136
1278
|
const position = this.snapshot.items.length ? `${this.selected + 1}/${this.snapshot.items.length}` : "0/0";
|
|
1137
1279
|
const footer = this.promptAuditOpen
|
|
1138
1280
|
? ` j/k child · 1/2/3 view · g redo with guidance · c copy · Esc close Prompt Audit · ${position}`
|
|
1139
|
-
:
|
|
1281
|
+
: selected?.kind === "external"
|
|
1282
|
+
? ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} job · display-only · ${bindingLabel(this.keybindings, "refresh")} refresh · ${bindingLabel(this.keybindings, "close")} close · ${position}`
|
|
1283
|
+
: ` ${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
1284
|
lines.push(this.theme.fg("border", "│") + fit(this.theme.fg("dim", footer), innerWidth) + this.theme.fg("border", "│"));
|
|
1141
1285
|
lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
|
|
1142
1286
|
return lines.map((line) => truncateToWidth(line, width));
|
|
@@ -1148,8 +1292,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
1148
1292
|
}
|
|
1149
1293
|
|
|
1150
1294
|
dispose(): void {
|
|
1151
|
-
this.
|
|
1152
|
-
clearInterval(this.timer);
|
|
1295
|
+
this.stopRefresh();
|
|
1153
1296
|
}
|
|
1154
1297
|
}
|
|
1155
1298
|
|
|
@@ -1164,23 +1307,38 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
|
|
|
1164
1307
|
await copyToClipboard(text);
|
|
1165
1308
|
});
|
|
1166
1309
|
const actions = options.actions ?? {
|
|
1167
|
-
steer: async (input: { runId: string; asyncDir: string; index?: number; message: string; mode: SteerDeliveryMode }) =>
|
|
1168
|
-
|
|
1169
|
-
runId
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1310
|
+
steer: async (input: { runId: string; asyncDir: string; index?: number; message: string; mode: SteerDeliveryMode }) => {
|
|
1311
|
+
const status = readStatus(input.asyncDir);
|
|
1312
|
+
const liveWorkflowRunId = status?.mode === "workflow" && state.workflowControllers?.has(status.runId || input.runId)
|
|
1313
|
+
? status.runId || input.runId
|
|
1314
|
+
: undefined;
|
|
1315
|
+
if (liveWorkflowRunId) {
|
|
1316
|
+
const route = state.foregroundControls.get(input.runId)?.parentWorkflowRunId === liveWorkflowRunId
|
|
1317
|
+
? resolveWorkflowForegroundSteeringTarget({ state, childRunId: input.runId, asyncDirRoot: options.asyncDirRoot ?? DIRS.async })
|
|
1318
|
+
: resolveWorkflowForegroundSteeringTarget({ state, workflowRunId: liveWorkflowRunId, asyncDirRoot: options.asyncDirRoot ?? DIRS.async });
|
|
1319
|
+
if (!route.ok) return { text: route.message, isError: true };
|
|
1320
|
+
return firstToolResultText(await steerWorkflowForegroundTarget({ target: route.target, message: input.message, mode: input.mode, ...(input.index !== undefined ? { index: input.index } : {}) }), `Failed to steer foreground run ${input.runId}.`);
|
|
1321
|
+
}
|
|
1322
|
+
return firstToolResultText(await steerAsyncRun({
|
|
1323
|
+
state,
|
|
1324
|
+
runId: input.runId,
|
|
1325
|
+
...(input.index !== undefined ? { index: input.index } : {}),
|
|
1326
|
+
message: input.message,
|
|
1327
|
+
mode: input.mode,
|
|
1328
|
+
location: { asyncDir: input.asyncDir, resolvedId: input.runId } as Parameters<typeof steerAsyncRun>[0]["location"],
|
|
1329
|
+
}), `Failed to steer async run ${input.runId}.`);
|
|
1330
|
+
},
|
|
1175
1331
|
stop: (input: { runId: string; asyncDir: string; index?: number }) => firstToolResultText(stopAsyncRun(state, input.runId, undefined, { asyncDir: input.asyncDir, resolvedId: input.runId }), `Failed to stop async run ${input.runId}.`),
|
|
1176
1332
|
inspect: async (input: { runId: string; asyncDir: string; index?: number }) => firstToolResultText(await handleHerdrInspectorAction("inspector.open", {
|
|
1177
1333
|
id: input.runId,
|
|
1178
1334
|
dir: input.asyncDir,
|
|
1335
|
+
focus: true,
|
|
1179
1336
|
...(input.index !== undefined ? { index: input.index } : {}),
|
|
1180
1337
|
}, {
|
|
1181
1338
|
state,
|
|
1182
1339
|
sessionRoots: state.trustedSessionRoots,
|
|
1183
1340
|
cwd: state.baseCwd,
|
|
1341
|
+
...(options.herdrClient ? { client: options.herdrClient } : {}),
|
|
1184
1342
|
...(state.authorityPolicy ? { authorityPolicy: state.authorityPolicy } : {}),
|
|
1185
1343
|
...(state.missionStoreConfig ? { missions: state.missionStoreConfig } : {}),
|
|
1186
1344
|
}), `Failed to open Herdr inspector for async run ${input.runId}.`),
|
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
|
}
|
|
@@ -90,13 +90,13 @@ export function resolveWorkflowChatProgress(input: ResolveWorkflowChatProgressIn
|
|
|
90
90
|
else mode = requestedMode;
|
|
91
91
|
|
|
92
92
|
if (mode === "live-card" && !sameRepo) return { error: "chatProgress: 'live-card' is only available for workflowScript runs in the same Git repository." };
|
|
93
|
-
if (mode === "live-card" && input.background) return { error: "chatProgress: 'live-card'
|
|
93
|
+
if (mode === "live-card" && input.background) return { error: "chatProgress: 'live-card' is unavailable for async workflowScript. Async workflows have no inline live card; omit chatProgress or use auto/off. Use async:false only when the parent must block." };
|
|
94
94
|
return { projection: { mode, repoRelation, ...(repoLabel ? { repoLabel } : {}) } };
|
|
95
95
|
}
|
|
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;
|