@ferris1225/pi-subagents 4.1.4 → 4.1.6
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/README.md +129 -116
- package/agents/cleaner.md +3 -2
- package/agents/documenter.md +6 -6
- package/agents/reviewer.md +8 -3
- package/agents/worker.md +4 -4
- package/package.json +1 -1
- package/src/config.ts +7 -5
- package/src/dispatch.ts +143 -43
- package/src/fixloop.ts +58 -32
- package/src/monitor.ts +54 -6
- package/src/prompt.ts +24 -26
- package/src/rpc-run.ts +26 -2
- package/src/thread-lifecycle.ts +103 -17
- package/src/tools.ts +49 -5
- package/src/widget.ts +94 -13
- package/src/worktree.ts +12 -0
package/src/widget.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
monitor,
|
|
11
11
|
statusIcon,
|
|
12
12
|
type RunView,
|
|
13
|
+
type WorkflowStage,
|
|
13
14
|
} from "./monitor.ts";
|
|
14
15
|
|
|
15
16
|
export const SUBAGENTS_WIDGET_ID = "pi-subagents";
|
|
@@ -38,6 +39,26 @@ function formatContinuationTask(label: string, task: string, width: number): str
|
|
|
38
39
|
return summary ? `${label}${separator}${summary}` : label;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/** Worktree-group badge shown on the row that owns the isolated worktree: the
|
|
43
|
+
* short group identity plus its integration state, so a workflow visibly moves
|
|
44
|
+
* through applying → applied (or retained) and a continuation worktree (new
|
|
45
|
+
* identity) is distinguishable from the original one. */
|
|
46
|
+
function worktreeBadge(run: RunView): string {
|
|
47
|
+
const id = run.worktreeId ?? "?";
|
|
48
|
+
switch (run.integrationStatus) {
|
|
49
|
+
case "finalizing":
|
|
50
|
+
return `wt:${id} applying`;
|
|
51
|
+
case "integrated":
|
|
52
|
+
return `wt:${id} applied`;
|
|
53
|
+
case "no_changes":
|
|
54
|
+
return `wt:${id} clean`;
|
|
55
|
+
case "retained":
|
|
56
|
+
return `wt:${id} retained`;
|
|
57
|
+
default:
|
|
58
|
+
return `wt:${id}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
/** One compact primary line per genuinely active run, plus an optional indented
|
|
42
63
|
* activity line. The primary line reserves stage model/thinking when present and
|
|
43
64
|
* elapsed width before truncating the task. Settled and parked threads never
|
|
@@ -48,23 +69,32 @@ function runPrimaryLine(
|
|
|
48
69
|
width: number,
|
|
49
70
|
now: number,
|
|
50
71
|
prefix: string,
|
|
72
|
+
isGroupOwner: boolean,
|
|
51
73
|
): string {
|
|
52
74
|
const dim = (text: string): string => theme.fg("dim", text);
|
|
53
|
-
const icon =
|
|
75
|
+
const icon = run.managedWorkflow && run.status === "running"
|
|
76
|
+
? theme.fg("accent", theme.bold("◆"))
|
|
77
|
+
: statusIcon(run.status, theme);
|
|
54
78
|
const displayName = run.managedWorkflow ? `${run.agent} workflow` : run.agent;
|
|
55
79
|
const name = theme.fg("accent", theme.bold(displayName));
|
|
56
|
-
|
|
80
|
+
// The stable run id is the handle for subagent_control/subagent_status; a
|
|
81
|
+
// queued run has not started, which must be visible at a glance. Its model
|
|
82
|
+
// is omitted too: the route is re-resolved when the run actually starts.
|
|
83
|
+
const queued = run.status === "queued";
|
|
84
|
+
const queuedTag = queued ? ` ${dim("· queued")}` : "";
|
|
85
|
+
const identity = `${prefix}${icon} #${run.id} ${name}${queuedTag}`;
|
|
57
86
|
const elapsed = formatElapsed(run, now);
|
|
58
87
|
// Render only the resolved model id plus thinking level. Provider auth and
|
|
59
88
|
// other configuration never enter monitor state or this line.
|
|
60
89
|
const modelId = run.model?.split("/").at(-1);
|
|
61
|
-
const modelSource = run.managedWorkflow
|
|
90
|
+
const modelSource = run.managedWorkflow || queued
|
|
62
91
|
? ""
|
|
63
92
|
: formatTaskSummary(
|
|
64
93
|
modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
|
|
65
94
|
64,
|
|
66
95
|
false,
|
|
67
96
|
);
|
|
97
|
+
const badge = isGroupOwner && run.isolation === "worktree" ? worktreeBadge(run) : "";
|
|
68
98
|
// A chain child shows its role in the chain plus a task-derived label; the
|
|
69
99
|
// templated fix brief itself would only repeat the parent review's content.
|
|
70
100
|
const continuation = run.parentRunId === undefined
|
|
@@ -72,11 +102,13 @@ function runPrimaryLine(
|
|
|
72
102
|
: undefined;
|
|
73
103
|
const taskSource = run.parentRunId !== undefined
|
|
74
104
|
? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(" · ")
|
|
75
|
-
:
|
|
105
|
+
: run.managedWorkflow
|
|
106
|
+
? run.label ?? formatTaskSummary(run.task, 64)
|
|
107
|
+
: formatTaskSummary(run.task, 64);
|
|
76
108
|
const taskDesiredSource = [continuation, taskSource]
|
|
77
109
|
.filter((part): part is string => Boolean(part))
|
|
78
110
|
.join(" · ");
|
|
79
|
-
const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
|
|
111
|
+
const primaryPartCount = 2 + (modelSource ? 1 : 0) + (badge ? 1 : 0) + (elapsed ? 1 : 0);
|
|
80
112
|
const contentWidth = Math.max(
|
|
81
113
|
0,
|
|
82
114
|
width -
|
|
@@ -125,10 +157,51 @@ function runPrimaryLine(
|
|
|
125
157
|
identity,
|
|
126
158
|
task ? dim(task) : undefined,
|
|
127
159
|
modelThinking ? dim(modelThinking) : undefined,
|
|
160
|
+
badge ? dim(badge) : undefined,
|
|
128
161
|
].filter((part): part is string => Boolean(part)).join(" · ");
|
|
129
162
|
return compactLine(primaryLeft, elapsed ? dim(`· ${elapsed}`) : "", width);
|
|
130
163
|
}
|
|
131
164
|
|
|
165
|
+
function workflowStageToken(stage: WorkflowStage, theme: Theme): string {
|
|
166
|
+
const content = (icon: string): string => `${icon} ${stage.relation}`;
|
|
167
|
+
switch (stage.status) {
|
|
168
|
+
case "done":
|
|
169
|
+
return theme.fg("success", content("✓"));
|
|
170
|
+
case "active":
|
|
171
|
+
return theme.fg("accent", theme.bold(content("●")));
|
|
172
|
+
case "changes":
|
|
173
|
+
return theme.fg("warning", content("!"));
|
|
174
|
+
case "failed":
|
|
175
|
+
return theme.fg("error", content("✗"));
|
|
176
|
+
default:
|
|
177
|
+
return theme.fg("dim", content("○"));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Render the real/currently planned stage sequence. On narrow terminals the
|
|
182
|
+
* active (or next actionable) stage becomes the left edge so it survives before
|
|
183
|
+
* older history; the workflow-wide elapsed tail is independently preserved on
|
|
184
|
+
* the primary line. */
|
|
185
|
+
function workflowTimelineLine(run: RunView, theme: Theme, width: number): string | undefined {
|
|
186
|
+
const stages = run.workflowStages;
|
|
187
|
+
const indent = " ";
|
|
188
|
+
if (!stages || stages.length === 0 || width <= visibleWidth(indent)) return undefined;
|
|
189
|
+
const separator = theme.fg("dim", " ─ ");
|
|
190
|
+
const render = (items: readonly WorkflowStage[]): string =>
|
|
191
|
+
items.map((stage) => workflowStageToken(stage, theme)).join(separator);
|
|
192
|
+
const full = `${indent}${render(stages)}`;
|
|
193
|
+
if (visibleWidth(full) <= width) return full;
|
|
194
|
+
|
|
195
|
+
let focusIndex = stages.findIndex((stage) => stage.status === "active");
|
|
196
|
+
if (focusIndex === -1) {
|
|
197
|
+
focusIndex = stages.findLastIndex((stage) => stage.status === "changes" || stage.status === "failed");
|
|
198
|
+
}
|
|
199
|
+
if (focusIndex === -1) focusIndex = stages.findIndex((stage) => stage.status === "pending");
|
|
200
|
+
if (focusIndex === -1) focusIndex = stages.length - 1;
|
|
201
|
+
const omittedPrefix = focusIndex > 0 ? theme.fg("dim", "… ─ ") : "";
|
|
202
|
+
return truncateToWidth(`${indent}${omittedPrefix}${render(stages.slice(focusIndex))}`, width, "…");
|
|
203
|
+
}
|
|
204
|
+
|
|
132
205
|
function runActivityLine(run: RunView, theme: Theme, width: number, indent: string): string[] {
|
|
133
206
|
const dim = (text: string): string => theme.fg("dim", text);
|
|
134
207
|
const activity = run.activity?.trim();
|
|
@@ -140,9 +213,10 @@ function runActivityLine(run: RunView, theme: Theme, width: number, indent: stri
|
|
|
140
213
|
return [truncateToWidth(`${indent}${dim(activitySummary)}`, width, "")];
|
|
141
214
|
}
|
|
142
215
|
|
|
143
|
-
/** Render active runs as
|
|
144
|
-
*
|
|
145
|
-
* include their source id; other
|
|
216
|
+
/** Render active runs as compact workflow-aware trees. Stable managed parents
|
|
217
|
+
* retain their stage timeline while the current internal child supplies exact
|
|
218
|
+
* model/thinking/activity telemetry. Fork labels include their source id; other
|
|
219
|
+
* control ids remain available through status. */
|
|
146
220
|
export function formatActiveRunLines(
|
|
147
221
|
runs: readonly RunView[],
|
|
148
222
|
theme: Theme,
|
|
@@ -165,13 +239,20 @@ export function formatActiveRunLines(
|
|
|
165
239
|
const lines: string[] = [];
|
|
166
240
|
for (const root of roots) {
|
|
167
241
|
const children = childrenOf.get(root.id) ?? [];
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
242
|
+
// Roots (including orphaned chain children whose parent row is gone) own
|
|
243
|
+
// their worktree group; nested children inherit the group via the tree.
|
|
244
|
+
lines.push(runPrimaryLine(root, theme, width, now, "", true));
|
|
245
|
+
const hasTimeline = Boolean(root.managedWorkflow && root.workflowStages?.length);
|
|
246
|
+
const timeline = hasTimeline ? workflowTimelineLine(root, theme, width) : undefined;
|
|
247
|
+
if (timeline) lines.push(timeline);
|
|
248
|
+
// A parent placeholder ("managed workflow running") would duplicate the
|
|
249
|
+
// timeline. Standalone roots keep their useful activity line as before.
|
|
250
|
+
if (children.length === 0 && !hasTimeline) {
|
|
251
|
+
lines.push(...runActivityLine(root, theme, width, " "));
|
|
252
|
+
}
|
|
172
253
|
children.forEach((child, index) => {
|
|
173
254
|
const connector = index === children.length - 1 ? "└ " : "├ ";
|
|
174
|
-
lines.push(runPrimaryLine(child, theme, width, now, theme.fg("dim", ` ${connector}`)));
|
|
255
|
+
lines.push(runPrimaryLine(child, theme, width, now, theme.fg("dim", ` ${connector}`), false));
|
|
175
256
|
lines.push(...runActivityLine(child, theme, width, " "));
|
|
176
257
|
});
|
|
177
258
|
}
|
package/src/worktree.ts
CHANGED
|
@@ -16,6 +16,18 @@ import { isAbsolute, join, relative, resolve } from "node:path";
|
|
|
16
16
|
|
|
17
17
|
export type IsolationMode = "shared" | "worktree";
|
|
18
18
|
|
|
19
|
+
const WORKTREE_TEMP_DIR_PREFIX = "pi-subagent-worktree-";
|
|
20
|
+
|
|
21
|
+
/** Short stable identity of one isolated worktree group (the mkdtemp suffix).
|
|
22
|
+
* Continuation/fork generations create a fresh worktree, so the identity
|
|
23
|
+
* visibly changes when the group's filesystem boundary changes. */
|
|
24
|
+
export function worktreeGroupId(worktree: Pick<WorktreeIsolation, "tempDir">): string {
|
|
25
|
+
const base = worktree.tempDir.split(/[\\/]/).filter(Boolean).pop() ?? worktree.tempDir;
|
|
26
|
+
return base.startsWith(WORKTREE_TEMP_DIR_PREFIX)
|
|
27
|
+
? base.slice(WORKTREE_TEMP_DIR_PREFIX.length)
|
|
28
|
+
: base;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
export interface CommandRunOptions {
|
|
20
32
|
cwd: string;
|
|
21
33
|
input?: Buffer;
|