pi-subagents 0.55.0 → 0.57.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 +81 -3
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +111 -15
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +2 -2
- package/docs/models.md +6 -0
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +11 -1
- package/docs/workflows.md +20 -0
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/src/agents/agent-management.ts +13 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +242 -49
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-registry.ts +8 -2
- package/src/api/preflight.ts +22 -3
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +18 -4
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +20 -5
- package/src/extension/schemas.ts +9 -3
- package/src/extension/tool-description.ts +11 -9
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +46 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +148 -27
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +3 -0
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +5 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +9 -5
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +254 -49
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +107 -16
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +25 -3
- package/src/runs/foreground/subagent-executor.ts +324 -84
- package/src/runs/foreground/workflow-detach-reconcile.ts +31 -7
- package/src/runs/shared/acceptance.ts +10 -5
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/completion-guard.ts +3 -2
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/extension-bindings.ts +78 -0
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +349 -54
- package/src/runs/shared/fast-mode-extension.ts +10 -0
- package/src/runs/shared/model-exclusions.ts +59 -8
- package/src/runs/shared/model-fallback.ts +25 -8
- package/src/runs/shared/mutation-evidence.ts +150 -0
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +3 -0
- package/src/runs/shared/pi-args.ts +47 -0
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/runs/shared/structured-output.ts +18 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -5
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +7 -1
- package/src/shared/launch-contract.ts +6 -0
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +16 -3
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +166 -9
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +47 -21
- package/src/workflows/scripted-workflow.ts +298 -30
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +119 -4
package/src/tui/fleet-status.ts
CHANGED
|
@@ -25,6 +25,7 @@ type FleetStatusEntry = {
|
|
|
25
25
|
description?: string;
|
|
26
26
|
startedAt: number;
|
|
27
27
|
tokens: number;
|
|
28
|
+
window?: number;
|
|
28
29
|
state: string;
|
|
29
30
|
external?: true;
|
|
30
31
|
projectPane?: HerdrProjectPaneSnapshot;
|
|
@@ -39,6 +40,7 @@ type FleetWorkflowRow = {
|
|
|
39
40
|
activity?: string;
|
|
40
41
|
startedAt?: number;
|
|
41
42
|
tokens?: number;
|
|
43
|
+
window?: number;
|
|
42
44
|
overflow?: number;
|
|
43
45
|
};
|
|
44
46
|
|
|
@@ -72,12 +74,15 @@ export function formatFleetElapsed(ms: number): string {
|
|
|
72
74
|
return `${Math.max(0, Math.round(ms / 1000))}s`;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
export function formatFleetTokens(count: number): string {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
export function formatFleetTokens(count: number, window?: number): string {
|
|
78
|
+
const compact = (value: number): string => value >= 1_000_000
|
|
79
|
+
? `${(value / 1_000_000).toFixed(1)}M`
|
|
80
|
+
: value >= 1_000
|
|
81
|
+
? `${(value / 1_000).toFixed(1)}k`
|
|
82
|
+
: `${Math.max(0, Math.round(value))}`;
|
|
83
|
+
return window !== undefined
|
|
84
|
+
? `↓ ${compact(window)} window · ${compact(count)} spent`
|
|
85
|
+
: `↓ ${compact(count)} tokens`;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
function rightAlign(left: string, right: string, width: number): string {
|
|
@@ -134,6 +139,7 @@ function workflowFleetRows(steps: AsyncJobStep[] | undefined): FleetWorkflowRow[
|
|
|
134
139
|
...(activity ? { activity } : {}),
|
|
135
140
|
...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
|
|
136
141
|
...(step.tokens?.total !== undefined ? { tokens: step.tokens.total } : {}),
|
|
142
|
+
...(step.tokens?.window !== undefined ? { window: step.tokens.window } : {}),
|
|
137
143
|
};
|
|
138
144
|
});
|
|
139
145
|
}
|
|
@@ -332,7 +338,8 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
332
338
|
...(modelThinking ? { modelThinking } : {}),
|
|
333
339
|
description: foregroundDescription(control, child.description),
|
|
334
340
|
startedAt: child.startedAt,
|
|
335
|
-
|
|
341
|
+
tokens: child.tokens ?? 0,
|
|
342
|
+
...(child.window !== undefined ? { window: child.window } : {}),
|
|
336
343
|
state: "running",
|
|
337
344
|
...((control.nestedChildren?.filter((nested) => nested.parentStepIndex === child.index).length ?? 0) > 0
|
|
338
345
|
? { nestedChildren: control.nestedChildren!.filter((nested) => nested.parentStepIndex === child.index) }
|
|
@@ -350,6 +357,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
350
357
|
description: foregroundDescription(control, control.description),
|
|
351
358
|
startedAt: control.startedAt,
|
|
352
359
|
tokens: control.tokens ?? 0,
|
|
360
|
+
...(control.window !== undefined ? { window: control.window } : {}),
|
|
353
361
|
state: "running",
|
|
354
362
|
...(control.nestedChildren?.length ? { nestedChildren: control.nestedChildren } : {}),
|
|
355
363
|
});
|
|
@@ -367,6 +375,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
367
375
|
description: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
|
|
368
376
|
startedAt,
|
|
369
377
|
tokens: job.totalTokens?.total ?? 0,
|
|
378
|
+
...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
|
|
370
379
|
state: job.status,
|
|
371
380
|
...(job.steps?.length ? { workflowRows: workflowFleetRows(job.steps) } : {}),
|
|
372
381
|
...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
|
|
@@ -387,6 +396,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
387
396
|
description: job.description,
|
|
388
397
|
startedAt,
|
|
389
398
|
tokens: job.totalTokens?.total ?? 0,
|
|
399
|
+
...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
|
|
390
400
|
state: job.status,
|
|
391
401
|
...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
|
|
392
402
|
});
|
|
@@ -404,6 +414,9 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
404
414
|
description: step.description ?? job.description,
|
|
405
415
|
startedAt: step.startedAt ?? startedAt,
|
|
406
416
|
tokens: step.tokens?.total ?? (steps.length === 1 ? job.totalTokens?.total ?? 0 : 0),
|
|
417
|
+
...((step.tokens?.window ?? (steps.length === 1 ? job.totalTokens?.window : undefined)) !== undefined
|
|
418
|
+
? { window: step.tokens?.window ?? job.totalTokens?.window }
|
|
419
|
+
: {}),
|
|
407
420
|
state: step.status,
|
|
408
421
|
...((step.children?.length ?? 0) > 0
|
|
409
422
|
? { nestedChildren: step.children }
|
|
@@ -611,6 +624,10 @@ export class SubagentFleetStatus {
|
|
|
611
624
|
const workEntries = this.entries.filter((entry) => !entry.surface);
|
|
612
625
|
const projectEntries = this.entries.filter((entry) => entry.surface === "project-pane");
|
|
613
626
|
const tokens = workEntries.reduce((total, entry) => total + entry.tokens, 0);
|
|
627
|
+
const nativeEntries = workEntries.filter((entry) => !entry.external);
|
|
628
|
+
const window = nativeEntries.length > 0 && nativeEntries.every((entry) => entry.window !== undefined)
|
|
629
|
+
? nativeEntries.reduce((total, entry) => total + entry.window!, 0)
|
|
630
|
+
: undefined;
|
|
614
631
|
const capacity = this.state.activeAsyncCapacity;
|
|
615
632
|
const hasNativeRows = workEntries.some((entry) => !entry.external);
|
|
616
633
|
const showNativeSummary = hasNativeRows || Boolean(capacity?.used);
|
|
@@ -621,7 +638,7 @@ export class SubagentFleetStatus {
|
|
|
621
638
|
const paneAttention = projectEntries.filter((entry) => entry.projectPane && projectPaneNeedsAttention(entry.projectPane)).length;
|
|
622
639
|
const panes = projectEntries.length > 0 ? `${projectEntries.length} pane${projectEntries.length === 1 ? "" : "s"}${paneAttention ? ` (${paneAttention} ⚠)` : ""}` : "";
|
|
623
640
|
const label = [agents, asyncRuns, panes].filter(Boolean).join(" · ");
|
|
624
|
-
const detail = [showNativeSummary ? formatFleetTokens(tokens) : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
|
|
641
|
+
const detail = [showNativeSummary ? formatFleetTokens(tokens, window) : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
|
|
625
642
|
return [truncateToWidth(` ${theme.fg("muted", label)}${label && detail ? " · " : ""}${theme.fg("dim", detail)}`, width)];
|
|
626
643
|
}
|
|
627
644
|
const roster = this.rosterKeys();
|
|
@@ -670,7 +687,7 @@ export class SubagentFleetStatus {
|
|
|
670
687
|
const elapsed = Date.now() - entry.startedAt;
|
|
671
688
|
const rightText = entry.projectPane
|
|
672
689
|
? `${entry.projectPane.summary ?? "—"} · ${formatFleetElapsed(Date.now() - entry.projectPane.refreshedAt)} ago`
|
|
673
|
-
: entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens)}`;
|
|
690
|
+
: entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens, entry.window)}`;
|
|
674
691
|
const right = theme.fg("dim", rightText);
|
|
675
692
|
return rightAlign(left, right, width);
|
|
676
693
|
}
|
|
@@ -695,7 +712,7 @@ export class SubagentFleetStatus {
|
|
|
695
712
|
const left = `${indent}${marker} ${nestedStatusGlyph(row.state, theme)} ${theme.fg("muted", `${row.name}${modelThinking}`)} · ${row.state}${activity}`;
|
|
696
713
|
const details = [
|
|
697
714
|
row.startedAt !== undefined ? formatFleetElapsed(Date.now() - row.startedAt) : undefined,
|
|
698
|
-
row.tokens !== undefined ? formatFleetTokens(row.tokens) : undefined,
|
|
715
|
+
row.tokens !== undefined ? formatFleetTokens(row.tokens, row.window) : undefined,
|
|
699
716
|
].filter(Boolean).join(" · ");
|
|
700
717
|
return truncateToWidth(`${left}${details ? theme.fg("dim", ` · ${details}`) : ""}`, width);
|
|
701
718
|
}
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { getLanguageFromPath, highlightCode, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
5
5
|
import { safeTerminalText as safeDisplayText } from "../shared/display-text.ts";
|
|
6
|
+
import { isTrustedRecordedSessionFile } from "../shared/session-file-trust.ts";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_MAX_RECORDS = 240;
|
|
8
9
|
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
@@ -61,6 +62,8 @@ export interface FleetTranscript {
|
|
|
61
62
|
|
|
62
63
|
interface FleetTranscriptReadOptions {
|
|
63
64
|
trustedRoots: string[];
|
|
65
|
+
trustedFiles?: string[];
|
|
66
|
+
trustedFileRoot?: string;
|
|
64
67
|
maxRecords?: number;
|
|
65
68
|
maxBytes?: number;
|
|
66
69
|
}
|
|
@@ -109,10 +112,13 @@ function isNotFoundError(error: unknown): boolean {
|
|
|
109
112
|
return Boolean(objectValue(error)?.code === "ENOENT");
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
function validateTranscriptPath(filePath: string, trustedRoots: string[]): { resolvedPath?: string; warning?: string } {
|
|
113
|
-
if (trustedRoots.length === 0) return { warning: `Transcript preview has no trusted root: ${filePath}` };
|
|
115
|
+
function validateTranscriptPath(filePath: string, trustedRoots: string[], trustedFiles: string[] = [], trustedFileRoot?: string): { resolvedPath?: string; warning?: string } {
|
|
116
|
+
if (trustedRoots.length === 0 && (!trustedFileRoot || trustedFiles.length === 0)) return { warning: `Transcript preview has no trusted root: ${filePath}` };
|
|
114
117
|
const resolvedPath = path.resolve(filePath);
|
|
115
|
-
|
|
118
|
+
const recordedCandidate = trustedFileRoot
|
|
119
|
+
&& pathWithin(trustedFileRoot, resolvedPath)
|
|
120
|
+
&& trustedFiles.some((file) => path.resolve(file) === resolvedPath);
|
|
121
|
+
if (!trustedRoots.some((root) => pathWithin(root, resolvedPath)) && !recordedCandidate) {
|
|
116
122
|
return { warning: `Transcript is outside trusted roots: ${filePath}` };
|
|
117
123
|
}
|
|
118
124
|
let stat: fs.Stats;
|
|
@@ -129,7 +135,7 @@ function validateTranscriptPath(filePath: string, trustedRoots: string[]): { res
|
|
|
129
135
|
const realRoots = trustedRoots
|
|
130
136
|
.filter((root) => fs.existsSync(root))
|
|
131
137
|
.map((root) => fs.realpathSync(root));
|
|
132
|
-
if (!realRoots.some((root) => pathWithin(root, realPath))) {
|
|
138
|
+
if (!realRoots.some((root) => pathWithin(root, realPath)) && !isTrustedRecordedSessionFile(realPath, trustedFiles, trustedFileRoot)) {
|
|
133
139
|
return { warning: `Transcript resolves outside trusted roots: ${filePath}` };
|
|
134
140
|
}
|
|
135
141
|
return { resolvedPath: realPath };
|
|
@@ -335,7 +341,7 @@ function parseTranscriptLines(lines: string[], conversationStarted = false): { e
|
|
|
335
341
|
}
|
|
336
342
|
|
|
337
343
|
export function readFleetTranscript(filePath: string, options: FleetTranscriptReadOptions): FleetTranscript {
|
|
338
|
-
const validated = validateTranscriptPath(filePath, options.trustedRoots);
|
|
344
|
+
const validated = validateTranscriptPath(filePath, options.trustedRoots, options.trustedFiles, options.trustedFileRoot);
|
|
339
345
|
if (!validated.resolvedPath) {
|
|
340
346
|
return { path: filePath, events: [], truncated: false, ...(validated.warning ? { warning: safeDisplayText(validated.warning) } : {}) };
|
|
341
347
|
}
|
package/src/tui/fleet.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { getMarkdownTheme, type ExtensionContext } from "@earendil-works/pi-codi
|
|
|
5
5
|
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
6
6
|
import { snapshotExternalRuns, type ExternalRun } from "../api/external-runs.ts";
|
|
7
7
|
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
8
|
-
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../shared/formatters.ts";
|
|
8
|
+
import { formatDuration, formatModelThinking, formatTokens, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
|
|
9
9
|
import { DIRS, type AsyncJobState, type AsyncJobStep, type Details, type FleetKeybindingAction, type FleetKeybindingsConfig, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
|
|
10
10
|
import { decodeUtf8Tail } from "../shared/utf8.ts";
|
|
11
11
|
import { readStatus } from "../shared/utils.ts";
|
|
@@ -403,7 +403,9 @@ function foregroundActiveDetail(item: Extract<FleetItem, { kind: "foreground-act
|
|
|
403
403
|
live.currentTool ? `Current tool: ${live.currentTool}${live.currentPath ? ` · ${shortenPath(live.currentPath)}` : ""}` : undefined,
|
|
404
404
|
live.turnCount !== undefined ? `Turns: ${live.turnCount}` : undefined,
|
|
405
405
|
live.toolCount !== undefined ? `Tools: ${live.toolCount}` : undefined,
|
|
406
|
-
live.tokens !== undefined
|
|
406
|
+
live.tokens !== undefined
|
|
407
|
+
? `Tokens: ${formatTokenUsage({ input: live.inputTokens ?? 0, output: live.outputTokens ?? 0, total: live.tokens, ...(live.window !== undefined ? { window: live.window } : {}), ...(live.windowPeak !== undefined ? { windowPeak: live.windowPeak } : {}) }, "tokens")}`
|
|
408
|
+
: undefined,
|
|
407
409
|
promptAuditCount > 0 ? `Prompt audit: ${promptAuditCount} live · 3 views · p opens` : undefined,
|
|
408
410
|
"",
|
|
409
411
|
"Transcript",
|
|
@@ -533,7 +535,7 @@ function workflowProgressLines(steps: AsyncJobStep[] | undefined): string[] {
|
|
|
533
535
|
}
|
|
534
536
|
const activity = workflowStepActivity(row.step);
|
|
535
537
|
const context = contextModeLabel(row.step.context);
|
|
536
|
-
const details = [row.step.status, activity, context, row.step.tokens
|
|
538
|
+
const details = [row.step.status, activity, context, row.step.tokens ? formatTokenUsage(row.step.tokens) : undefined].filter(Boolean).join(" · ");
|
|
537
539
|
lines.push(` ${row.index + 1}. ${workflowStepLabel(row.step, row.index)}${details ? ` — ${details}` : ""}`);
|
|
538
540
|
}
|
|
539
541
|
return lines;
|
|
@@ -547,6 +549,8 @@ function asyncDetail(item: Extract<FleetItem, { kind: "async" }>, state: Subagen
|
|
|
547
549
|
index: item.index,
|
|
548
550
|
lines: TRANSCRIPT_LINES,
|
|
549
551
|
sessionRoots: uniquePaths([...(state.trustedSessionRoots ?? []), trackedJob?.sessionRoot]),
|
|
552
|
+
trustedSessionFiles: [item.step?.sessionFile ?? item.run.sessionFile].filter((value): value is string => Boolean(value)),
|
|
553
|
+
trustedSessionFileRoot: state.trustedSessionFileRoot,
|
|
550
554
|
}).split("\n");
|
|
551
555
|
if (status.mode === "workflow" && item.index === undefined) {
|
|
552
556
|
const progress = workflowProgressLines((status.steps ?? item.run.steps) as AsyncJobStep[]);
|
|
@@ -628,7 +632,7 @@ function fleetArtifactsRoot(state: SubagentState, cwd: string): string {
|
|
|
628
632
|
);
|
|
629
633
|
}
|
|
630
634
|
|
|
631
|
-
function transcriptTarget(item: FleetItem, state: SubagentState): { path: string; trustedRoots: string[] } | undefined {
|
|
635
|
+
function transcriptTarget(item: FleetItem, state: SubagentState): { path: string; trustedRoots: string[]; trustedFiles?: string[]; trustedFileRoot?: string } | undefined {
|
|
632
636
|
if (item.kind === "external") return undefined;
|
|
633
637
|
if (item.kind === "foreground-active") {
|
|
634
638
|
const artifactsRoot = fleetArtifactsRoot(state, item.control.cwd ?? state.baseCwd);
|
|
@@ -651,10 +655,12 @@ function transcriptTarget(item: FleetItem, state: SubagentState): { path: string
|
|
|
651
655
|
};
|
|
652
656
|
}
|
|
653
657
|
const step = item.step ?? (item.run.steps.length === 1 ? item.run.steps[0] : undefined);
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
+
const recordedSessionFile = step?.sessionFile ?? item.run.sessionFile;
|
|
659
|
+
const recordedPath = step?.transcriptPath ?? recordedSessionFile;
|
|
660
|
+
if (!recordedPath) return undefined;
|
|
661
|
+
const transcriptPath = path.isAbsolute(recordedPath)
|
|
662
|
+
? recordedPath
|
|
663
|
+
: path.resolve(item.run.asyncDir, recordedPath);
|
|
658
664
|
const trackedJob = state.fleetJobs?.get(item.runId) ?? state.asyncJobs.get(item.runId);
|
|
659
665
|
return {
|
|
660
666
|
path: transcriptPath,
|
|
@@ -664,6 +670,7 @@ function transcriptTarget(item: FleetItem, state: SubagentState): { path: string
|
|
|
664
670
|
trackedJob?.cwd ? fleetArtifactsRoot(state, trackedJob.cwd) : undefined,
|
|
665
671
|
item.run.sessionFile ? getArtifactsDir(item.run.sessionFile, item.run.cwd ?? state.baseCwd, state.artifactDirPreference) : undefined,
|
|
666
672
|
]),
|
|
673
|
+
...(!step?.transcriptPath && recordedSessionFile ? { trustedFiles: [recordedSessionFile], trustedFileRoot: state.trustedSessionFileRoot } : {}),
|
|
667
674
|
};
|
|
668
675
|
}
|
|
669
676
|
|
|
@@ -687,23 +694,27 @@ function itemSource(item: FleetItem): string {
|
|
|
687
694
|
function itemStats(item: FleetItem): string[] {
|
|
688
695
|
let model: string | undefined;
|
|
689
696
|
let tokens: number | undefined;
|
|
697
|
+
let tokenUsage: AsyncJobStep["tokens"];
|
|
690
698
|
let tools: number | undefined;
|
|
691
699
|
let durationMs: number | undefined;
|
|
692
700
|
if (item.kind === "foreground-active") {
|
|
693
701
|
const live = item.activeChild ?? item.control;
|
|
694
702
|
model = formatModelThinking(live.model, live.thinking) || undefined;
|
|
695
703
|
tokens = live.tokens;
|
|
704
|
+
if (tokens !== undefined) tokenUsage = { input: live.inputTokens ?? 0, output: live.outputTokens ?? 0, total: tokens, ...(live.window !== undefined ? { window: live.window } : {}), ...(live.windowPeak !== undefined ? { windowPeak: live.windowPeak } : {}) };
|
|
696
705
|
tools = live.toolCount;
|
|
697
706
|
durationMs = Math.max(0, Date.now() - live.startedAt);
|
|
698
707
|
} else if (item.kind === "foreground-recent") {
|
|
699
708
|
model = formatModelThinking(item.child.model, item.child.thinking) || undefined;
|
|
700
709
|
tokens = item.child.tokens;
|
|
710
|
+
if (tokens !== undefined) tokenUsage = { input: 0, output: 0, total: tokens, ...(item.child.window !== undefined ? { window: item.child.window } : {}), ...(item.child.windowPeak !== undefined ? { windowPeak: item.child.windowPeak } : {}) };
|
|
701
711
|
tools = item.child.toolCount;
|
|
702
712
|
} else if (item.kind === "external") {
|
|
703
713
|
durationMs = Math.max(0, externalElapsedEnd(item.run) - item.run.startedAt);
|
|
704
714
|
} else {
|
|
705
715
|
model = formatModelThinking(item.step?.model, item.step?.thinking) || undefined;
|
|
706
|
-
|
|
716
|
+
tokenUsage = item.step?.tokens ?? (item.index === undefined ? item.run.totalTokens : undefined);
|
|
717
|
+
tokens = tokenUsage?.total;
|
|
707
718
|
tools = item.step?.toolCount ?? (item.index === undefined ? item.run.toolCount : undefined);
|
|
708
719
|
const terminalRun = item.state !== "queued" && item.state !== "running" && item.state !== "pending";
|
|
709
720
|
const endTime = item.run.endedAt ?? (terminalRun ? item.run.lastUpdate : undefined) ?? Date.now();
|
|
@@ -711,7 +722,7 @@ function itemStats(item: FleetItem): string[] {
|
|
|
711
722
|
}
|
|
712
723
|
return [
|
|
713
724
|
model,
|
|
714
|
-
tokens !== undefined ? `${formatTokens(tokens)} tok` : undefined,
|
|
725
|
+
tokenUsage ? formatTokenUsage(tokenUsage) : tokens !== undefined ? `${formatTokens(tokens)} tok` : undefined,
|
|
715
726
|
tools !== undefined ? `${tools} tool${tools === 1 ? "" : "s"}` : undefined,
|
|
716
727
|
durationMs !== undefined ? formatDuration(durationMs) : undefined,
|
|
717
728
|
].filter((value): value is string => Boolean(value));
|
|
@@ -1201,8 +1212,8 @@ export class SubagentFleetComponent implements Component {
|
|
|
1201
1212
|
});
|
|
1202
1213
|
}
|
|
1203
1214
|
|
|
1204
|
-
private renderedTranscript(target: { path: string; trustedRoots: string[] }, width: number): { transcript: FleetTranscript; body: string[] } {
|
|
1205
|
-
const fingerprint = `${target.trustedRoots.join("\0")}|${transcriptFingerprint(target.path)}`;
|
|
1215
|
+
private renderedTranscript(target: { path: string; trustedRoots: string[]; trustedFiles?: string[]; trustedFileRoot?: string }, width: number): { transcript: FleetTranscript; body: string[] } {
|
|
1216
|
+
const fingerprint = `${target.trustedRoots.join("\0")}|${target.trustedFiles?.join("\0") ?? ""}|${target.trustedFileRoot ?? ""}|${transcriptFingerprint(target.path)}`;
|
|
1206
1217
|
if (this.transcriptCache
|
|
1207
1218
|
&& this.transcriptCache.path === target.path
|
|
1208
1219
|
&& this.transcriptCache.fingerprint === fingerprint
|
|
@@ -1210,7 +1221,11 @@ export class SubagentFleetComponent implements Component {
|
|
|
1210
1221
|
&& this.transcriptCache.expandedTools === this.expandedTools) {
|
|
1211
1222
|
return { transcript: this.transcriptCache.transcript, body: [...this.transcriptCache.body] };
|
|
1212
1223
|
}
|
|
1213
|
-
const transcript = readFleetTranscript(target.path, {
|
|
1224
|
+
const transcript = readFleetTranscript(target.path, {
|
|
1225
|
+
trustedRoots: target.trustedRoots,
|
|
1226
|
+
...(target.trustedFiles ? { trustedFiles: target.trustedFiles } : {}),
|
|
1227
|
+
...(target.trustedFileRoot ? { trustedFileRoot: target.trustedFileRoot } : {}),
|
|
1228
|
+
});
|
|
1214
1229
|
const body = transcript.events.length > 0
|
|
1215
1230
|
? renderFleetTranscript(transcript, width, this.theme, this.markdownTheme, { expandedTools: this.expandedTools })
|
|
1216
1231
|
: [];
|
package/src/tui/render.ts
CHANGED
|
@@ -21,9 +21,10 @@ import {
|
|
|
21
21
|
WIDGET_ANIMATION_INTERVAL_MS,
|
|
22
22
|
WIDGET_KEY,
|
|
23
23
|
} from "../shared/types.ts";
|
|
24
|
-
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
24
|
+
import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
25
|
+
import { FLEET_OPEN_SHORTCUT, formatShortcutLabel } from "../shared/shortcuts.ts";
|
|
26
|
+
import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
|
|
27
|
+
import { getDisplayItems, getSingleResultOutput, PROMPT_REDACTED } from "../shared/utils.ts";
|
|
27
28
|
import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
|
|
28
29
|
import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
|
|
29
30
|
import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
|
|
@@ -69,23 +70,13 @@ function liveDetailKeyText(): string {
|
|
|
69
70
|
return keyText("app.tools.expand");
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
function liveDetailHintText(): string {
|
|
73
|
-
return `Press ${liveDetailKeyText()} for live detail`;
|
|
73
|
+
export function liveDetailHintText(): string {
|
|
74
|
+
return `Press ${liveDetailKeyText()} for live detail · ${formatShortcutLabel(FLEET_OPEN_SHORTCUT)} Fleet`;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
function foregroundSingleHintText(shortcut?: string): string {
|
|
77
78
|
if (!shortcut) return liveDetailHintText();
|
|
78
|
-
const label = shortcut
|
|
79
|
-
.split("+")
|
|
80
|
-
.map((part) => {
|
|
81
|
-
const normalized = part.trim().toLowerCase();
|
|
82
|
-
if (normalized === "ctrl") return "Ctrl";
|
|
83
|
-
if (normalized === "alt") return "Alt";
|
|
84
|
-
if (normalized === "shift") return "Shift";
|
|
85
|
-
if (normalized === "super") return "Super";
|
|
86
|
-
return normalized.length === 1 ? normalized.toUpperCase() : part.trim();
|
|
87
|
-
})
|
|
88
|
-
.join("+");
|
|
79
|
+
const label = formatShortcutLabel(shortcut);
|
|
89
80
|
return `${liveDetailHintText()} · ${label} to run in background`;
|
|
90
81
|
}
|
|
91
82
|
|
|
@@ -273,6 +264,29 @@ function oneLine(text: string): string {
|
|
|
273
264
|
return text.replace(ansiEscapePattern, "").replace(/\s+/g, " ").trim();
|
|
274
265
|
}
|
|
275
266
|
|
|
267
|
+
const COMPACT_TASK_MAX_CHARS = 96;
|
|
268
|
+
|
|
269
|
+
export function compactTaskText(task: string | undefined, label?: string): string | undefined {
|
|
270
|
+
const source = label?.trim() || task?.trim();
|
|
271
|
+
if (!source || source === PROMPT_REDACTED) return undefined;
|
|
272
|
+
const normalized = oneLine(source);
|
|
273
|
+
if (!normalized) return undefined;
|
|
274
|
+
return previewDisplayText(normalized, COMPACT_TASK_MAX_CHARS);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function workflowLabelForResult(details: Details, resultIndex: number): string | undefined {
|
|
278
|
+
const flatIndex = details.results[resultIndex]?.progress?.index ?? resultIndex;
|
|
279
|
+
const visit = (nodes: NonNullable<Details["workflowGraph"]>["nodes"]): string | undefined => {
|
|
280
|
+
for (const node of nodes) {
|
|
281
|
+
if (node.flatIndex === flatIndex && node.label.trim()) return node.label;
|
|
282
|
+
const nested = node.children ? visit(node.children) : undefined;
|
|
283
|
+
if (nested) return nested;
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
};
|
|
287
|
+
return details.workflowGraph ? visit(details.workflowGraph.nodes) : undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
276
290
|
function hasLiveOutputSignal(line: string): boolean {
|
|
277
291
|
const clean = oneLine(line);
|
|
278
292
|
return liveOutputWordSignalPattern.test(clean) || liveOutputCodeSignalPattern.test(clean);
|
|
@@ -740,7 +754,7 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], s
|
|
|
740
754
|
if (step.currentPath) facts.push(shortenPath(step.currentPath));
|
|
741
755
|
if (step.turnCount !== undefined) facts.push(`${step.turnCount} turns`);
|
|
742
756
|
if (step.toolCount !== undefined) facts.push(`${step.toolCount} tools`);
|
|
743
|
-
if (step.tokens?.total) facts.push(
|
|
757
|
+
if (step.tokens?.total) facts.push(formatTokenUsage(step.tokens, "token"));
|
|
744
758
|
const activity = buildLiveStatusLine(step, snapshotNow);
|
|
745
759
|
if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
|
|
746
760
|
if (activity) return activity;
|
|
@@ -780,7 +794,9 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
|
|
|
780
794
|
const activity = widgetStepActivity(step, job.updatedAt);
|
|
781
795
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
782
796
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
783
|
-
|
|
797
|
+
const label = compactTaskText(undefined, step.label);
|
|
798
|
+
const display = label ? `${label} (${step.agent})` : step.agent;
|
|
799
|
+
lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${display} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
|
|
784
800
|
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
|
|
785
801
|
}
|
|
786
802
|
return lines;
|
|
@@ -1058,7 +1074,7 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
|
|
|
1058
1074
|
parts.push(`steps ${stepsTotal}`);
|
|
1059
1075
|
}
|
|
1060
1076
|
if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
|
|
1061
|
-
if (job.totalTokens?.total) parts.push(
|
|
1077
|
+
if (job.totalTokens?.total) parts.push(formatTokenUsage(job.totalTokens, "token"));
|
|
1062
1078
|
if (job.startedAt !== undefined && job.updatedAt !== undefined) parts.push(formatDuration(Math.max(0, job.updatedAt - job.startedAt)));
|
|
1063
1079
|
return statJoin(theme, parts);
|
|
1064
1080
|
}
|
|
@@ -1067,7 +1083,7 @@ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>
|
|
|
1067
1083
|
return statJoin(theme, [
|
|
1068
1084
|
step.turnCount !== undefined ? `${step.turnCount} turns` : "",
|
|
1069
1085
|
step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
|
|
1070
|
-
step.tokens?.total ?
|
|
1086
|
+
step.tokens?.total ? formatTokenUsage(step.tokens, "token") : "",
|
|
1071
1087
|
step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
|
|
1072
1088
|
]);
|
|
1073
1089
|
}
|
|
@@ -1259,6 +1275,8 @@ function foregroundStyleWidgetStepLines(
|
|
|
1259
1275
|
const stats = widgetStepStats(theme, step);
|
|
1260
1276
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
1261
1277
|
const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), frame)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
|
|
1278
|
+
const task = compactTaskText(step.description, step.label);
|
|
1279
|
+
if (task) lines.push(` ${theme.fg("dim", `task: ${task}`)}`);
|
|
1262
1280
|
const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
|
|
1263
1281
|
if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
|
|
1264
1282
|
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
|
|
@@ -1329,7 +1347,9 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
|
|
|
1329
1347
|
const stepStats = widgetStepStats(theme, step);
|
|
1330
1348
|
const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
|
|
1331
1349
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
1332
|
-
|
|
1350
|
+
const task = compactTaskText(step.description, step.label);
|
|
1351
|
+
const taskSuffix = task ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", `task: ${task}`)}` : "";
|
|
1352
|
+
lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${taskSuffix}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
|
|
1333
1353
|
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
|
|
1334
1354
|
}
|
|
1335
1355
|
if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
|
|
@@ -1703,6 +1723,8 @@ function renderSingleCompact(
|
|
|
1703
1723
|
c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, undefined, frame)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
1704
1724
|
|
|
1705
1725
|
if (isRunning && r.progress) {
|
|
1726
|
+
const task = compactTaskText(r.task);
|
|
1727
|
+
if (task) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}task: ${task}`), width), 0, 0));
|
|
1706
1728
|
const progressSnapshotNow = snapshotNowForProgress(r.progress);
|
|
1707
1729
|
const activity = compactCurrentActivity(r.progress);
|
|
1708
1730
|
c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}⎿ ${activity}`), width), 0, 0));
|
|
@@ -1872,6 +1894,10 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
|
|
|
1872
1894
|
const rowModelDisplay = modelThinkingBadge(theme, r.model ?? rowProgressModel?.model, r.thinking ?? rowProgressModel?.thinking);
|
|
1873
1895
|
const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
|
|
1874
1896
|
c.addChild(new Text(truncLine(`${rowIndent}${line}`, width), 0, 0));
|
|
1897
|
+
if (rRunning || rPending) {
|
|
1898
|
+
const task = compactTaskText(r.task, workflowLabelForResult(d, i));
|
|
1899
|
+
if (task) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}task: ${task}`), width), 0, 0));
|
|
1900
|
+
}
|
|
1875
1901
|
if (rRunning && rProg && "status" in rProg) {
|
|
1876
1902
|
const liveProgress = rProg as AgentProgress;
|
|
1877
1903
|
const activity = compactCurrentActivity(liveProgress);
|