pi-subagents 0.41.0 → 0.42.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 +21 -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 +1 -1
- 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/execution.ts +19 -7
- package/src/runs/foreground/subagent-executor.ts +16 -4
- 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/tui/fleet.ts +1 -1
- package/src/tui/render.ts +77 -43
- package/src/types/pi-runtime-compat.d.ts +0 -6
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/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,11 @@ 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
|
+
const text = ` ${line} `;
|
|
1295
|
+
return `${text}${" ".repeat(Math.max(0, width - visibleWidth(text)))}`;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1274
1298
|
function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
|
|
1275
1299
|
const rows = process.stdout.rows || 30;
|
|
1276
1300
|
const budget = expanded
|
|
@@ -1285,7 +1309,7 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
|
|
|
1285
1309
|
return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
|
|
1286
1310
|
}
|
|
1287
1311
|
|
|
1288
|
-
function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
|
|
1312
|
+
function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
|
|
1289
1313
|
if (expanded) {
|
|
1290
1314
|
resetWidgetLayoutSession();
|
|
1291
1315
|
return fitWidgetLineBudget(lines, theme, width, true);
|
|
@@ -1297,11 +1321,11 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
|
|
|
1297
1321
|
const availableRows = estimateAvailableWidgetRows();
|
|
1298
1322
|
|
|
1299
1323
|
if (hasMatchingSession && widgetLayoutSession?.tier === "single-line") {
|
|
1300
|
-
return buildSingleLineWidgetLines(jobs, theme, width);
|
|
1324
|
+
return buildSingleLineWidgetLines(jobs, theme, width, frame);
|
|
1301
1325
|
}
|
|
1302
1326
|
|
|
1303
1327
|
if (hasMatchingSession && widgetLayoutSession?.tier === "progressive" && widgetLayoutSession.lockedRows !== undefined) {
|
|
1304
|
-
const rendered = buildProgressiveWidgetLines(jobs, theme, width, widgetLayoutSession.lockedRows, widgetLayoutSession.visibleJobKeys);
|
|
1328
|
+
const rendered = buildProgressiveWidgetLines(jobs, theme, width, widgetLayoutSession.lockedRows, widgetLayoutSession.visibleJobKeys, frame);
|
|
1305
1329
|
widgetLayoutSession.visibleJobKeys = rendered.visibleJobKeys;
|
|
1306
1330
|
return rendered.lines;
|
|
1307
1331
|
}
|
|
@@ -1313,39 +1337,48 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
|
|
|
1313
1337
|
|
|
1314
1338
|
if (availableRows <= 2) {
|
|
1315
1339
|
widgetLayoutSession = { expanded, rows, columns, tier: "single-line", visibleJobKeys: [] };
|
|
1316
|
-
return buildSingleLineWidgetLines(jobs, theme, width);
|
|
1340
|
+
return buildSingleLineWidgetLines(jobs, theme, width, frame);
|
|
1317
1341
|
}
|
|
1318
1342
|
|
|
1319
1343
|
const lockedRows = Math.min(availableRows, collapsedWidgetLineBudget(rows));
|
|
1320
|
-
const rendered = buildProgressiveWidgetLines(jobs, theme, width, lockedRows, []);
|
|
1344
|
+
const rendered = buildProgressiveWidgetLines(jobs, theme, width, lockedRows, [], frame);
|
|
1321
1345
|
widgetLayoutSession = { expanded, rows, columns, tier: "progressive", lockedRows, visibleJobKeys: rendered.visibleJobKeys };
|
|
1322
1346
|
return rendered.lines;
|
|
1323
1347
|
}
|
|
1324
1348
|
|
|
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);
|
|
1349
|
+
function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (tui: { requestRender(): void }, theme: Theme) => Component & { dispose?(): void } {
|
|
1350
|
+
return (tui, theme) => {
|
|
1351
|
+
const requestRender = () => tui.requestRender();
|
|
1352
|
+
widgetRequestRender = requestRender;
|
|
1333
1353
|
const container = new Container();
|
|
1334
|
-
|
|
1335
|
-
|
|
1354
|
+
container.render = (renderWidth: number): string[] => {
|
|
1355
|
+
const width = getTermWidth();
|
|
1356
|
+
const frame = widgetAnimationFrame();
|
|
1357
|
+
const lines = expanded
|
|
1358
|
+
? buildWidgetLines(jobs, theme, width, true, frame)
|
|
1359
|
+
: jobs.length === 1
|
|
1360
|
+
? compactSingleWidgetLines(jobs[0]!, theme, width, frame)
|
|
1361
|
+
: buildWidgetLines(jobs, theme, width, false, frame);
|
|
1362
|
+
return fitAdaptiveWidgetLines(jobs, lines, theme, width, expanded, frame).map((line) => paddedWidgetLine(line, renderWidth));
|
|
1363
|
+
};
|
|
1364
|
+
const component = container as Component & { dispose?(): void };
|
|
1365
|
+
component.dispose = () => {
|
|
1366
|
+
if (widgetRequestRender === requestRender) widgetRequestRender = undefined;
|
|
1367
|
+
};
|
|
1368
|
+
return component;
|
|
1336
1369
|
};
|
|
1337
1370
|
}
|
|
1338
1371
|
|
|
1339
|
-
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false): string[] {
|
|
1372
|
+
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number): string[] {
|
|
1340
1373
|
if (jobs.length === 0) return [];
|
|
1341
|
-
if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded);
|
|
1374
|
+
if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame);
|
|
1342
1375
|
const running = jobs.filter((job) => job.status === "running");
|
|
1343
1376
|
const queued = jobs.filter((job) => job.status === "queued");
|
|
1344
1377
|
const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
|
|
1345
1378
|
|
|
1346
1379
|
const lines: string[] = [];
|
|
1347
1380
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
1348
|
-
const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running)) : hasActive ? "●" : "○";
|
|
1381
|
+
const headerGlyph = running.length > 0 ? runningGlyph(animatedSeed(widgetJobsRunningSeed(running), frame)) : hasActive ? "●" : "○";
|
|
1349
1382
|
lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", headerGlyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
|
|
1350
1383
|
|
|
1351
1384
|
const items: string[][] = [];
|
|
@@ -1358,7 +1391,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1358
1391
|
if (slots <= 0) { hiddenRunning++; continue; }
|
|
1359
1392
|
const stats = widgetStats(job, theme);
|
|
1360
1393
|
items.push([
|
|
1361
|
-
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1394
|
+
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1362
1395
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1363
1396
|
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1364
1397
|
]);
|
|
@@ -1375,7 +1408,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1375
1408
|
if (slots <= 0) { hiddenFinished++; continue; }
|
|
1376
1409
|
const stats = widgetStats(job, theme);
|
|
1377
1410
|
items.push([
|
|
1378
|
-
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1411
|
+
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1379
1412
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1380
1413
|
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1381
1414
|
]);
|
|
@@ -1412,6 +1445,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1412
1445
|
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
1413
1446
|
if (jobs.length === 0) {
|
|
1414
1447
|
resetWidgetLayoutSession();
|
|
1448
|
+
widgetRequestRender = undefined;
|
|
1415
1449
|
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
1416
1450
|
return;
|
|
1417
1451
|
}
|