pi-subagents 0.41.0 → 0.42.1
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 +28 -0
- package/README.md +54 -1600
- package/inspector-runner.mjs +11 -0
- package/package.json +3 -3
- package/skills/pi-subagents/SKILL.md +5 -3
- package/skills/pi-subagents/references/constraints-and-recipes.md +5 -6
- package/skills/pi-subagents/references/execution-controls.md +6 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +5 -3
- package/src/extension/schemas.ts +4 -4
- package/src/extension/tool-description.ts +4 -4
- package/src/inspectors/herdr/actions.ts +2 -2
- package/src/missions/actions.ts +7 -5
- package/src/runs/background/async-execution.ts +12 -1
- package/src/runs/background/async-job-tracker.ts +17 -2
- package/src/runs/foreground/chain-clarify.ts +13 -16
- package/src/runs/foreground/execution.ts +19 -7
- package/src/runs/foreground/subagent-executor.ts +20 -9
- package/src/runs/shared/acceptance.ts +2 -2
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -0
- package/src/runs/shared/pi-args.ts +465 -116
- package/src/shared/artifacts.ts +125 -0
- package/src/shared/types.ts +1 -1
- package/src/slash/slash-commands.ts +3 -3
- package/src/tui/fleet.ts +1 -1
- package/src/tui/render.ts +78 -43
- package/src/types/pi-runtime-compat.d.ts +0 -6
- package/src/workflows/chat-progress.ts +2 -2
- package/src/workflows/scripted-workflow.ts +6 -4
package/src/shared/artifacts.ts
CHANGED
|
@@ -5,6 +5,131 @@ import { getAgentDir } from "./utils.ts";
|
|
|
5
5
|
const CLEANUP_MARKER_FILE = ".last-cleanup";
|
|
6
6
|
const PROJECT_ARTIFACT_ROOT = ".pi-subagents";
|
|
7
7
|
|
|
8
|
+
const PROJECT_ARTIFACT_PATHS = [
|
|
9
|
+
`${PROJECT_ARTIFACT_ROOT}/artifacts/output.md`,
|
|
10
|
+
`${PROJECT_ARTIFACT_ROOT}/artifacts/run_worker_input.md`,
|
|
11
|
+
`${PROJECT_ARTIFACT_ROOT}/artifacts/run_worker_output.md`,
|
|
12
|
+
`${PROJECT_ARTIFACT_ROOT}/artifacts/run_worker.jsonl`,
|
|
13
|
+
`${PROJECT_ARTIFACT_ROOT}/artifacts/run_worker_transcript.jsonl`,
|
|
14
|
+
`${PROJECT_ARTIFACT_ROOT}/artifacts/run_worker_meta.json`,
|
|
15
|
+
`${PROJECT_ARTIFACT_ROOT}/artifacts/progress/run/progress.md`,
|
|
16
|
+
`${PROJECT_ARTIFACT_ROOT}/artifacts/outputs/output.md`,
|
|
17
|
+
`${PROJECT_ARTIFACT_ROOT}/artifacts/outputs/run/output.md`,
|
|
18
|
+
`${PROJECT_ARTIFACT_ROOT}/chain-runs/run.json`,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function globMatchesPath(pattern: string, filePath: string): boolean {
|
|
22
|
+
let expression = "^";
|
|
23
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
24
|
+
const character = pattern[index];
|
|
25
|
+
if (character === undefined) break;
|
|
26
|
+
if (character === "*" && pattern[index + 1] === "*") {
|
|
27
|
+
index += 1;
|
|
28
|
+
if (pattern[index + 1] === "/") {
|
|
29
|
+
index += 1;
|
|
30
|
+
expression += "(?:.*/)?";
|
|
31
|
+
} else {
|
|
32
|
+
expression += ".*";
|
|
33
|
+
}
|
|
34
|
+
} else if (character === "*") {
|
|
35
|
+
expression += "[^/]*";
|
|
36
|
+
} else if (character === "?") {
|
|
37
|
+
expression += "[^/]";
|
|
38
|
+
} else if (character === "[") {
|
|
39
|
+
const end = pattern.indexOf("]", index + 1);
|
|
40
|
+
if (end === -1) expression += "\\[";
|
|
41
|
+
else {
|
|
42
|
+
expression += pattern.slice(index, end + 1);
|
|
43
|
+
index = end;
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
expression += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return new RegExp(`${expression}$`).test(filePath);
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizePattern(pattern: string): string {
|
|
57
|
+
return pattern.replace(/^\.\//, "").replace(/^\//, "").replace(/\/+$/, "");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function patternMatchesArtifactPath(pattern: string, artifactPath: string): boolean {
|
|
61
|
+
const normalized = normalizePattern(pattern);
|
|
62
|
+
return normalized === PROJECT_ARTIFACT_ROOT
|
|
63
|
+
|| normalized === "*"
|
|
64
|
+
|| artifactPath.startsWith(`${normalized}/`)
|
|
65
|
+
|| globMatchesPath(normalized, artifactPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function patternMatchesProjectArtifacts(pattern: string): boolean {
|
|
69
|
+
return PROJECT_ARTIFACT_PATHS.some((artifactPath) => patternMatchesArtifactPath(pattern, artifactPath));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ignoreFileExcludesProjectArtifacts(filePath: string): boolean {
|
|
73
|
+
if (!fs.existsSync(filePath)) return false;
|
|
74
|
+
try {
|
|
75
|
+
const excluded = new Set<string>();
|
|
76
|
+
for (const rawLine of fs.readFileSync(filePath, "utf-8").split(/\r?\n/)) {
|
|
77
|
+
const line = rawLine.trim();
|
|
78
|
+
if (!line || line.startsWith("#")) continue;
|
|
79
|
+
const negated = line.startsWith("!");
|
|
80
|
+
const pattern = negated ? line.slice(1) : line;
|
|
81
|
+
for (const artifactPath of PROJECT_ARTIFACT_PATHS) {
|
|
82
|
+
if (!patternMatchesArtifactPath(pattern, artifactPath)) continue;
|
|
83
|
+
if (negated) excluded.delete(artifactPath);
|
|
84
|
+
else excluded.add(artifactPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return excluded.size === PROJECT_ARTIFACT_PATHS.length;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function filesIncludeProjectArtifacts(files: unknown): boolean | undefined {
|
|
94
|
+
if (!Array.isArray(files)) return undefined;
|
|
95
|
+
const included = new Set<string>();
|
|
96
|
+
for (const entry of files) {
|
|
97
|
+
if (typeof entry !== "string") continue;
|
|
98
|
+
const negated = entry.startsWith("!");
|
|
99
|
+
const pattern = negated ? entry.slice(1) : entry;
|
|
100
|
+
for (const artifactPath of PROJECT_ARTIFACT_PATHS) {
|
|
101
|
+
if (!patternMatchesArtifactPath(pattern, artifactPath)) continue;
|
|
102
|
+
if (negated) included.delete(artifactPath);
|
|
103
|
+
else included.add(artifactPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return included.size > 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Returns a package-publishing warning when project artifacts can enter npm packages. */
|
|
110
|
+
export function getProjectArtifactPackagingWarning(cwd: string): string | undefined {
|
|
111
|
+
const packagePath = path.join(cwd, "package.json");
|
|
112
|
+
if (!fs.existsSync(packagePath)) return undefined;
|
|
113
|
+
|
|
114
|
+
let packageJson: Record<string, unknown>;
|
|
115
|
+
try {
|
|
116
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
117
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
118
|
+
packageJson = parsed as Record<string, unknown>;
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const filesIncludeArtifacts = filesIncludeProjectArtifacts(packageJson.files);
|
|
124
|
+
if (filesIncludeArtifacts === false) return undefined;
|
|
125
|
+
|
|
126
|
+
const npmIgnorePath = path.join(cwd, ".npmignore");
|
|
127
|
+
const ignorePath = fs.existsSync(npmIgnorePath) ? npmIgnorePath : path.join(cwd, ".gitignore");
|
|
128
|
+
if (filesIncludeArtifacts === undefined && ignoreFileExcludesProjectArtifacts(ignorePath)) return undefined;
|
|
129
|
+
|
|
130
|
+
return "Project-scoped subagent artifacts can be included when this package is published. Add '.pi-subagents/' to .npmignore, restrict package.json files, or set artifactDir to 'session' or 'temp'.";
|
|
131
|
+
}
|
|
132
|
+
|
|
8
133
|
export function getProjectSubagentsDir(cwd: string): string {
|
|
9
134
|
return path.join(cwd, PROJECT_ARTIFACT_ROOT);
|
|
10
135
|
}
|
package/src/shared/types.ts
CHANGED
|
@@ -1007,7 +1007,7 @@ export interface Details {
|
|
|
1007
1007
|
console: Array<{ level: "log" | "info" | "warn" | "error"; text: string }>;
|
|
1008
1008
|
};
|
|
1009
1009
|
chatProgress?: {
|
|
1010
|
-
mode: "off" | "
|
|
1010
|
+
mode: "off" | "live-card";
|
|
1011
1011
|
repoRelation: "same" | "other";
|
|
1012
1012
|
repoLabel?: string;
|
|
1013
1013
|
};
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { Key, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
5
|
+
import { Key, matchesKey, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
6
6
|
import { BUILTIN_AGENT_NAMES, discoverAgents } from "../agents/agents.ts";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,
|
|
@@ -281,7 +281,7 @@ class SubagentsStopSelector implements Component {
|
|
|
281
281
|
invalidate(): void {}
|
|
282
282
|
|
|
283
283
|
render(width: number): string[] {
|
|
284
|
-
const contentWidth = Math.max(
|
|
284
|
+
const contentWidth = Math.max(0, Math.min(this.width, Math.floor(width)));
|
|
285
285
|
const lines = [this.theme.bold("Stop subagent run"), this.theme.fg("dim", "Select a current-session async run to stop, or a scheduled run to cancel."), ""];
|
|
286
286
|
const maxRows = 10;
|
|
287
287
|
const start = Math.max(0, Math.min(this.selected - maxRows + 1, Math.max(0, this.targets.length - maxRows)));
|
|
@@ -305,7 +305,7 @@ class SubagentsStopSelector implements Component {
|
|
|
305
305
|
} else {
|
|
306
306
|
lines.push(this.theme.fg("dim", "↑↓/jk select · Enter confirm · Esc cancel"));
|
|
307
307
|
}
|
|
308
|
-
return lines;
|
|
308
|
+
return lines.map((line) => truncateToWidth(line, contentWidth));
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
|
package/src/tui/fleet.ts
CHANGED
|
@@ -780,7 +780,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
780
780
|
if (width < 36) return [truncateToWidth("Subagent fleet needs at least 36 columns. Esc closes.", width)];
|
|
781
781
|
const innerWidth = width - 2;
|
|
782
782
|
const rows = this.tui.terminal?.rows ?? 32;
|
|
783
|
-
this.bodyHeight = Math.max(2, Math.
|
|
783
|
+
this.bodyHeight = Math.max(2, Math.floor(rows * 0.85) - 6);
|
|
784
784
|
const rosterWidth = Math.max(22, Math.min(46, Math.floor((innerWidth - 1) * 0.38)));
|
|
785
785
|
const detailWidth = Math.max(1, innerWidth - rosterWidth - 1);
|
|
786
786
|
const roster = this.rosterLines(rosterWidth);
|
package/src/tui/render.ts
CHANGED
|
@@ -144,6 +144,24 @@ function runningGlyph(seed?: number): string {
|
|
|
144
144
|
return RUNNING_FRAMES[Math.abs(seed) % RUNNING_FRAMES.length]!;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
const WIDGET_ANIMATION_FRAME_MS = 500;
|
|
148
|
+
let widgetRequestRender: (() => void) | undefined;
|
|
149
|
+
|
|
150
|
+
function animatedSeed(seed: number | undefined, frame: number | undefined): number | undefined {
|
|
151
|
+
if (frame === undefined) return seed;
|
|
152
|
+
return (seed ?? 0) + frame;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function widgetAnimationFrame(): number {
|
|
156
|
+
return Math.floor(Date.now() / WIDGET_ANIMATION_FRAME_MS);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function requestWidgetRender(): boolean {
|
|
160
|
+
if (!widgetRequestRender) return false;
|
|
161
|
+
widgetRequestRender();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
147
165
|
function progressRunningSeed(progress: ProgressSeedSource | undefined): number | undefined {
|
|
148
166
|
if (!progress) return undefined;
|
|
149
167
|
return runningSeed(
|
|
@@ -461,8 +479,8 @@ function widgetJobsRunningSeed(jobs: AsyncJobState[]): number | undefined {
|
|
|
461
479
|
return seed;
|
|
462
480
|
}
|
|
463
481
|
|
|
464
|
-
function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
465
|
-
if (job.status === "running") return theme.fg("accent", runningGlyph(widgetJobRunningSeed(job)));
|
|
482
|
+
function widgetStatusGlyph(job: AsyncJobState, theme: Theme, frame?: number): string {
|
|
483
|
+
if (job.status === "running") return theme.fg("accent", runningGlyph(animatedSeed(widgetJobRunningSeed(job), frame)));
|
|
466
484
|
if (job.status === "queued") return theme.fg("muted", "◦");
|
|
467
485
|
if (job.status === "complete") return theme.fg("success", "✓");
|
|
468
486
|
if (job.status === "paused") return theme.fg("warning", "■");
|
|
@@ -470,8 +488,8 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
|
470
488
|
return theme.fg("error", "✗");
|
|
471
489
|
}
|
|
472
490
|
|
|
473
|
-
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number): string {
|
|
474
|
-
if (status === "running") return theme.fg("accent", runningGlyph(seed));
|
|
491
|
+
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number, frame?: number): string {
|
|
492
|
+
if (status === "running") return theme.fg("accent", runningGlyph(animatedSeed(seed, frame)));
|
|
475
493
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
476
494
|
if (status === "failed") return theme.fg("error", "✗");
|
|
477
495
|
if (status === "paused") return theme.fg("warning", "■");
|
|
@@ -1008,11 +1026,12 @@ function foregroundStyleWidgetStepLines(
|
|
|
1008
1026
|
total: number,
|
|
1009
1027
|
expanded: boolean,
|
|
1010
1028
|
width: number,
|
|
1029
|
+
frame?: number,
|
|
1011
1030
|
): string[] {
|
|
1012
1031
|
const status = widgetStepStatus(step.status, theme);
|
|
1013
1032
|
const stats = widgetStepStats(theme, step);
|
|
1014
1033
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
1015
|
-
const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1))} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
|
|
1034
|
+
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}` : ""}`];
|
|
1016
1035
|
const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
|
|
1017
1036
|
if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
|
|
1018
1037
|
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
|
|
@@ -1038,7 +1057,7 @@ function foregroundStyleWidgetStepLines(
|
|
|
1038
1057
|
return lines;
|
|
1039
1058
|
}
|
|
1040
1059
|
|
|
1041
|
-
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
|
|
1060
|
+
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number): string[] {
|
|
1042
1061
|
if (!job.steps?.length) return [
|
|
1043
1062
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1044
1063
|
...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
|
|
@@ -1048,7 +1067,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
1048
1067
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
1049
1068
|
const lines: string[] = [];
|
|
1050
1069
|
for (const [index, step] of job.steps.entries()) {
|
|
1051
|
-
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
|
|
1070
|
+
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, frame));
|
|
1052
1071
|
}
|
|
1053
1072
|
const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
1054
1073
|
const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
@@ -1058,20 +1077,20 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
1058
1077
|
return lines;
|
|
1059
1078
|
}
|
|
1060
1079
|
|
|
1061
|
-
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean): string[] {
|
|
1080
|
+
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
|
|
1062
1081
|
const stats = widgetStats(job, theme);
|
|
1063
1082
|
const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
|
|
1064
1083
|
const mode = widgetJobName(job);
|
|
1065
1084
|
const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
|
|
1066
1085
|
return [
|
|
1067
1086
|
`${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
|
|
1068
|
-
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, mode)}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1069
|
-
...foregroundStyleWidgetDetails(job, theme, expanded, width),
|
|
1087
|
+
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, mode)}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1088
|
+
...foregroundStyleWidgetDetails(job, theme, expanded, width, frame),
|
|
1070
1089
|
].map((line) => truncLine(line, width));
|
|
1071
1090
|
}
|
|
1072
1091
|
|
|
1073
|
-
function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number): string[] {
|
|
1074
|
-
const fullLines = buildSingleWidgetLines(job, theme, width, false);
|
|
1092
|
+
function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, frame?: number): string[] {
|
|
1093
|
+
const fullLines = buildSingleWidgetLines(job, theme, width, false, frame);
|
|
1075
1094
|
if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
|
|
1076
1095
|
|
|
1077
1096
|
const total = job.stepsTotal ?? job.steps.length;
|
|
@@ -1083,7 +1102,7 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
|
|
|
1083
1102
|
const stepStats = widgetStepStats(theme, step);
|
|
1084
1103
|
const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
|
|
1085
1104
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
1086
|
-
lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
|
|
1105
|
+
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}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
|
|
1087
1106
|
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
|
|
1088
1107
|
}
|
|
1089
1108
|
if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
|
|
@@ -1139,10 +1158,10 @@ function widgetHeaderCounts(jobs: AsyncJobState[]): { running: AsyncJobState[];
|
|
|
1139
1158
|
};
|
|
1140
1159
|
}
|
|
1141
1160
|
|
|
1142
|
-
function buildSingleLineWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number): string[] {
|
|
1161
|
+
function buildSingleLineWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number, frame?: number): string[] {
|
|
1143
1162
|
const counts = widgetHeaderCounts(jobs);
|
|
1144
1163
|
const hasActive = counts.running.length > 0 || counts.queued.length > 0;
|
|
1145
|
-
const glyph = counts.running.length > 0 ? runningGlyph(widgetJobsRunningSeed(counts.running)) : hasActive ? "●" : "○";
|
|
1164
|
+
const glyph = counts.running.length > 0 ? runningGlyph(animatedSeed(widgetJobsRunningSeed(counts.running), frame)) : hasActive ? "●" : "○";
|
|
1146
1165
|
const parts: string[] = [];
|
|
1147
1166
|
if (counts.running.length > 0) parts.push(`${counts.running.length}/${jobs.length} running`);
|
|
1148
1167
|
if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
|
|
@@ -1202,10 +1221,10 @@ function selectProgressiveJobKeys(jobs: AsyncJobState[], previousKeys: string[],
|
|
|
1202
1221
|
return selected;
|
|
1203
1222
|
}
|
|
1204
1223
|
|
|
1205
|
-
function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: number): string {
|
|
1224
|
+
function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: number, frame?: number): string {
|
|
1206
1225
|
const counts = widgetHeaderCounts(jobs);
|
|
1207
1226
|
const hasActive = counts.running.length > 0 || counts.queued.length > 0;
|
|
1208
|
-
const glyph = counts.running.length > 0 ? runningGlyph(widgetJobsRunningSeed(counts.running)) : hasActive ? "●" : "○";
|
|
1227
|
+
const glyph = counts.running.length > 0 ? runningGlyph(animatedSeed(widgetJobsRunningSeed(counts.running), frame)) : hasActive ? "●" : "○";
|
|
1209
1228
|
const parts: string[] = [];
|
|
1210
1229
|
if (counts.running.length > 0) parts.push(formatAgentRunningLabel(counts.running.length));
|
|
1211
1230
|
if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
|
|
@@ -1218,7 +1237,7 @@ function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: numbe
|
|
|
1218
1237
|
return truncLine(`${theme.fg(hasActive ? "accent" : "dim", glyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(", ") || `${jobs.length} total`)}`, width);
|
|
1219
1238
|
}
|
|
1220
1239
|
|
|
1221
|
-
function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number): string {
|
|
1240
|
+
function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number, frame?: number): string {
|
|
1222
1241
|
const stats = widgetStats(job, theme);
|
|
1223
1242
|
const activity = widgetActivity(job);
|
|
1224
1243
|
const status = job.status === "complete" ? "done" : job.status;
|
|
@@ -1228,7 +1247,7 @@ function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number): st
|
|
|
1228
1247
|
stats,
|
|
1229
1248
|
activity && activity.toLowerCase() !== status ? theme.fg("dim", activity) : "",
|
|
1230
1249
|
].filter(Boolean);
|
|
1231
|
-
return truncLine(` ${widgetStatusGlyph(job, theme)} ${parts.join(` ${theme.fg("dim", "·")} `)}`, width);
|
|
1250
|
+
return truncLine(` ${widgetStatusGlyph(job, theme, frame)} ${parts.join(` ${theme.fg("dim", "·")} `)}`, width);
|
|
1232
1251
|
}
|
|
1233
1252
|
|
|
1234
1253
|
function progressiveHiddenLine(hiddenJobs: AsyncJobState[], theme: Theme, width: number): string {
|
|
@@ -1241,9 +1260,9 @@ function progressiveHiddenLine(hiddenJobs: AsyncJobState[], theme: Theme, width:
|
|
|
1241
1260
|
return truncLine(theme.fg("dim", ` +${hiddenJobs.length} more${parts.length ? ` (${parts.join(", ")})` : ""}`), width);
|
|
1242
1261
|
}
|
|
1243
1262
|
|
|
1244
|
-
function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number, lockedRows: number, previousKeys: string[]): { lines: string[]; visibleJobKeys: string[] } {
|
|
1263
|
+
function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number, lockedRows: number, previousKeys: string[], frame?: number): { lines: string[]; visibleJobKeys: string[] } {
|
|
1245
1264
|
const rowCount = Math.max(1, lockedRows);
|
|
1246
|
-
if (rowCount === 1) return { lines: buildSingleLineWidgetLines(jobs, theme, width), visibleJobKeys: [] };
|
|
1265
|
+
if (rowCount === 1) return { lines: buildSingleLineWidgetLines(jobs, theme, width, frame), visibleJobKeys: [] };
|
|
1247
1266
|
|
|
1248
1267
|
const bodyRows = rowCount - 1;
|
|
1249
1268
|
let visibleJobKeys = selectProgressiveJobKeys(jobs, previousKeys, bodyRows);
|
|
@@ -1259,8 +1278,8 @@ function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width:
|
|
|
1259
1278
|
}
|
|
1260
1279
|
|
|
1261
1280
|
const lines = [
|
|
1262
|
-
progressiveHeaderLine(jobs, theme, width),
|
|
1263
|
-
...visibleJobs.map((job) => progressiveJobLine(job, theme, width)),
|
|
1281
|
+
progressiveHeaderLine(jobs, theme, width, frame),
|
|
1282
|
+
...visibleJobs.map((job) => progressiveJobLine(job, theme, width, frame)),
|
|
1264
1283
|
];
|
|
1265
1284
|
if (hiddenJobs.length > 0 && lines.length < rowCount) lines.push(progressiveHiddenLine(hiddenJobs, theme, width));
|
|
1266
1285
|
while (lines.length < rowCount) lines.push(" ");
|
|
@@ -1271,6 +1290,12 @@ function collapsedWidgetLineBudget(rows: number): number {
|
|
|
1271
1290
|
return Math.max(10, Math.min(14, Math.floor(rows * 0.35)));
|
|
1272
1291
|
}
|
|
1273
1292
|
|
|
1293
|
+
function paddedWidgetLine(line: string, width: number): string {
|
|
1294
|
+
if (width <= 2) return " ".repeat(Math.max(0, width));
|
|
1295
|
+
const text = ` ${truncLine(line, width - 2)} `;
|
|
1296
|
+
return `${text}${" ".repeat(Math.max(0, width - visibleWidth(text)))}`;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1274
1299
|
function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
|
|
1275
1300
|
const rows = process.stdout.rows || 30;
|
|
1276
1301
|
const budget = expanded
|
|
@@ -1285,7 +1310,7 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
|
|
|
1285
1310
|
return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
|
|
1286
1311
|
}
|
|
1287
1312
|
|
|
1288
|
-
function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
|
|
1313
|
+
function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
|
|
1289
1314
|
if (expanded) {
|
|
1290
1315
|
resetWidgetLayoutSession();
|
|
1291
1316
|
return fitWidgetLineBudget(lines, theme, width, true);
|
|
@@ -1297,11 +1322,11 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
|
|
|
1297
1322
|
const availableRows = estimateAvailableWidgetRows();
|
|
1298
1323
|
|
|
1299
1324
|
if (hasMatchingSession && widgetLayoutSession?.tier === "single-line") {
|
|
1300
|
-
return buildSingleLineWidgetLines(jobs, theme, width);
|
|
1325
|
+
return buildSingleLineWidgetLines(jobs, theme, width, frame);
|
|
1301
1326
|
}
|
|
1302
1327
|
|
|
1303
1328
|
if (hasMatchingSession && widgetLayoutSession?.tier === "progressive" && widgetLayoutSession.lockedRows !== undefined) {
|
|
1304
|
-
const rendered = buildProgressiveWidgetLines(jobs, theme, width, widgetLayoutSession.lockedRows, widgetLayoutSession.visibleJobKeys);
|
|
1329
|
+
const rendered = buildProgressiveWidgetLines(jobs, theme, width, widgetLayoutSession.lockedRows, widgetLayoutSession.visibleJobKeys, frame);
|
|
1305
1330
|
widgetLayoutSession.visibleJobKeys = rendered.visibleJobKeys;
|
|
1306
1331
|
return rendered.lines;
|
|
1307
1332
|
}
|
|
@@ -1313,39 +1338,48 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
|
|
|
1313
1338
|
|
|
1314
1339
|
if (availableRows <= 2) {
|
|
1315
1340
|
widgetLayoutSession = { expanded, rows, columns, tier: "single-line", visibleJobKeys: [] };
|
|
1316
|
-
return buildSingleLineWidgetLines(jobs, theme, width);
|
|
1341
|
+
return buildSingleLineWidgetLines(jobs, theme, width, frame);
|
|
1317
1342
|
}
|
|
1318
1343
|
|
|
1319
1344
|
const lockedRows = Math.min(availableRows, collapsedWidgetLineBudget(rows));
|
|
1320
|
-
const rendered = buildProgressiveWidgetLines(jobs, theme, width, lockedRows, []);
|
|
1345
|
+
const rendered = buildProgressiveWidgetLines(jobs, theme, width, lockedRows, [], frame);
|
|
1321
1346
|
widgetLayoutSession = { expanded, rows, columns, tier: "progressive", lockedRows, visibleJobKeys: rendered.visibleJobKeys };
|
|
1322
1347
|
return rendered.lines;
|
|
1323
1348
|
}
|
|
1324
1349
|
|
|
1325
|
-
function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (
|
|
1326
|
-
return (
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
? buildWidgetLines(jobs, theme, width, true)
|
|
1330
|
-
: jobs.length === 1
|
|
1331
|
-
? compactSingleWidgetLines(jobs[0]!, theme, width)
|
|
1332
|
-
: buildWidgetLines(jobs, theme, width, false);
|
|
1350
|
+
function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (tui: { requestRender(): void }, theme: Theme) => Component & { dispose?(): void } {
|
|
1351
|
+
return (tui, theme) => {
|
|
1352
|
+
const requestRender = () => tui.requestRender();
|
|
1353
|
+
widgetRequestRender = requestRender;
|
|
1333
1354
|
const container = new Container();
|
|
1334
|
-
|
|
1335
|
-
|
|
1355
|
+
container.render = (renderWidth: number): string[] => {
|
|
1356
|
+
const width = Math.max(0, renderWidth - 2);
|
|
1357
|
+
const frame = widgetAnimationFrame();
|
|
1358
|
+
const lines = expanded
|
|
1359
|
+
? buildWidgetLines(jobs, theme, width, true, frame)
|
|
1360
|
+
: jobs.length === 1
|
|
1361
|
+
? compactSingleWidgetLines(jobs[0]!, theme, width, frame)
|
|
1362
|
+
: buildWidgetLines(jobs, theme, width, false, frame);
|
|
1363
|
+
return fitAdaptiveWidgetLines(jobs, lines, theme, width, expanded, frame).map((line) => paddedWidgetLine(line, renderWidth));
|
|
1364
|
+
};
|
|
1365
|
+
const component = container as Component & { dispose?(): void };
|
|
1366
|
+
component.dispose = () => {
|
|
1367
|
+
if (widgetRequestRender === requestRender) widgetRequestRender = undefined;
|
|
1368
|
+
};
|
|
1369
|
+
return component;
|
|
1336
1370
|
};
|
|
1337
1371
|
}
|
|
1338
1372
|
|
|
1339
|
-
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false): string[] {
|
|
1373
|
+
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number): string[] {
|
|
1340
1374
|
if (jobs.length === 0) return [];
|
|
1341
|
-
if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded);
|
|
1375
|
+
if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame);
|
|
1342
1376
|
const running = jobs.filter((job) => job.status === "running");
|
|
1343
1377
|
const queued = jobs.filter((job) => job.status === "queued");
|
|
1344
1378
|
const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
|
|
1345
1379
|
|
|
1346
1380
|
const lines: string[] = [];
|
|
1347
1381
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
1348
|
-
const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running)) : hasActive ? "●" : "○";
|
|
1382
|
+
const headerGlyph = running.length > 0 ? runningGlyph(animatedSeed(widgetJobsRunningSeed(running), frame)) : hasActive ? "●" : "○";
|
|
1349
1383
|
lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", headerGlyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
|
|
1350
1384
|
|
|
1351
1385
|
const items: string[][] = [];
|
|
@@ -1358,7 +1392,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1358
1392
|
if (slots <= 0) { hiddenRunning++; continue; }
|
|
1359
1393
|
const stats = widgetStats(job, theme);
|
|
1360
1394
|
items.push([
|
|
1361
|
-
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1395
|
+
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1362
1396
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1363
1397
|
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1364
1398
|
]);
|
|
@@ -1375,7 +1409,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1375
1409
|
if (slots <= 0) { hiddenFinished++; continue; }
|
|
1376
1410
|
const stats = widgetStats(job, theme);
|
|
1377
1411
|
items.push([
|
|
1378
|
-
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1412
|
+
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1379
1413
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1380
1414
|
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1381
1415
|
]);
|
|
@@ -1412,6 +1446,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1412
1446
|
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
1413
1447
|
if (jobs.length === 0) {
|
|
1414
1448
|
resetWidgetLayoutSession();
|
|
1449
|
+
widgetRequestRender = undefined;
|
|
1415
1450
|
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
1416
1451
|
return;
|
|
1417
1452
|
}
|
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { Details } from "../shared/types.ts";
|
|
5
5
|
|
|
6
|
-
export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "
|
|
6
|
+
export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "live-card"] as const;
|
|
7
7
|
export type WorkflowChatProgressMode = typeof WORKFLOW_CHAT_PROGRESS_MODES[number];
|
|
8
8
|
export type ResolvedWorkflowChatProgressMode = Exclude<WorkflowChatProgressMode, "auto">;
|
|
9
9
|
|
|
@@ -86,7 +86,7 @@ export function resolveWorkflowChatProgress(input: ResolveWorkflowChatProgressIn
|
|
|
86
86
|
|
|
87
87
|
const requestedMode = requested.mode ?? "auto";
|
|
88
88
|
let mode: ResolvedWorkflowChatProgressMode;
|
|
89
|
-
if (requestedMode === "auto") mode = sameRepo
|
|
89
|
+
if (requestedMode === "auto") mode = sameRepo && !input.background ? "live-card" : "off";
|
|
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." };
|
|
@@ -175,7 +175,7 @@ export class WorkflowScriptError extends Error {
|
|
|
175
175
|
|
|
176
176
|
export interface RunWorkflowScriptOptions {
|
|
177
177
|
script: string;
|
|
178
|
-
timeoutMs
|
|
178
|
+
timeoutMs?: number;
|
|
179
179
|
signal?: AbortSignal;
|
|
180
180
|
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
181
181
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
@@ -261,7 +261,7 @@ function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowS
|
|
|
261
261
|
|
|
262
262
|
export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
|
|
263
263
|
if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
|
|
264
|
-
if (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1) throw new Error("workflow script timeout must be a positive integer.");
|
|
264
|
+
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
|
|
265
265
|
|
|
266
266
|
const worker = new Worker(WORKER_SOURCE, { eval: true });
|
|
267
267
|
const emits: unknown[] = [];
|
|
@@ -283,7 +283,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
283
283
|
const finish = (outcome: { value: unknown } | { error: Error }) => {
|
|
284
284
|
if (settled) return;
|
|
285
285
|
settled = true;
|
|
286
|
-
clearTimeout(timer);
|
|
286
|
+
if (timer) clearTimeout(timer);
|
|
287
287
|
options.signal?.removeEventListener("abort", onAbort);
|
|
288
288
|
void worker.terminate();
|
|
289
289
|
childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed; unawaited child launches are aborted."));
|
|
@@ -291,7 +291,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
291
291
|
else resolve({ value: outcome.value, ...partial() });
|
|
292
292
|
};
|
|
293
293
|
const onAbort = () => finish({ error: new Error("Workflow script aborted.") });
|
|
294
|
-
const timer =
|
|
294
|
+
const timer = options.timeoutMs === undefined
|
|
295
|
+
? undefined
|
|
296
|
+
: setTimeout(() => finish({ error: new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) }), options.timeoutMs);
|
|
295
297
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
296
298
|
if (options.signal?.aborted) return onAbort();
|
|
297
299
|
|