pi-subagents 0.45.2 → 0.47.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 +47 -0
- package/README.md +2 -0
- package/docs/agents.md +342 -0
- package/docs/configuration.md +328 -0
- package/docs/extension-api.md +308 -0
- package/docs/missions.md +119 -0
- package/docs/models.md +192 -0
- package/docs/observability.md +174 -0
- package/docs/tool-reference.md +343 -0
- package/docs/watchdog.md +176 -0
- package/docs/workflows.md +163 -0
- package/package.json +4 -2
- package/skills/pi-subagents/references/execution-controls.md +6 -6
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/src/agents/agents.ts +17 -8
- package/src/agents/frontmatter.ts +7 -3
- package/src/agents/skills.ts +2 -9
- package/src/api/project-panes.ts +30 -0
- package/src/extension/config.ts +18 -1
- package/src/extension/fanout-child.ts +5 -4
- package/src/extension/index.ts +66 -19
- package/src/extension/rpc.ts +3 -6
- package/src/extension/schemas.ts +28 -7
- package/src/extension/subagent-guide.ts +39 -0
- package/src/extension/tool-description.ts +30 -12
- package/src/inspectors/herdr/project-panes.ts +459 -63
- package/src/missions/actions.ts +25 -2
- package/src/missions/lifecycle.ts +21 -2
- package/src/missions/store.ts +79 -2
- package/src/missions/types.ts +33 -0
- package/src/missions/workflow-state.ts +19 -13
- package/src/runs/background/async-execution.ts +17 -6
- package/src/runs/background/async-job-tracker.ts +15 -0
- package/src/runs/background/async-resume.ts +19 -3
- package/src/runs/background/async-status.ts +6 -1
- package/src/runs/background/completion-replay.ts +267 -0
- package/src/runs/background/control-channel.ts +36 -0
- package/src/runs/background/result-watcher.ts +28 -6
- package/src/runs/background/scheduled-runs.ts +2 -1
- package/src/runs/background/stale-run-reconciler.ts +2 -21
- package/src/runs/background/subagent-runner.ts +47 -6
- package/src/runs/background/wait-completions.ts +39 -5
- package/src/runs/background/wait-subscriptions.ts +18 -3
- package/src/runs/foreground/async-steering-action.ts +1 -1
- package/src/runs/foreground/chain-execution.ts +3 -0
- package/src/runs/foreground/execution.ts +7 -0
- package/src/runs/foreground/foreground-history.ts +137 -0
- package/src/runs/foreground/subagent-executor.ts +403 -54
- package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +8 -4
- package/src/runs/shared/model-scope.ts +12 -2
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/worktree.ts +3 -2
- package/src/shared/artifacts.ts +14 -14
- package/src/shared/display-text.ts +100 -0
- package/src/shared/fork-context.ts +13 -0
- package/src/shared/formatters.ts +4 -6
- package/src/shared/prompt-resources.ts +51 -0
- package/src/shared/settings.ts +15 -2
- package/src/shared/types.ts +41 -2
- package/src/shared/utf8.ts +11 -0
- package/src/shared/utils.ts +43 -33
- package/src/slash/prompt-workflows.ts +2 -15
- package/src/slash/slash-commands.ts +22 -2
- package/src/tui/fleet-status.ts +22 -12
- package/src/tui/fleet.ts +135 -25
- package/src/tui/render.ts +150 -33
- package/src/watchdog/change-signature.ts +4 -3
- package/src/workflows/scripted-workflow.ts +167 -10
package/src/tui/fleet.ts
CHANGED
|
@@ -2,10 +2,11 @@ import * as fs from "node:fs";
|
|
|
2
2
|
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
|
-
import {
|
|
5
|
+
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
6
6
|
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
7
7
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../shared/formatters.ts";
|
|
8
|
-
import { DIRS, type AsyncJobState, type Details, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
|
|
8
|
+
import { DIRS, type AsyncJobState, type Details, type FleetKeybindingAction, type FleetKeybindingsConfig, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
|
|
9
|
+
import { decodeUtf8Tail } from "../shared/utf8.ts";
|
|
9
10
|
import { readStatus } from "../shared/utils.ts";
|
|
10
11
|
import { formatAsyncRunTranscript } from "../runs/background/fleet-view.ts";
|
|
11
12
|
import { listAsyncRuns, type AsyncRunSummary } from "../runs/background/async-status.ts";
|
|
@@ -21,6 +22,47 @@ const REFRESH_MS = 750;
|
|
|
21
22
|
const MAX_RECENT_ASYNC_RUNS = 20;
|
|
22
23
|
const MAX_FLEET_HISTORY_CANDIDATES = 100;
|
|
23
24
|
const TRANSCRIPT_LINES = 200;
|
|
25
|
+
const OUTPUT_TAIL_BYTES = 64 * 1024;
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_FLEET_KEYBINDINGS: Record<FleetKeybindingAction, string[]> = {
|
|
28
|
+
close: ["escape", "ctrl+c", "q"],
|
|
29
|
+
scrollUp: ["K"],
|
|
30
|
+
scrollDown: ["J"],
|
|
31
|
+
selectUp: ["up", "k"],
|
|
32
|
+
selectDown: ["down", "j"],
|
|
33
|
+
selectFirst: ["home"],
|
|
34
|
+
selectLast: ["end"],
|
|
35
|
+
pageUp: ["pageUp"],
|
|
36
|
+
pageDown: ["pageDown"],
|
|
37
|
+
refresh: ["r", "R"],
|
|
38
|
+
steer: ["s"],
|
|
39
|
+
inspect: ["H"],
|
|
40
|
+
stop: ["D"],
|
|
41
|
+
toggleTools: ["x", "X", "ctrl+o"],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type ResolvedFleetKeybindings = Record<FleetKeybindingAction, string[]>;
|
|
45
|
+
|
|
46
|
+
export function resolveFleetKeybindings(config: FleetKeybindingsConfig | undefined): ResolvedFleetKeybindings {
|
|
47
|
+
return Object.fromEntries(
|
|
48
|
+
Object.entries(DEFAULT_FLEET_KEYBINDINGS).map(([action, defaults]) => [action, config?.[action as FleetKeybindingAction] ?? defaults]),
|
|
49
|
+
) as ResolvedFleetKeybindings;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function matchesFleetBinding(data: string, binding: string): boolean {
|
|
53
|
+
const key = /^[A-Z]$/.test(binding) ? `shift+${binding.toLowerCase()}` : binding;
|
|
54
|
+
return matchesKey(data, key as Parameters<typeof matchesKey>[1]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function matchesFleetAction(data: string, bindings: ResolvedFleetKeybindings, action: FleetKeybindingAction): boolean {
|
|
58
|
+
return bindings[action].some((binding) => matchesFleetBinding(data, binding));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function bindingLabel(bindings: ResolvedFleetKeybindings, action: FleetKeybindingAction): string {
|
|
62
|
+
return bindings[action]
|
|
63
|
+
.map((binding) => binding === "up" ? "↑" : binding === "down" ? "↓" : binding === "escape" ? "Esc" : binding === "return" ? "Enter" : binding)
|
|
64
|
+
.join("/");
|
|
65
|
+
}
|
|
24
66
|
|
|
25
67
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
26
68
|
type FleetTui = {
|
|
@@ -57,6 +99,7 @@ export interface FleetViewOptions {
|
|
|
57
99
|
refreshMs?: number;
|
|
58
100
|
initialKey?: string;
|
|
59
101
|
markdownTheme?: MarkdownTheme;
|
|
102
|
+
fleetKeybindings?: FleetKeybindingsConfig;
|
|
60
103
|
actions?: FleetActionHandlers;
|
|
61
104
|
}
|
|
62
105
|
|
|
@@ -246,6 +289,7 @@ function foregroundActiveDetail(item: Extract<FleetItem, { kind: "foreground-act
|
|
|
246
289
|
"Source: foreground",
|
|
247
290
|
`State: running`,
|
|
248
291
|
`Mode: ${control.mode}`,
|
|
292
|
+
control.parentWorkflowRunId ? `Workflow child of: ${control.parentWorkflowRunId}${control.workflowKey ? ` (${control.workflowKey})` : ""}` : undefined,
|
|
249
293
|
item.index !== undefined ? `Child: ${item.index} (${item.agent})` : `Agent: ${item.agent}`,
|
|
250
294
|
modelThinking ? `Model: ${modelThinking}` : undefined,
|
|
251
295
|
`Started: ${new Date(live.startedAt).toISOString()}`,
|
|
@@ -260,7 +304,59 @@ function foregroundActiveDetail(item: Extract<FleetItem, { kind: "foreground-act
|
|
|
260
304
|
return lines.filter((line): line is string => line !== undefined);
|
|
261
305
|
}
|
|
262
306
|
|
|
263
|
-
function
|
|
307
|
+
function pathWithin(base: string, candidate: string): boolean {
|
|
308
|
+
const resolvedBase = path.resolve(base);
|
|
309
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
310
|
+
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function trustedFileTail(filePath: string, trustedRoots: string[]): { text?: string; warning?: string; unavailable?: string } {
|
|
314
|
+
const resolvedPath = path.resolve(filePath);
|
|
315
|
+
if (trustedRoots.length === 0 || !trustedRoots.some((root) => pathWithin(root, resolvedPath))) return { warning: `output artifact is outside trusted roots: ${filePath}` };
|
|
316
|
+
let stat: fs.Stats;
|
|
317
|
+
try {
|
|
318
|
+
stat = fs.lstatSync(resolvedPath);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return { unavailable: `output artifact unavailable: ${filePath}` };
|
|
321
|
+
return { warning: `output artifact could not be inspected: ${error instanceof Error ? error.message : String(error)}` };
|
|
322
|
+
}
|
|
323
|
+
if (stat.isSymbolicLink()) return { warning: `output artifact refused a symlink: ${filePath}` };
|
|
324
|
+
if (!stat.isFile()) return { warning: `output artifact is not a file: ${filePath}` };
|
|
325
|
+
try {
|
|
326
|
+
const realPath = fs.realpathSync(resolvedPath);
|
|
327
|
+
const realRoots = trustedRoots.filter((root) => fs.existsSync(root)).map((root) => fs.realpathSync(root));
|
|
328
|
+
if (!realRoots.some((root) => pathWithin(root, realPath))) return { warning: `output artifact resolves outside trusted roots: ${filePath}` };
|
|
329
|
+
const fd = fs.openSync(realPath, "r");
|
|
330
|
+
try {
|
|
331
|
+
const bytes = Math.min(stat.size, OUTPUT_TAIL_BYTES);
|
|
332
|
+
const buffer = Buffer.alloc(bytes);
|
|
333
|
+
fs.readSync(fd, buffer, 0, bytes, stat.size - bytes);
|
|
334
|
+
return { text: decodeUtf8Tail(buffer) };
|
|
335
|
+
} finally {
|
|
336
|
+
fs.closeSync(fd);
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return { warning: `output artifact could not be read: ${error instanceof Error ? error.message : String(error)}` };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function foregroundRecentOutputLines(item: Extract<FleetItem, { kind: "foreground-recent" }>, state: SubagentState): string[] {
|
|
344
|
+
const outputPath = item.child.artifactPaths?.outputPath ?? item.child.savedOutputPath;
|
|
345
|
+
const output = item.child.finalOutput
|
|
346
|
+
? { text: item.child.finalOutput }
|
|
347
|
+
: outputPath
|
|
348
|
+
? trustedFileTail(path.isAbsolute(outputPath) ? outputPath : path.resolve(item.run.cwd, outputPath), uniquePaths([
|
|
349
|
+
fleetArtifactsRoot(state, item.run.cwd),
|
|
350
|
+
fleetArtifactsRoot(state, state.baseCwd),
|
|
351
|
+
]))
|
|
352
|
+
: undefined;
|
|
353
|
+
if (output?.warning) return [`(${output.warning})`];
|
|
354
|
+
if (output?.unavailable) return [`(${output.unavailable})`];
|
|
355
|
+
const outputLines = (output?.text ?? "").split(/\r?\n/).filter((line) => line.trim()).slice(-TRANSCRIPT_LINES);
|
|
356
|
+
return outputLines.length ? outputLines : ["(no recovered output available)"];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function foregroundRecentDetail(item: Extract<FleetItem, { kind: "foreground-recent" }>, state: SubagentState): string[] {
|
|
264
360
|
const { child, run } = item;
|
|
265
361
|
const outputPath = child.artifactPaths?.outputPath ?? child.savedOutputPath;
|
|
266
362
|
const modelThinking = formatModelThinking(child.model, child.thinking);
|
|
@@ -281,8 +377,7 @@ function foregroundRecentDetail(item: Extract<FleetItem, { kind: "foreground-rec
|
|
|
281
377
|
"",
|
|
282
378
|
"Result transcript tail",
|
|
283
379
|
];
|
|
284
|
-
|
|
285
|
-
lines.push(...(outputLines.length ? outputLines : ["(no recovered output available)"]));
|
|
380
|
+
lines.push(...foregroundRecentOutputLines(item, state));
|
|
286
381
|
return lines.filter((line): line is string => line !== undefined);
|
|
287
382
|
}
|
|
288
383
|
|
|
@@ -306,12 +401,12 @@ function asyncDetail(item: Extract<FleetItem, { kind: "async" }>): string[] {
|
|
|
306
401
|
].filter((line): line is string => line !== undefined);
|
|
307
402
|
}
|
|
308
403
|
|
|
309
|
-
function detailLines(item: FleetItem | undefined, error: string | undefined): string[] {
|
|
404
|
+
function detailLines(item: FleetItem | undefined, error: string | undefined, state: SubagentState): string[] {
|
|
310
405
|
if (!item) return [error ? `Fleet scan failed: ${error}` : "No current-session foreground or recent async children.", "", "New runs appear here automatically while this inspector remains open."];
|
|
311
406
|
const lines = item.kind === "foreground-active"
|
|
312
407
|
? foregroundActiveDetail(item)
|
|
313
408
|
: item.kind === "foreground-recent"
|
|
314
|
-
? foregroundRecentDetail(item)
|
|
409
|
+
? foregroundRecentDetail(item, state)
|
|
315
410
|
: asyncDetail(item);
|
|
316
411
|
if (error) lines.unshift(`Fleet scan warning: ${error}`, "");
|
|
317
412
|
return lines;
|
|
@@ -497,6 +592,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
497
592
|
private readonly state: SubagentState;
|
|
498
593
|
private readonly done: (result: undefined) => void;
|
|
499
594
|
private readonly options: FleetViewOptions;
|
|
595
|
+
private readonly keybindings: ResolvedFleetKeybindings;
|
|
500
596
|
|
|
501
597
|
constructor(
|
|
502
598
|
tui: FleetTui,
|
|
@@ -511,6 +607,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
511
607
|
this.state = state;
|
|
512
608
|
this.done = done;
|
|
513
609
|
this.options = options;
|
|
610
|
+
this.keybindings = resolveFleetKeybindings(options.fleetKeybindings);
|
|
514
611
|
this.selectedKey = options.initialKey;
|
|
515
612
|
this.refresh();
|
|
516
613
|
this.timer = setInterval(() => {
|
|
@@ -552,6 +649,19 @@ export class SubagentFleetComponent implements Component {
|
|
|
552
649
|
return { item };
|
|
553
650
|
}
|
|
554
651
|
|
|
652
|
+
private selectedHerdrInspectAction(): { runId: string; asyncDir: string; index?: number } | { reason: string } {
|
|
653
|
+
const item = this.snapshot.items[this.selected];
|
|
654
|
+
if (!item) return { reason: "No child is selected." };
|
|
655
|
+
if (item.kind === "async") {
|
|
656
|
+
if (!isActionableAsyncState(item.run.state) || !isActionableAsyncState(item.state)) return { reason: `Selected child is ${item.state}; controls require a running or queued async child.` };
|
|
657
|
+
return { runId: item.runId, asyncDir: item.run.asyncDir, ...(item.index !== undefined ? { index: item.index } : {}) };
|
|
658
|
+
}
|
|
659
|
+
if (item.kind !== "foreground-active" || !item.control.parentWorkflowRunId) return { reason: "Fleet controls are available for current-session top-level async runs only." };
|
|
660
|
+
const parent = this.state.asyncJobs.get(item.control.parentWorkflowRunId) ?? this.state.fleetJobs?.get(item.control.parentWorkflowRunId);
|
|
661
|
+
if (!parent || !isActionableAsyncState(parent.status)) return { reason: "The parent workflow is no longer available for Herdr inspection." };
|
|
662
|
+
return { runId: parent.asyncId, asyncDir: parent.asyncDir };
|
|
663
|
+
}
|
|
664
|
+
|
|
555
665
|
private actionLines(): string[] {
|
|
556
666
|
const lines: string[] = [];
|
|
557
667
|
if (this.actionBusy) lines.push(this.theme.fg("accent", "Action pending..."));
|
|
@@ -657,25 +767,25 @@ export class SubagentFleetComponent implements Component {
|
|
|
657
767
|
}
|
|
658
768
|
return;
|
|
659
769
|
}
|
|
660
|
-
if (
|
|
770
|
+
if (matchesFleetAction(data, this.keybindings, "close")) {
|
|
661
771
|
this.done(undefined);
|
|
662
772
|
return;
|
|
663
773
|
}
|
|
664
|
-
if (
|
|
665
|
-
if (
|
|
666
|
-
if (
|
|
667
|
-
if (
|
|
668
|
-
if (
|
|
669
|
-
if (
|
|
670
|
-
if (
|
|
671
|
-
if (
|
|
672
|
-
if (data.
|
|
774
|
+
if (matchesFleetAction(data, this.keybindings, "scrollUp")) return this.scrollDetail(-1);
|
|
775
|
+
if (matchesFleetAction(data, this.keybindings, "scrollDown")) return this.scrollDetail(1);
|
|
776
|
+
if (matchesFleetAction(data, this.keybindings, "selectUp")) return this.moveSelection(-1);
|
|
777
|
+
if (matchesFleetAction(data, this.keybindings, "selectDown")) return this.moveSelection(1);
|
|
778
|
+
if (matchesFleetAction(data, this.keybindings, "selectFirst")) return this.moveSelection(-this.snapshot.items.length);
|
|
779
|
+
if (matchesFleetAction(data, this.keybindings, "selectLast")) return this.moveSelection(this.snapshot.items.length);
|
|
780
|
+
if (matchesFleetAction(data, this.keybindings, "pageUp")) return this.scrollDetail(-this.detailViewportHeight);
|
|
781
|
+
if (matchesFleetAction(data, this.keybindings, "pageDown")) return this.scrollDetail(this.detailViewportHeight);
|
|
782
|
+
if (matchesFleetAction(data, this.keybindings, "refresh")) {
|
|
673
783
|
this.transcriptCache = undefined;
|
|
674
784
|
this.refresh();
|
|
675
785
|
this.tui.requestRender();
|
|
676
786
|
return;
|
|
677
787
|
}
|
|
678
|
-
if (data
|
|
788
|
+
if (matchesFleetAction(data, this.keybindings, "steer")) {
|
|
679
789
|
const target = this.selectedAsyncAction();
|
|
680
790
|
if ("reason" in target || !this.options.actions) this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
|
|
681
791
|
else {
|
|
@@ -687,13 +797,13 @@ export class SubagentFleetComponent implements Component {
|
|
|
687
797
|
}
|
|
688
798
|
return;
|
|
689
799
|
}
|
|
690
|
-
if (data
|
|
691
|
-
const target = this.
|
|
800
|
+
if (matchesFleetAction(data, this.keybindings, "inspect")) {
|
|
801
|
+
const target = this.selectedHerdrInspectAction();
|
|
692
802
|
if ("reason" in target || !this.options.actions?.inspect) this.setActionNotice({ text: "reason" in target ? target.reason : "Herdr inspector controls are unavailable in this context.", isError: true });
|
|
693
|
-
else this.runAction(() => this.options.actions!.inspect!(
|
|
803
|
+
else this.runAction(() => this.options.actions!.inspect!(target));
|
|
694
804
|
return;
|
|
695
805
|
}
|
|
696
|
-
if (data
|
|
806
|
+
if (matchesFleetAction(data, this.keybindings, "stop")) {
|
|
697
807
|
const target = this.selectedAsyncAction();
|
|
698
808
|
if ("reason" in target || !this.options.actions) this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
|
|
699
809
|
else {
|
|
@@ -705,7 +815,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
705
815
|
}
|
|
706
816
|
return;
|
|
707
817
|
}
|
|
708
|
-
if (
|
|
818
|
+
if (matchesFleetAction(data, this.keybindings, "toggleTools")) {
|
|
709
819
|
this.expandedTools = !this.expandedTools;
|
|
710
820
|
this.transcriptCache = undefined;
|
|
711
821
|
this.tui.requestRender();
|
|
@@ -766,7 +876,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
766
876
|
}
|
|
767
877
|
}
|
|
768
878
|
|
|
769
|
-
const raw = detailLines(selected, this.snapshot.error);
|
|
879
|
+
const raw = detailLines(selected, this.snapshot.error, this.state);
|
|
770
880
|
if (transcriptWarning) raw.unshift(`Transcript preview warning: ${transcriptWarning}`, "");
|
|
771
881
|
const lines: string[] = [];
|
|
772
882
|
for (const line of raw) {
|
|
@@ -823,7 +933,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
823
933
|
}
|
|
824
934
|
lines.push(this.theme.fg("border", `├${"─".repeat(rosterWidth)}┴${"─".repeat(detailWidth)}┤`));
|
|
825
935
|
const position = this.snapshot.items.length ? `${this.selected + 1}/${this.snapshot.items.length}` : "0/0";
|
|
826
|
-
const footer = `
|
|
936
|
+
const footer = ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} agent · ${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}`;
|
|
827
937
|
lines.push(this.theme.fg("border", "│") + fit(this.theme.fg("dim", footer), innerWidth) + this.theme.fg("border", "│"));
|
|
828
938
|
lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
|
|
829
939
|
return lines.map((line) => truncateToWidth(line, width));
|
package/src/tui/render.ts
CHANGED
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
type NestedStepSummary,
|
|
17
17
|
type WorkflowNodeStatus,
|
|
18
18
|
MAX_WIDGET_JOBS,
|
|
19
|
+
POLL_INTERVAL_MS,
|
|
19
20
|
WIDGET_KEY,
|
|
20
21
|
} from "../shared/types.ts";
|
|
22
|
+
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
21
23
|
import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
|
|
22
24
|
import { getDisplayItems, getSingleResultOutput } from "../shared/utils.ts";
|
|
23
25
|
import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
|
|
@@ -202,12 +204,130 @@ function getToolCallLines(
|
|
|
202
204
|
return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
|
|
203
205
|
}
|
|
204
206
|
|
|
207
|
+
const ansiEscapePattern = /\x1b\[[0-9;]*m/g;
|
|
208
|
+
const noisyStatusPatterns = [
|
|
209
|
+
/^(?:i|we)\s+(?:will|need|can|should|am|are)\b/i,
|
|
210
|
+
/^i(?:'m|’m| am)\b/i,
|
|
211
|
+
/\bso i (?:will|need|can)\b/i,
|
|
212
|
+
/^(?:checking|fetching|reading|inspecting|verifying|collecting|confirming|polling)\b/i,
|
|
213
|
+
/^(?:async\s+subagent\s+)?[\w.-]+\s*·\s*(?:step|agent)\s+\d+\/\d+\s*·/i,
|
|
214
|
+
/^(?:Step|Agent)\s+\d+\/\d+:\s+[\w.-]+\s*·\s*(?:running|queued|pending|complete|completed)\b/i,
|
|
215
|
+
/^Press\s+\S+\s+for\s+live\s+detail$/i,
|
|
216
|
+
/^output:\s+.+\/async-subagent-runs\//i,
|
|
217
|
+
];
|
|
218
|
+
const liveOutputWordSignalPattern = /\b(?:access denied|denied|error|exception|fail(?:ed|ure)?|fatal|panic|rejected|timeout|timed out|unable|warning)\b/i;
|
|
219
|
+
const liveOutputCodeSignalPattern = /\bE[A-Z0-9_]{2,}\b/;
|
|
220
|
+
|
|
221
|
+
function oneLine(text: string): string {
|
|
222
|
+
return text.replace(ansiEscapePattern, "").replace(/\s+/g, " ").trim();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasLiveOutputSignal(line: string): boolean {
|
|
226
|
+
const clean = oneLine(line);
|
|
227
|
+
return liveOutputWordSignalPattern.test(clean) || liveOutputCodeSignalPattern.test(clean);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isNoisyStatusLine(line: string): boolean {
|
|
231
|
+
const clean = oneLine(line);
|
|
232
|
+
return clean.length > 0
|
|
233
|
+
&& clean.length <= 240
|
|
234
|
+
&& !hasLiveOutputSignal(clean)
|
|
235
|
+
&& noisyStatusPatterns.some((pattern) => pattern.test(clean));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function latestActivityText(line: string): string {
|
|
239
|
+
return oneLine(line)
|
|
240
|
+
.replace(/^i (?:will|can|need to|am going to)\s+/i, "")
|
|
241
|
+
.replace(/^i(?:'m|’m| am)\s+/i, "");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function progressUpdateSummary(lines: string[]): string {
|
|
245
|
+
const counts = new Map<string, number>();
|
|
246
|
+
for (const line of lines) counts.set(line.toLowerCase(), (counts.get(line.toLowerCase()) ?? 0) + 1);
|
|
247
|
+
const exactRepeatCount = Math.max(...counts.values());
|
|
248
|
+
const latest = latestActivityText(lines[lines.length - 1]!);
|
|
249
|
+
const repeat = exactRepeatCount > 1 ? ` · repeated ${exactRepeatCount}×` : "";
|
|
250
|
+
return `↻ ${lines.length} progress updates${repeat} · latest: ${latest}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function compactRecentOutputLines(recentOutput: string[] | undefined): string[] {
|
|
254
|
+
const lines: string[] = [];
|
|
255
|
+
const noisyLines: string[] = [];
|
|
256
|
+
const otherLines: string[] = [];
|
|
257
|
+
for (const rawLine of recentOutput ?? []) {
|
|
258
|
+
const line = oneLine(rawLine);
|
|
259
|
+
if (!line || line === "(running...)") continue;
|
|
260
|
+
lines.push(line);
|
|
261
|
+
(isNoisyStatusLine(line) ? noisyLines : otherLines).push(line);
|
|
262
|
+
}
|
|
263
|
+
if (noisyLines.length >= 4 && !otherLines.some(hasLiveOutputSignal)) {
|
|
264
|
+
if (otherLines.length === 0) {
|
|
265
|
+
return [
|
|
266
|
+
progressUpdateSummary(noisyLines),
|
|
267
|
+
"pattern: repeated short status lines",
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
const visibleTail = otherLines.slice(-3);
|
|
271
|
+
const hiddenSignals = otherLines.slice(0, -3).filter(hasLiveOutputSignal);
|
|
272
|
+
return [
|
|
273
|
+
progressUpdateSummary(noisyLines),
|
|
274
|
+
...(hiddenSignals.length > 0 ? [`… ${hiddenSignals.length} older signal ${hiddenSignals.length === 1 ? "line" : "lines"}: ${hiddenSignals.at(-1)}`] : []),
|
|
275
|
+
...visibleTail,
|
|
276
|
+
].slice(0, 5);
|
|
277
|
+
}
|
|
278
|
+
if (lines.length <= 5) return lines;
|
|
279
|
+
|
|
280
|
+
const tail = lines.slice(-5);
|
|
281
|
+
const hiddenSignals = lines.slice(0, -5).filter(hasLiveOutputSignal);
|
|
282
|
+
if (hiddenSignals.length === 0) return tail;
|
|
283
|
+
return [
|
|
284
|
+
`… ${hiddenSignals.length} older signal ${hiddenSignals.length === 1 ? "line" : "lines"}: ${hiddenSignals.at(-1)}`,
|
|
285
|
+
...lines.slice(-4),
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function compactWorkflowError(error: string): string {
|
|
290
|
+
const outputMatch = error.match(/(?:^|\n)Output:\s*([\s\S]+)/);
|
|
291
|
+
if (!outputMatch) return oneLine(error);
|
|
292
|
+
const prefix = oneLine(error.slice(0, outputMatch.index)).replace(/:$/, "") || "Failed";
|
|
293
|
+
const outputLines = outputMatch[1]!.split(/\r?\n/).map(oneLine).filter(Boolean);
|
|
294
|
+
const allOutputLinesAreNoisy = outputLines.length > 0 && outputLines.every(isNoisyStatusLine);
|
|
295
|
+
const latest = outputLines.at(-1);
|
|
296
|
+
return allOutputLinesAreNoisy && latest
|
|
297
|
+
? `${prefix} · latest: ${latestActivityText(latest)}`
|
|
298
|
+
: `${prefix} · ${oneLine(outputMatch[1] ?? "")}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const WORKFLOW_LIVE_ROW_LIMIT = 8;
|
|
302
|
+
|
|
303
|
+
function visibleWorkflowRows(rows: WorkflowChatProgressRow[]): { rows: WorkflowChatProgressRow[]; hiddenRows: number } {
|
|
304
|
+
if (rows.length <= WORKFLOW_LIVE_ROW_LIMIT) return { rows, hiddenRows: 0 };
|
|
305
|
+
const selected = new Set<string>();
|
|
306
|
+
const add = (row: WorkflowChatProgressRow): void => {
|
|
307
|
+
if (selected.size >= WORKFLOW_LIVE_ROW_LIMIT || selected.has(row.key)) return;
|
|
308
|
+
selected.add(row.key);
|
|
309
|
+
};
|
|
310
|
+
for (const row of [...rows].reverse()) {
|
|
311
|
+
if (row.state === "failed") add(row);
|
|
312
|
+
}
|
|
313
|
+
for (const row of [...rows].reverse()) add(row);
|
|
314
|
+
return {
|
|
315
|
+
rows: rows.filter((row) => selected.has(row.key)),
|
|
316
|
+
hiddenRows: rows.length - selected.size,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
205
319
|
|
|
206
320
|
function snapshotNowForProgress(progress: Pick<AgentProgress, "currentToolStartedAt" | "durationMs" | "lastActivityAt">): number | undefined {
|
|
207
321
|
if (progress.currentToolStartedAt !== undefined && progress.durationMs !== undefined) return progress.currentToolStartedAt + progress.durationMs;
|
|
208
322
|
return progress.lastActivityAt;
|
|
209
323
|
}
|
|
210
324
|
|
|
325
|
+
function renderToolArgsPreview(value: string, maxLength: number, expanded: boolean): string {
|
|
326
|
+
const normalized = sanitizeDisplayText(value);
|
|
327
|
+
if (expanded || normalized.length <= maxLength) return normalized;
|
|
328
|
+
return `${truncateDisplayText(normalized, maxLength)}...`;
|
|
329
|
+
}
|
|
330
|
+
|
|
211
331
|
function formatCurrentToolLine(
|
|
212
332
|
progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">,
|
|
213
333
|
availableWidth: number,
|
|
@@ -217,9 +337,7 @@ function formatCurrentToolLine(
|
|
|
217
337
|
if (!progress.currentTool) return undefined;
|
|
218
338
|
const maxToolArgsLen = Math.max(50, availableWidth - 20);
|
|
219
339
|
const toolArgsPreview = progress.currentToolArgs
|
|
220
|
-
? (
|
|
221
|
-
? progress.currentToolArgs
|
|
222
|
-
: `${progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
|
|
340
|
+
? renderToolArgsPreview(progress.currentToolArgs, maxToolArgsLen, expanded)
|
|
223
341
|
: "";
|
|
224
342
|
const durationSuffix = progress.currentToolStartedAt !== undefined && snapshotNow !== undefined
|
|
225
343
|
? ` | ${formatDuration(Math.max(0, snapshotNow - progress.currentToolStartedAt))}`
|
|
@@ -510,7 +628,7 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], s
|
|
|
510
628
|
}
|
|
511
629
|
|
|
512
630
|
|
|
513
|
-
function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
|
|
631
|
+
function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), frame?: number): string[] {
|
|
514
632
|
if (!job.steps?.length) return [];
|
|
515
633
|
const total = job.chainStepCount ?? job.steps.length;
|
|
516
634
|
const lines: string[] = [];
|
|
@@ -518,7 +636,7 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
|
|
|
518
636
|
const steps = job.steps.slice(span.start, span.start + span.count);
|
|
519
637
|
if (span.isParallel) {
|
|
520
638
|
const status = aggregateStepStatus(steps);
|
|
521
|
-
lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps))} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
|
|
639
|
+
lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps), frame)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
|
|
522
640
|
continue;
|
|
523
641
|
}
|
|
524
642
|
const step = steps[0];
|
|
@@ -526,15 +644,15 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
|
|
|
526
644
|
lines.push(` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`);
|
|
527
645
|
continue;
|
|
528
646
|
}
|
|
529
|
-
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width));
|
|
647
|
+
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width, frame));
|
|
530
648
|
}
|
|
531
649
|
return lines;
|
|
532
650
|
}
|
|
533
651
|
|
|
534
|
-
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
|
|
652
|
+
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), frame?: number): string[] {
|
|
535
653
|
if (!job.steps?.length) return [];
|
|
536
654
|
if (job.mode !== "parallel" && job.mode !== "chain") return [];
|
|
537
|
-
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
|
|
655
|
+
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
|
|
538
656
|
const total = job.stepsTotal ?? job.steps.length;
|
|
539
657
|
const lines: string[] = [];
|
|
540
658
|
for (const [index, step] of job.steps.entries()) {
|
|
@@ -542,7 +660,7 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
|
|
|
542
660
|
const activity = widgetStepActivity(step, job.updatedAt);
|
|
543
661
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
544
662
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
545
|
-
lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
|
|
663
|
+
lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
|
|
546
664
|
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
|
|
547
665
|
}
|
|
548
666
|
return lines;
|
|
@@ -1035,10 +1153,10 @@ function foregroundStyleWidgetStepLines(
|
|
|
1035
1153
|
if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
|
|
1036
1154
|
for (const tool of step.recentTools?.slice(-3) ?? []) {
|
|
1037
1155
|
const maxArgsLen = Math.max(40, width - 30);
|
|
1038
|
-
const argsPreview = tool.args
|
|
1156
|
+
const argsPreview = renderToolArgsPreview(tool.args, maxArgsLen, expanded);
|
|
1039
1157
|
lines.push(` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
|
|
1040
1158
|
}
|
|
1041
|
-
for (const line of step.recentOutput
|
|
1159
|
+
for (const line of compactRecentOutputLines(step.recentOutput)) {
|
|
1042
1160
|
lines.push(` ${theme.fg("dim", line)}`);
|
|
1043
1161
|
}
|
|
1044
1162
|
}
|
|
@@ -1051,7 +1169,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
1051
1169
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1052
1170
|
...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
|
|
1053
1171
|
];
|
|
1054
|
-
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
|
|
1172
|
+
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
|
|
1055
1173
|
const total = job.stepsTotal ?? job.steps.length;
|
|
1056
1174
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
1057
1175
|
const lines: string[] = [];
|
|
@@ -1299,10 +1417,10 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
|
|
|
1299
1417
|
return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
|
|
1300
1418
|
}
|
|
1301
1419
|
|
|
1302
|
-
function fitAdaptiveWidgetLines(jobs: AsyncJobState[],
|
|
1420
|
+
function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[], theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
|
|
1303
1421
|
if (expanded) {
|
|
1304
1422
|
resetWidgetLayoutSession();
|
|
1305
|
-
return fitWidgetLineBudget(
|
|
1423
|
+
return fitWidgetLineBudget(buildLines(), theme, width, true);
|
|
1306
1424
|
}
|
|
1307
1425
|
|
|
1308
1426
|
const hasMatchingSession = widgetSessionMatches(expanded);
|
|
@@ -1320,6 +1438,7 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
|
|
|
1320
1438
|
return rendered.lines;
|
|
1321
1439
|
}
|
|
1322
1440
|
|
|
1441
|
+
const lines = buildLines();
|
|
1323
1442
|
if (lines.length <= availableRows) {
|
|
1324
1443
|
widgetLayoutSession = { expanded, rows, columns, tier: "full", visibleJobKeys: [] };
|
|
1325
1444
|
return fitWidgetLineBudget(lines, theme, width, false);
|
|
@@ -1341,12 +1460,13 @@ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui:
|
|
|
1341
1460
|
const container = new Container();
|
|
1342
1461
|
container.render = (renderWidth: number): string[] => {
|
|
1343
1462
|
const width = Math.max(0, renderWidth - 2);
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1463
|
+
const frame = Math.floor(Date.now() / POLL_INTERVAL_MS);
|
|
1464
|
+
const buildLines = (): string[] => expanded
|
|
1465
|
+
? buildWidgetLines(jobs, theme, width, true, frame)
|
|
1346
1466
|
: jobs.length === 1
|
|
1347
|
-
? compactSingleWidgetLines(jobs[0]!, theme, width)
|
|
1348
|
-
: buildWidgetLines(jobs, theme, width, false);
|
|
1349
|
-
return fitAdaptiveWidgetLines(jobs,
|
|
1467
|
+
? compactSingleWidgetLines(jobs[0]!, theme, width, frame)
|
|
1468
|
+
: buildWidgetLines(jobs, theme, width, false, frame);
|
|
1469
|
+
return fitAdaptiveWidgetLines(jobs, buildLines, theme, width, expanded, frame).map((line) => paddedWidgetLine(line, renderWidth));
|
|
1350
1470
|
};
|
|
1351
1471
|
return container;
|
|
1352
1472
|
};
|
|
@@ -1376,7 +1496,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1376
1496
|
items.push([
|
|
1377
1497
|
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1378
1498
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1379
|
-
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1499
|
+
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
1380
1500
|
]);
|
|
1381
1501
|
slots--;
|
|
1382
1502
|
}
|
|
@@ -1393,7 +1513,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1393
1513
|
items.push([
|
|
1394
1514
|
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1395
1515
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1396
|
-
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1516
|
+
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
1397
1517
|
]);
|
|
1398
1518
|
slots--;
|
|
1399
1519
|
}
|
|
@@ -1513,12 +1633,14 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
|
|
|
1513
1633
|
c.addChild(new Text(truncLine(theme.fg("dim", " ◦ waiting for workflow child launches"), width), 0, 0));
|
|
1514
1634
|
return c;
|
|
1515
1635
|
}
|
|
1516
|
-
|
|
1636
|
+
const visible = visibleWorkflowRows(rows);
|
|
1637
|
+
if (visible.hiddenRows > 0) c.addChild(new Text(truncLine(theme.fg("dim", ` … ${visible.hiddenRows} older workflow rows hidden`), width), 0, 0));
|
|
1638
|
+
for (const row of visible.rows) {
|
|
1517
1639
|
const status = workflowRowStateLabel(row, theme);
|
|
1518
|
-
const label = row.label && row.label !== row.key ? ` ${row.label}` : "";
|
|
1640
|
+
const label = row.label && row.label !== row.key ? ` ${oneLine(row.label)}` : "";
|
|
1519
1641
|
const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
|
|
1520
1642
|
const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
|
|
1521
|
-
const error = row.error ? ` ${theme.fg("error", `· ${row.error}`)}` : "";
|
|
1643
|
+
const error = row.error ? ` ${theme.fg("error", `· ${compactWorkflowError(row.error)}`)}` : "";
|
|
1522
1644
|
c.addChild(new Text(truncLine(` ${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
|
|
1523
1645
|
}
|
|
1524
1646
|
if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", ` Emits ${workflow.emits.length}`), width), 0, 0));
|
|
@@ -1756,13 +1878,11 @@ export function renderSubagentResult(
|
|
|
1756
1878
|
if (r.progress.recentTools?.length) {
|
|
1757
1879
|
for (const t of r.progress.recentTools.slice(-3)) {
|
|
1758
1880
|
const maxArgsLen = Math.max(40, w - 24);
|
|
1759
|
-
const argsPreview =
|
|
1760
|
-
? t.args
|
|
1761
|
-
: `${t.args.slice(0, maxArgsLen)}...`;
|
|
1881
|
+
const argsPreview = renderToolArgsPreview(t.args, maxArgsLen, expanded);
|
|
1762
1882
|
c.addChild(new Text(fit(theme.fg("dim", `${t.tool}: ${argsPreview}`)), 0, 0));
|
|
1763
1883
|
}
|
|
1764
1884
|
}
|
|
1765
|
-
for (const line of (r.progress.recentOutput
|
|
1885
|
+
for (const line of compactRecentOutputLines(r.progress.recentOutput)) {
|
|
1766
1886
|
c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
|
|
1767
1887
|
}
|
|
1768
1888
|
if (toolLine || liveStatusLine || r.progress.recentTools?.length || r.progress.recentOutput?.length || r.artifactPaths) {
|
|
@@ -1989,14 +2109,11 @@ export function renderSubagentResult(
|
|
|
1989
2109
|
if (rProg.recentTools?.length) {
|
|
1990
2110
|
for (const t of rProg.recentTools.slice(-3)) {
|
|
1991
2111
|
const maxArgsLen = Math.max(40, w - 30);
|
|
1992
|
-
const argsPreview =
|
|
1993
|
-
? t.args
|
|
1994
|
-
: `${t.args.slice(0, maxArgsLen)}...`;
|
|
2112
|
+
const argsPreview = renderToolArgsPreview(t.args, maxArgsLen, expanded);
|
|
1995
2113
|
c.addChild(new Text(fit(theme.fg("dim", ` ${t.tool}: ${argsPreview}`)), 0, 0));
|
|
1996
2114
|
}
|
|
1997
2115
|
}
|
|
1998
|
-
const
|
|
1999
|
-
for (const line of recentLines) {
|
|
2116
|
+
for (const line of compactRecentOutputLines(rProg.recentOutput)) {
|
|
2000
2117
|
c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
|
|
2001
2118
|
}
|
|
2002
2119
|
}
|
|
@@ -2,10 +2,11 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { PROJECT_SUBAGENTS_RELATIVE_DIR } from "../shared/artifacts.ts";
|
|
5
6
|
|
|
6
|
-
const IGNORED_CHANGE_PREFIXES = [
|
|
7
|
-
const IGNORED_CHANGE_PATHS = new Set([
|
|
8
|
-
const IGNORED_CHANGE_SEGMENTS = new Set([".git", "
|
|
7
|
+
const IGNORED_CHANGE_PREFIXES = [`${PROJECT_SUBAGENTS_RELATIVE_DIR}/`, "tmp/", "node_modules/"];
|
|
8
|
+
const IGNORED_CHANGE_PATHS = new Set([PROJECT_SUBAGENTS_RELATIVE_DIR, "tmp", "node_modules"]);
|
|
9
|
+
const IGNORED_CHANGE_SEGMENTS = new Set([".git", "node_modules"]);
|
|
9
10
|
|
|
10
11
|
const DEFAULT_MAX_HASH_FILE_BYTES = 64 * 1024 * 1024;
|
|
11
12
|
const DEFAULT_MAX_HASH_TOTAL_BYTES = 64 * 1024 * 1024;
|